diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bc30adbd48c..864f3db6fe7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1078,7 +1078,7 @@ region save --- #### GPS anti-spoofing guard (When built with `GPS_GUARD`) -With `GPS_GUARD`, a fix becomes the node's last fix (position, telemetry, adverts) only after it passes plausibility checks: at least `GPS_GUARD_MIN_SATS` satellites, HDOP ≤ `GPS_GUARD_MAX_HDOP10`/10, inside the optional build-time region (`GPS_GUARD_LAT_MIN/MAX`, `GPS_GUARD_LON_MIN/MAX`), within the fence around the node's configured `lat`/`lon` (if set), no jump faster than `GPS_GUARD_MAX_SPEED_KMH`, and `GPS_GUARD_STABLE_FIXES` consistent fixes in a row. An unverified position is never reported. +With `GPS_GUARD`, a fix becomes the node's last fix (position, telemetry, adverts) only after it passes plausibility checks: a position on Earth (latitude within ±90°, longitude within ±180°), at least `GPS_GUARD_MIN_SATS` satellites, HDOP ≤ `GPS_GUARD_MAX_HDOP10`/10, inside the optional build-time region (`GPS_GUARD_LAT_MIN/MAX`, `GPS_GUARD_LON_MIN/MAX`), within the fence around the node's configured `lat`/`lon` (if set), no jump faster than `GPS_GUARD_MAX_SPEED_KMH`, and `GPS_GUARD_STABLE_FIXES` consistent fixes in a row. An unverified position is never reported. GPS time is written to the clock only while such a fix is current, never earlier than the firmware build date, and, once the clock holds a plausible time (from the build date to 10 years after it), only within `GPS_GUARD_TIME_MAX_STEP` seconds for the first sync and within clock drift (`GPS_GUARD_TIME_DRIFT_BASE` s + `GPS_GUARD_TIME_DRIFT_PPM`) afterwards. Setting the clock manually (`time`, `clock sync`) resets that anchor. A clock outside that window (e.g. a bogus date in 2080) is corrected by the first trusted fix. diff --git a/src/helpers/sensors/GpsGuard.h b/src/helpers/sensors/GpsGuard.h index 46b1936caae..7108939ca3b 100644 --- a/src/helpers/sensors/GpsGuard.h +++ b/src/helpers/sensors/GpsGuard.h @@ -144,7 +144,9 @@ class GpsGuard { const char* why = NULL; double dist = 0; - if (sats < GPS_GUARD_MIN_SATS) { + if (lat_e6 < -90000000L || lat_e6 > 90000000L || lon_e6 < -180000000L || lon_e6 > 180000000L) { + why = "coord"; // not a position on Earth (e.g. a garbled sentence that still passes its checksum) + } else if (sats < GPS_GUARD_MIN_SATS) { why = "sats"; } else if (hdop10 == 255 || hdop10 > GPS_GUARD_MAX_HDOP10) { why = "hdop"; diff --git a/test/mocks/RTClib.h b/test/mocks/RTClib.h new file mode 100644 index 00000000000..05805271838 --- /dev/null +++ b/test/mocks/RTClib.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +// Just enough of RTClib's DateTime for GpsGuard: the firmware build date as an epoch. +class DateTime { + uint32_t _unix = 0; + +public: + // __DATE__ ("Mmm dd yyyy") and __TIME__ ("hh:mm:ss") + DateTime(const char* date, const char* time) { + static const char* months = "JanFebMarAprMayJunJulAugSepOctNovDec"; + int mon = 1; + for (int i = 0; i < 12; ++i) if (strncmp(date, months + 3 * i, 3) == 0) mon = i + 1; + const int day = atoi(date + 4), year = atoi(date + 7); + // days from civil (Howard Hinnant) + const int y = year - (mon <= 2); + const int era = y / 400, yoe = y - era * 400; + const int doy = (153 * (mon + (mon > 2 ? -3 : 9)) + 2) / 5 + day - 1; + const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + const long days = (long)era * 146097 + doe - 719468; + _unix = (uint32_t)(days * 86400L + atoi(time) * 3600L + atoi(time + 3) * 60L + atoi(time + 6)); + } + uint32_t unixtime() const { return _unix; } +}; diff --git a/test/test_gps_guard/test_gps_guard_range.cpp b/test/test_gps_guard/test_gps_guard_range.cpp new file mode 100644 index 00000000000..ae413f42701 --- /dev/null +++ b/test/test_gps_guard/test_gps_guard_range.cpp @@ -0,0 +1,44 @@ +#include +#include + +// A fix is judged on its position alone here: 8 satellites, HDOP 0.9, no reference +// position, no earlier fix. Positions are in micro-degrees, as MicroNMEA reports them. +static bool rejectedAsCoord(long lat_e6, long lon_e6) { + GpsGuard guard; + guard.acceptFix(lat_e6, lon_e6, 8, 9, 1800000000UL); + char status[64]; + guard.formatStatus(status, sizeof(status)); + return strstr(status, "rej coord") != nullptr; +} + +TEST(GpsGuardRange, ordinaryPositionIsNotRejected) { + EXPECT_FALSE(rejectedAsCoord(47268723L, -122559463L)); + EXPECT_FALSE(rejectedAsCoord(0L, 0L)); +} + +TEST(GpsGuardRange, limitsThemselvesAreAccepted) { + EXPECT_FALSE(rejectedAsCoord(90000000L, 180000000L)); + EXPECT_FALSE(rejectedAsCoord(-90000000L, -180000000L)); +} + +TEST(GpsGuardRange, latitudeOutsideRangeIsRejected) { + EXPECT_TRUE(rejectedAsCoord(90000001L, 0L)); + EXPECT_TRUE(rejectedAsCoord(-90000001L, 0L)); + EXPECT_TRUE(rejectedAsCoord(100666665L, 181650000L)); // what a garbled "9999.9999,N" reports +} + +TEST(GpsGuardRange, longitudeOutsideRangeIsRejected) { + EXPECT_TRUE(rejectedAsCoord(0L, 180000001L)); + EXPECT_TRUE(rejectedAsCoord(0L, -180000001L)); +} + +TEST(GpsGuardRange, guardOffAcceptsEverything) { + GpsGuard guard; + guard.setEnabled(false); + EXPECT_TRUE(guard.acceptFix(100666665L, 181650000L, 8, 9, 1800000000UL)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}