sec(sender): ACK rate-limiting, unknown device-ID rejection, fuzz tests
- Add 500 ms minimum interval between accepted ACKs to mitigate replay floods - Reject ACK packets from unrecognised device IDs (DeviceIdMismatch) - Add test/test_security_fuzz: negative/boundary tests for decode_batch, uleb128_decode, svarint_decode, lora_parse_frame entry points
This commit is contained in:
@@ -126,6 +126,9 @@ static uint32_t g_sender_rx_reject_log_ms = 0;
|
||||
static RxRejectReason g_receiver_rx_reject_reason = RxRejectReason::None;
|
||||
static uint32_t g_receiver_rx_reject_log_ms = 0;
|
||||
static MeterData g_last_meter_data = {};
|
||||
// Rate-limit: track ACK accept timestamps to detect replay floods.
|
||||
static uint32_t g_ack_accept_last_ms = 0;
|
||||
static constexpr uint32_t ACK_MIN_INTERVAL_MS = 500;
|
||||
static bool g_last_meter_valid = false;
|
||||
static uint32_t g_last_meter_rx_ms = 0;
|
||||
static uint32_t g_meter_stale_seconds = 0;
|
||||
@@ -1422,32 +1425,49 @@ static void sender_loop() {
|
||||
static_cast<unsigned>(ack_pkt.payload_len),
|
||||
ack_id);
|
||||
}
|
||||
} else if (sender_id_from_short_id(ack_pkt.device_id_short) == 0 &&
|
||||
ack_pkt.device_id_short != g_short_id) {
|
||||
// Reject ACKs from unknown device IDs to prevent spoofing.
|
||||
sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack");
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("ack: reject device_id=%04X (unknown)",
|
||||
ack_pkt.device_id_short);
|
||||
}
|
||||
} else {
|
||||
uint8_t time_valid = ack_pkt.payload[0] & 0x01;
|
||||
uint16_t ack_id = read_u16_be(&ack_pkt.payload[1]);
|
||||
uint32_t ack_epoch = read_u32_be(&ack_pkt.payload[3]);
|
||||
bool set_time = false;
|
||||
if (g_batch_ack_pending && ack_id == g_last_sent_batch_id) {
|
||||
ack_accepted = true;
|
||||
g_sender_ack_rtt_last_ms = rx_elapsed;
|
||||
if (g_sender_ack_rtt_ewma_ms == 0) {
|
||||
g_sender_ack_rtt_ewma_ms = rx_elapsed;
|
||||
// Rate-limit: reject if another ACK was accepted less than ACK_MIN_INTERVAL_MS ago
|
||||
if (g_ack_accept_last_ms != 0 && (millis() - g_ack_accept_last_ms < ACK_MIN_INTERVAL_MS)) {
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("ack: rate-limited (last accepted %lums ago)",
|
||||
static_cast<unsigned long>(millis() - g_ack_accept_last_ms));
|
||||
}
|
||||
} else {
|
||||
g_sender_ack_rtt_ewma_ms = (g_sender_ack_rtt_ewma_ms * 3U + rx_elapsed + 1U) / 4U;
|
||||
ack_accepted = true;
|
||||
g_ack_accept_last_ms = millis();
|
||||
g_sender_ack_rtt_last_ms = rx_elapsed;
|
||||
if (g_sender_ack_rtt_ewma_ms == 0) {
|
||||
g_sender_ack_rtt_ewma_ms = rx_elapsed;
|
||||
} else {
|
||||
g_sender_ack_rtt_ewma_ms = (g_sender_ack_rtt_ewma_ms * 3U + rx_elapsed + 1U) / 4U;
|
||||
}
|
||||
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||
time_set_utc(ack_epoch);
|
||||
g_time_acquired = true;
|
||||
sender_reset_fault_stats_on_first_sync(ack_epoch);
|
||||
set_time = true;
|
||||
}
|
||||
g_last_acked_batch_id = ack_id;
|
||||
serial_debug_printf("ack: rx ok batch_id=%u time_valid=%u epoch=%lu set=%u",
|
||||
ack_id,
|
||||
static_cast<unsigned>(time_valid),
|
||||
static_cast<unsigned long>(ack_epoch),
|
||||
set_time ? 1 : 0);
|
||||
finish_inflight_batch();
|
||||
}
|
||||
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||
time_set_utc(ack_epoch);
|
||||
g_time_acquired = true;
|
||||
sender_reset_fault_stats_on_first_sync(ack_epoch);
|
||||
set_time = true;
|
||||
}
|
||||
g_last_acked_batch_id = ack_id;
|
||||
serial_debug_printf("ack: rx ok batch_id=%u time_valid=%u epoch=%lu set=%u",
|
||||
ack_id,
|
||||
static_cast<unsigned>(time_valid),
|
||||
static_cast<unsigned long>(ack_epoch),
|
||||
set_time ? 1 : 0);
|
||||
finish_inflight_batch();
|
||||
} else {
|
||||
if (ack_id != g_last_sent_batch_id) {
|
||||
sender_note_rx_reject(RxRejectReason::BatchIdMismatch, "ack");
|
||||
|
||||
Reference in New Issue
Block a user