Stop charging a rekey msg1 refused by a missing interface to the peer

When the rekey initiator's msg1 send failed, the cycle was abandoned
cleanly and retried when rekey next came due, but every failure was
recorded as the reject that means the remote sent something invalid. A
send refused because the interface under the transport is absent or
mid-rebind is a local, self-clearing condition, so an interface flap was
still charged to the peer. A transient refusal is now logged at debug and
not counted; a terminal one is still warned and counted.

Tests drive the rekey check against an established peer on an Ethernet
transport bound to an absent interface and on a transport that was never
started.

No wire format change.
This commit is contained in:
Johnathan Corgan
2026-10-02 17:53:37 +00:00
parent 4cd46ce6f6
commit 0e0116b957
2 changed files with 74 additions and 6 deletions
+6 -6
View File
@@ -429,10 +429,10 @@ impl Node {
// The teardown here is already benign — this returns before
// `set_rekey_state`, so the cycle simply does not start and
// is retried when rekey next comes due, with nothing torn
// down and nothing charged to the peer. Only the severity
// is wrong for a transport between interfaces, which is a
// local and self-clearing condition the presence machine
// has already reported.
// down. A transport between interfaces is a local and
// self-clearing condition the presence machine has already
// reported, so it is logged at debug and not charged to
// the peer as a reject; a terminal error is both.
if e.is_transient() {
debug!(
peer = %self.peer_display_name(node_addr),
@@ -445,10 +445,10 @@ impl Node {
error = %e,
"Failed to send rekey msg1"
);
self.stats_mut()
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
}
let _ = self.index_allocator.free(our_index);
self.stats_mut()
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
return;
}
}
+68
View File
@@ -5612,3 +5612,71 @@ async fn a_transient_msg2_failure_from_an_established_peer_address_leaves_the_fr
#[cfg(debug_assertions)]
node.debug_assert_peer_maps_coherent();
}
// ===========================================================================
// A rekey msg1 send refused because the interface under the transport is
// absent or mid-rebind is not charged to the peer as a reject; a terminal
// refusal still is. Either way the cycle does not start and is retried when
// rekey next comes due.
// ===========================================================================
/// Establish a peer on a rekey-enabled node whose one transport refuses every
/// send, age the session past the rekey trigger, and run the rekey check.
/// With `transient` the transport is Ethernet on an absent interface
/// (`InterfaceUnavailable`); otherwise it is a UDP transport that was never
/// started (`NotStarted`, terminal). Returns how many `BadState` rejects the
/// refused rekey msg1 recorded.
#[cfg(any(target_os = "linux", target_os = "macos"))]
async fn rekey_rejects(transient: bool) -> u64 {
use crate::config::UdpConfig;
use crate::transport::udp::UdpTransport;
let transport_id = TransportId::new(1);
let (mut node, addr) = if transient {
let node = absent_node(transport_id, rekey_config()).await;
(node, TransportAddr::from_string("aa:bb:cc:dd:ee:ff"))
} else {
let mut node = make_node_with(rekey_config());
node.supervisor.state = NodeState::Running;
let (tx, _rx) = packet_channel(8);
let udp = UdpTransport::new(transport_id, None, UdpConfig::default(), tx);
node.transports
.insert(transport_id, TransportHandle::Udp(udp));
(node, TransportAddr::from_string("127.0.0.1:5000"))
};
let (peer_addr, _, _) = seed_peer(&mut node, transport_id, &addr);
node.get_peer_mut(&peer_addr)
.unwrap()
.test_backdate_session_established(std::time::Duration::from_secs(120));
let rejects_before = node.stats().handshake.bad_state;
node.check_rekey().await;
assert!(
!node.get_peer(&peer_addr).unwrap().rekey_in_progress(),
"a refused rekey msg1 must leave no rekey cycle running"
);
#[cfg(debug_assertions)]
node.debug_assert_peer_maps_coherent();
node.stats().handshake.bad_state - rejects_before
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test]
async fn a_transient_rekey_msg1_send_failure_is_not_charged_to_the_peer() {
assert_eq!(
rekey_rejects(true).await,
0,
"a local interface flap must not be recorded as the peer's misbehaviour"
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test]
async fn a_terminal_rekey_msg1_send_failure_is_recorded_as_a_reject() {
assert_eq!(
rekey_rejects(false).await,
1,
"a terminal send failure is still counted"
);
}