Skip to content

Commit 98efc13

Browse files
refactor: simplify passive keepalive (#70)
WireGuard has a passive keepalive mechanism. To quote the spec: > If a peer has received a validly-authenticated transport data message (section 5.4.6), but does not have any packets itself to send back for Keepalive-Timeout seconds, it sends a keepalive message. This is currently implemented correctly in `boringtun` but it is somewhat convoluted. 1. On various parts in the code, the internal timers are ticked over. For example, when we receive keepalive messages or data messages. 2. Whether or not we should send a passive keepalive is tracked in the `want_keepalive` boolean. 3. This boolean is set whenever we receive _any_ packet (including keepalives). This is a bug. 4. The above bug is mitigated because of an additional condition that the last received data packet must be after the last sent packet. 5. Lastly, the `want_keepalive` boolean is checked and directly set to false as part of our timer code. Combining these two things makes the code hard to reason about. We can simplify this greatly by directly tracking the timestamp, when a keepalive is due. The new `want_keepalive_at` timer is set every time we receive a data packet and cleared every time we send a packet. In `update_timers_at`, we simply check if `now` has surpassed that timer and send a keepalive if that is the case. As a bonus, this functionality is now also unit-tested.
1 parent 0b87029 commit 98efc13

File tree

2 files changed

+102
-11
lines changed

2 files changed

+102
-11
lines changed

boringtun/src/noise/mod.rs

+84-1
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,8 @@ mod tests {
717717
use super::*;
718718
use rand::{rngs::OsRng, RngCore};
719719
use timers::{KEEPALIVE_TIMEOUT, MAX_JITTER, REJECT_AFTER_TIME, SHOULD_NOT_USE_AFTER_TIME};
720-
use tracing::Level;
720+
use tracing::{level_filters::LevelFilter, Level};
721+
use tracing_subscriber::util::SubscriberInitExt;
721722

722723
fn create_two_tuns(now: Instant) -> (Tunn, Tunn) {
723724
let my_secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng);
@@ -1050,6 +1051,88 @@ mod tests {
10501051
assert_eq!(sent_packet_buf, recv_packet_buf);
10511052
}
10521053

1054+
#[test]
1055+
fn silent_without_application_traffic_and_persistent_keepalive() {
1056+
let _guard = tracing_subscriber::fmt()
1057+
.with_test_writer()
1058+
.with_max_level(LevelFilter::DEBUG)
1059+
.set_default();
1060+
1061+
let mut now = Instant::now();
1062+
1063+
let (mut my_tun, mut their_tun) = create_two_tuns_and_handshake(now);
1064+
assert_eq!(my_tun.persistent_keepalive(), None);
1065+
their_tun.set_persistent_keepalive(10);
1066+
1067+
let mut my_dst = [0u8; 1024];
1068+
let mut their_dst = [0u8; 1024];
1069+
1070+
now += Duration::from_secs(1);
1071+
1072+
let sent_packet_buf = create_ipv4_udp_packet();
1073+
1074+
// First, perform an application-level handshake.
1075+
1076+
{
1077+
// Send the request.
1078+
1079+
let data = my_tun
1080+
.encapsulate_at(&sent_packet_buf, &mut my_dst, now)
1081+
.unwrap_network();
1082+
1083+
now += Duration::from_secs(1);
1084+
1085+
let data = their_tun.decapsulate_at(None, data, &mut their_dst, now);
1086+
assert!(matches!(data, TunnResult::WriteToTunnelV4(..)));
1087+
}
1088+
1089+
now += Duration::from_secs(1);
1090+
1091+
{
1092+
// Send the response.
1093+
1094+
let data = their_tun
1095+
.encapsulate_at(&sent_packet_buf, &mut their_dst, now)
1096+
.unwrap_network();
1097+
1098+
now += Duration::from_secs(1);
1099+
1100+
let data = my_tun.decapsulate_at(None, data, &mut my_dst, now);
1101+
assert!(matches!(data, TunnResult::WriteToTunnelV4(..)));
1102+
}
1103+
1104+
// Wait for `KEEPALIVE_TIMEOUT`.
1105+
1106+
now += KEEPALIVE_TIMEOUT;
1107+
1108+
let keepalive = my_tun.update_timers_at(&mut my_dst, now).unwrap_network();
1109+
parse_keepalive(&mut their_tun, keepalive, now);
1110+
1111+
// Idle for 60 seconds.
1112+
1113+
for _ in 0..60 {
1114+
now += Duration::from_secs(1);
1115+
1116+
// `my_tun` stays silent (i.e. does not respond to keepalives with keepalives).
1117+
assert!(matches!(
1118+
my_tun.update_timers_at(&mut my_dst, now),
1119+
TunnResult::Done
1120+
));
1121+
1122+
// `their_tun` will emit persistent keep-alives as we idle.
1123+
match their_tun.update_timers_at(&mut their_dst, now) {
1124+
TunnResult::Done => {}
1125+
TunnResult::Err(wire_guard_error) => panic!("{wire_guard_error}"),
1126+
TunnResult::WriteToNetwork(keepalive) => {
1127+
parse_keepalive(&mut my_tun, keepalive, now)
1128+
}
1129+
TunnResult::WriteToTunnelV4(_, _) | TunnResult::WriteToTunnelV6(_, _) => {
1130+
unreachable!()
1131+
}
1132+
}
1133+
}
1134+
}
1135+
10531136
#[test]
10541137
fn rekey_without_response() {
10551138
let _guard = tracing_subscriber::fmt()

boringtun/src/noise/timers.rs

+18-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
use super::errors::WireGuardError;
55
use crate::noise::{Tunn, TunnResult, N_SESSIONS};
6-
use std::mem;
76
use std::ops::{Index, IndexMut};
87

98
use rand::Rng;
@@ -57,7 +56,9 @@ pub struct Timers {
5756
is_initiator: bool,
5857
timers: [Instant; TimerName::Top as usize],
5958
/// Did we receive data without sending anything back?
60-
want_keepalive: bool,
59+
///
60+
/// If `Some`, tracks the timestamp when we want to send a keepalive.
61+
want_passive_keepalive_at: Option<Instant>,
6162
/// Did we send data without hearing back?
6263
///
6364
/// If `Some`, tracks the timestamp when we want to initiate the new handshake.
@@ -81,7 +82,7 @@ impl Timers {
8182
Timers {
8283
is_initiator: false,
8384
timers: [now; TimerName::Top as usize],
84-
want_keepalive: Default::default(),
85+
want_passive_keepalive_at: Default::default(),
8586
want_handshake_at: Default::default(),
8687
persistent_keepalive: usize::from(persistent_keepalive.unwrap_or(0)),
8788
should_reset_rr: reset_rr,
@@ -105,7 +106,7 @@ impl Timers {
105106
*t = now;
106107
}
107108
self.want_handshake_at = None;
108-
self.want_keepalive = false;
109+
self.want_passive_keepalive_at = None;
109110
}
110111
}
111112

@@ -126,11 +127,13 @@ impl Tunn {
126127
pub(super) fn timer_tick(&mut self, timer_name: TimerName, now: Instant) {
127128
match timer_name {
128129
TimeLastPacketReceived => {
129-
self.timers.want_keepalive = true;
130130
self.timers.want_handshake_at = None;
131131
}
132132
TimeLastPacketSent => {
133-
self.timers.want_keepalive = false;
133+
self.timers.want_passive_keepalive_at = None;
134+
}
135+
TimeLastDataPacketReceived => {
136+
self.timers.want_passive_keepalive_at = Some(now + KEEPALIVE_TIMEOUT);
134137
}
135138
TimeLastDataPacketSent => {
136139
match self.timers.want_handshake_at {
@@ -231,7 +234,6 @@ impl Tunn {
231234
// Load timers only once:
232235
let session_established = self.timers[TimeSessionEstablished];
233236
let handshake_started = self.timers[TimeLastHandshakeStarted];
234-
let aut_packet_sent = self.timers[TimeLastPacketSent];
235237
let data_packet_received = self.timers[TimeLastDataPacketReceived];
236238
let data_packet_sent = self.timers[TimeLastDataPacketSent];
237239
let persistent_keepalive = self.timers.persistent_keepalive;
@@ -325,9 +327,10 @@ impl Tunn {
325327
if !handshake_initiation_required {
326328
// If a packet has been received from a given peer, but we have not sent one back
327329
// to the given peer in KEEPALIVE ms, we send an empty packet.
328-
if data_packet_received > aut_packet_sent
329-
&& now - aut_packet_sent >= KEEPALIVE_TIMEOUT
330-
&& mem::replace(&mut self.timers.want_keepalive, false)
330+
if self
331+
.timers
332+
.want_passive_keepalive_at
333+
.is_some_and(|keepalive_at| now >= keepalive_at)
331334
{
332335
tracing::debug!("KEEPALIVE(KEEPALIVE_TIMEOUT)");
333336
keepalive_required = true;
@@ -401,4 +404,9 @@ impl Tunn {
401404
None
402405
}
403406
}
407+
408+
#[cfg(test)]
409+
pub fn set_persistent_keepalive(&mut self, keepalive: u16) {
410+
self.timers.persistent_keepalive = keepalive as usize;
411+
}
404412
}

0 commit comments

Comments
 (0)