ts_dataplane/async_tokio.rs
1//! The packet processing dataplane, as a tokio task.
2
3use std::{collections::HashMap, convert::Infallible, ops::DerefMut, sync::atomic::AtomicU32};
4
5use tokio::sync::{Mutex, mpsc};
6use ts_packet::PacketMut;
7use ts_transport::{OverlayTransportId, PeerId, UnderlayTransportId};
8use ts_tunnel::NodeKeyPair;
9
10use crate::{EventResult, InboundResult, OutboundResult};
11
12/// Queue for packets leaving the data plane "up" into an overlay transport.
13pub type DataplaneToOverlay = mpsc::UnboundedSender<Vec<PacketMut>>;
14
15/// Queue for packets entering the data plane "down" from an overlay transport.
16pub type DataplaneFromOverlay = mpsc::UnboundedReceiver<Vec<PacketMut>>;
17
18/// Queue for packets leaving the data plane "down" into an underlay transport.
19pub type DataplaneToUnderlay = mpsc::UnboundedSender<(PeerId, Vec<PacketMut>)>;
20
21/// Queue for packets entering the data plane "up" from an underlay transport.
22pub type DataplaneFromUnderlay = mpsc::UnboundedReceiver<(PeerId, Vec<PacketMut>)>;
23
24/// A disco key a peer advertised over TSMP, paired with the WireGuard peer that sent it. See
25/// [`crate::InboundResult::learned_disco_keys`].
26pub type LearnedDiscoKey = (PeerId, ts_packet::tsmp::DiscoKeyAdvertisement);
27
28/// Sink the data plane writes TSMP-learned peer disco keys to, installed by the embedder with
29/// [`DataPlane::install_disco_key_sink`]. Go's `tstun.Wrapper` publishes the equivalent event on
30/// its event bus for `wgengine` to consume.
31pub type DataplaneToDiscoKeySink = mpsc::UnboundedSender<LearnedDiscoKey>;
32
33/// Read end of a [`DataplaneToDiscoKeySink`].
34pub type DiscoKeysFromDataplane = mpsc::UnboundedReceiver<LearnedDiscoKey>;
35
36// TODO: wire in overlay/underlay transport traits
37
38/// Transforms packets to make tailscale happen.
39pub struct DataPlane {
40 core_state: Mutex<CoreState>,
41 poll_state: Mutex<PollState>,
42
43 transports_changed: tokio::sync::Notify,
44
45 underlay_down: DataplaneToUnderlay,
46 overlay_up: DataplaneToOverlay,
47
48 next_underlay_transport: AtomicU32,
49 next_overlay_transport: AtomicU32,
50}
51
52struct CoreState {
53 /// The synchronous core of the data plane.
54 sync: crate::DataPlane,
55
56 /// Queues to write packets to overlay transports.
57 overlay_transports: HashMap<OverlayTransportId, DataplaneToOverlay>,
58 /// Queues to write packets to underlay transports.
59 underlay_transports: HashMap<UnderlayTransportId, DataplaneToUnderlay>,
60
61 /// Where TSMP-learned peer disco keys go, if anyone is listening. `None` (the default) simply
62 /// discards them: parsing still happens and the advertisement packets are still dropped, so an
63 /// embedder that does not wire a sink is not left delivering TSMP control messages to its
64 /// local stack.
65 disco_key_sink: Option<DataplaneToDiscoKeySink>,
66}
67
68/// State that must be held during async polling.
69struct PollState {
70 /// Queue for packets entering the data plane ("coming down") from overlay transports.
71 from_overlay: DataplaneFromOverlay,
72 /// Queue for packets entering the data plane ("coming up") from underlay transports.
73 from_underlay: DataplaneFromUnderlay,
74}
75
76impl DataPlane {
77 /// Create a new data plane for a wireguard node key.
78 ///
79 /// The caller must configure overlay/underlay output queues for the data plane to be useful,
80 /// otherwise all it can do is drop packets.
81 pub fn new(my_key: NodeKeyPair) -> Self {
82 let (overlay_up, overlay_down) = mpsc::unbounded_channel();
83 let (underlay_down, underlay_up) = mpsc::unbounded_channel();
84
85 let sync = crate::DataPlane::new(my_key);
86
87 Self {
88 underlay_down,
89 overlay_up,
90
91 next_overlay_transport: Default::default(),
92 next_underlay_transport: Default::default(),
93
94 transports_changed: tokio::sync::Notify::new(),
95
96 core_state: Mutex::new(CoreState {
97 sync,
98 overlay_transports: Default::default(),
99 underlay_transports: Default::default(),
100 disco_key_sink: None,
101 }),
102
103 poll_state: Mutex::new(PollState {
104 from_overlay: overlay_down,
105 from_underlay: underlay_up,
106 }),
107 }
108 }
109
110 /// Allocate a new underlay transport.
111 pub async fn new_underlay_transport(
112 &self,
113 ) -> (
114 UnderlayTransportId,
115 DataplaneFromUnderlay,
116 DataplaneToUnderlay,
117 ) {
118 let id = self
119 .next_underlay_transport
120 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
121 .into();
122
123 let (tx, rx) = mpsc::unbounded_channel();
124
125 {
126 let mut rest = self.core_state.lock().await;
127 rest.underlay_transports.insert(id, tx);
128 }
129
130 self.transports_changed.notify_waiters();
131
132 (id, rx, self.underlay_down.clone())
133 }
134
135 /// Allocate a new overlay transport.
136 pub async fn new_overlay_transport(
137 &self,
138 ) -> (OverlayTransportId, DataplaneToOverlay, DataplaneFromOverlay) {
139 let id = self
140 .next_overlay_transport
141 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
142 .into();
143
144 let (tx, rx) = mpsc::unbounded_channel();
145
146 {
147 let mut rest = self.core_state.lock().await;
148 rest.overlay_transports.insert(id, tx);
149 }
150
151 self.transports_changed.notify_waiters();
152
153 (id, self.overlay_up.clone(), rx)
154 }
155
156 /// Install (`Some`) or clear (`None`) the sink that receives peer disco keys learned from TSMP
157 /// disco-key advertisements (Go `packet.TSMPDiscoKeyAdvertisement`).
158 ///
159 /// Install this before [`run`](Self::run) starts: an advertisement arrives immediately after a
160 /// WireGuard session is established, so a sink installed later can miss the first one.
161 pub async fn install_disco_key_sink(&self, sink: Option<DataplaneToDiscoKeySink>) {
162 self.core_state.lock().await.disco_key_sink = sink;
163 }
164
165 /// Run the data plane forever, moving packets from the input queues to output queues.
166 pub async fn run(&self) -> Infallible {
167 loop {
168 self.step().await;
169 }
170 }
171
172 /// Run the data plane for a single step.
173 #[tracing::instrument(skip_all)]
174 pub async fn step(&self) {
175 enum SelectResult {
176 OverlayDown(Vec<PacketMut>),
177 UnderlayUp(PeerId, Vec<PacketMut>),
178 TransportsChanged,
179 Event,
180 }
181
182 // process in two phases:
183 //
184 // - SELECT: wait for underlying i/o or timer to make progress: don't lock the
185 // user-modifiable (core) state. self.transports_changed is used to break out of this
186 // state if the caller changes the underlying transports
187 // - UPDATE: lock the user-modifiable state and actually write out the packets produced
188 // in the SELECT phase (if any)
189 //
190 // designed this way to ensure that users can add and remove transports at any time without
191 // having to wait for the network or a timer to make progress (which may never happen)
192
193 let select_result = {
194 let next_event = {
195 let state = self.core_state.lock().await;
196 state.sync.next_event()
197 };
198
199 let mut poll_state = self.poll_state.lock().await;
200
201 let PollState {
202 from_overlay: overlay_down,
203 from_underlay: underlay_up,
204 ..
205 } = &mut *poll_state;
206
207 tokio::select! {
208 overlay_pkts = overlay_down.recv() => {
209 let overlay_pkts = overlay_pkts.unwrap();
210 tracing::trace!(n_overlay_pkts = overlay_pkts.len());
211
212 SelectResult::OverlayDown(overlay_pkts)
213 }
214
215 underlay_pkts = underlay_up.recv() => {
216 let (peer_id, underlay_pkts) = underlay_pkts.unwrap();
217 tracing::trace!(%peer_id, n_underlay_pkts = underlay_pkts.len());
218
219 SelectResult::UnderlayUp(peer_id, underlay_pkts)
220 }
221
222 _ = self.transports_changed.notified() => {
223 tracing::trace!("transports changed");
224
225 SelectResult::TransportsChanged
226 }
227
228 _ = sleep_until_event(next_event.map(Into::into)) => {
229 tracing::trace!("event");
230
231 SelectResult::Event
232 }
233 }
234 };
235
236 let mut core = self.core_state.lock().await;
237
238 let mut learned_disco_keys = Vec::new();
239
240 let (to_peers, to_local) = match select_result {
241 SelectResult::OverlayDown(overlay_down) => {
242 let OutboundResult { to_peers, loopback } =
243 core.sync.process_outbound(overlay_down);
244
245 (Some(to_peers), Some(loopback))
246 }
247 SelectResult::UnderlayUp(peer_id, underlay_up) => {
248 let InboundResult {
249 to_local,
250 to_peers,
251 learned_disco_keys: learned,
252 // Peers' TSMP rejected-connection messages are logged (at `debug!` — the
253 // record is peer-supplied and unmatched, so it must not be able to drive an
254 // operator's default-level log) and dropped by the filter step that parses
255 // them; nothing in this task has a flow table to match them against, so there
256 // is no second consumer here yet.
257 rejected_flows: _,
258 } = core.sync.process_inbound_from(Some(peer_id), underlay_up);
259
260 learned_disco_keys = learned;
261
262 (Some(to_peers), Some(to_local))
263 }
264 SelectResult::Event => {
265 let EventResult { to_peers } = core.sync.process_events();
266 (Some(to_peers), None)
267 }
268 SelectResult::TransportsChanged => (None, None),
269 };
270
271 if let Some(to_peers) = to_peers {
272 write_to_underlay(&core, to_peers).await;
273 }
274
275 if let Some(to_local) = to_local {
276 write_to_overlay(&core, to_local).await;
277 }
278
279 if !learned_disco_keys.is_empty()
280 && let Some(sink) = &core.disco_key_sink
281 {
282 for learned in learned_disco_keys {
283 // A closed sink means the consumer is gone (runtime shutting down); the key is
284 // simply not learned, which is the same position we were in before this existed.
285 if sink.send(learned).is_err() {
286 tracing::debug!("disco key sink closed; dropping TSMP disco-key advertisement");
287 break;
288 }
289 }
290 }
291 }
292
293 /// Get a mutable reference to the inner [`crate::DataPlane`].
294 ///
295 /// Primarily intended for mutating the routing tables.
296 ///
297 /// The returned value is a mutex guard, so limit how long it's held.
298 pub async fn inner(&self) -> impl DerefMut<Target = crate::DataPlane> {
299 let core = self.core_state.lock().await;
300 tokio::sync::MutexGuard::map(core, |x| &mut x.sync)
301 }
302}
303
304async fn write_to_overlay(slf: &CoreState, packets: HashMap<OverlayTransportId, Vec<PacketMut>>) {
305 for (id, packets) in packets {
306 if let Some(queue) = slf.overlay_transports.get(&id) {
307 tracing::trace!(overlay_id = ?id, n_packets = packets.len());
308 queue.send(packets).unwrap();
309 }
310 }
311}
312
313async fn write_to_underlay(
314 slf: &CoreState,
315 packets: impl IntoIterator<Item = ((UnderlayTransportId, PeerId), Vec<PacketMut>)>,
316) {
317 for ((tid, peer_id), packets) in packets {
318 tracing::trace!(underlay_id = ?tid, %peer_id, n_packets = packets.len());
319
320 if let Some(queue) = slf.underlay_transports.get(&tid) {
321 queue.send((peer_id, packets)).unwrap();
322 }
323 }
324}
325
326/// The longest the dataplane will sleep waiting for a timer when *no* event is scheduled, before
327/// re-checking the wireguard state machine.
328///
329/// The primary driver of timer progress is a real scheduled event: an endpoint with persistent
330/// keepalive enabled (the default) always reports a next-event deadline via
331/// [`crate::DataPlane::next_event`], so `step` wakes exactly on it and the keepalive / rekey / expiry
332/// timers fire on schedule even on an otherwise idle, fully-relayed tunnel. When such a deadline
333/// exists we sleep all the way to it (it is itself the coalesced *soonest* timer), so an idle tunnel
334/// with a keepalive due in ~25s sleeps ~25s and wakes *once* — not once per second.
335///
336/// This bound is purely a defensive safety net for the *no-event* case: it guarantees the dataplane
337/// can never block *forever* on I/O with nothing scheduled — the wedge where `next_event() == None`
338/// turned the sleep into `future::pending()`, so an idle session aged past expiry with nothing to
339/// refresh it. A spurious wakeup with no due event is harmless (the dispatch finds nothing and writes
340/// nothing); a few-second bound keeps that idle-wakeup overhead negligible (≈17k wakeups/day vs the
341/// old unconditional 1 Hz floor's ≈86k) while still bounding the wedge window.
342const MAX_IDLE_SLEEP: core::time::Duration = core::time::Duration::from_secs(5);
343
344/// Sleep until the next scheduled event deadline; if none is scheduled, sleep at most
345/// [`MAX_IDLE_SLEEP`] rather than blocking forever.
346///
347/// When `deadline` is `Some`, this wakes exactly on it (a finite instant, so it can never block
348/// forever and never wakes later than the deadline). When `deadline` is `None` (no event scheduled)
349/// it sleeps for [`MAX_IDLE_SLEEP`] so the dataplane periodically re-services the wireguard state
350/// machine even with zero traffic and zero scheduled events.
351async fn sleep_until_event(deadline: Option<tokio::time::Instant>) {
352 let until = next_wakeup(deadline, tokio::time::Instant::now(), MAX_IDLE_SLEEP);
353 tokio::time::sleep_until(until).await;
354}
355
356/// Compute the next wakeup instant.
357///
358/// - `Some(deadline)`: wake exactly on the next scheduled event. Real timers (persistent keepalive /
359/// rekey / expiry) are always reported as events, and `next_event` already returns the *soonest*
360/// one, so honoring it directly means an idle tunnel with a keepalive due in 25s sleeps ~25s and
361/// wakes *once*. We deliberately do **not** clamp the deadline down to an idle floor — that would
362/// wake ~25× more often for no benefit, since nothing is due before the deadline. The deadline is
363/// itself a finite instant, so this can never block forever, and we never wake *later* than it.
364/// - `None` (nothing scheduled): collapse to the bounded floor `now + max_idle_sleep` so the result
365/// is *always* a finite instant — the dataplane can never sleep forever even with no events.
366///
367/// Pure so the cadence (and the "never block forever" guarantee) is unit-testable without a runtime.
368fn next_wakeup<I: core::ops::Add<core::time::Duration, Output = I> + Copy>(
369 deadline: Option<I>,
370 now: I,
371 max_idle_sleep: core::time::Duration,
372) -> I {
373 match deadline {
374 Some(deadline) => deadline,
375 None => now + max_idle_sleep,
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 /// The wedge fix, distilled: with no event scheduled, the dataplane must still wake within the
384 /// bounded floor instead of `future::pending()` (block forever). This is what guarantees an idle
385 /// endpoint's timers (persistent keepalive / rekey / expiry) keep getting serviced.
386 #[test]
387 fn no_scheduled_event_still_wakes_within_floor() {
388 let now = std::time::Instant::now();
389 let woke = next_wakeup(None, now, MAX_IDLE_SLEEP);
390 assert_eq!(
391 woke,
392 now + MAX_IDLE_SLEEP,
393 "a None deadline must collapse to the bounded floor, never block forever"
394 );
395 }
396
397 /// A soon scheduled event is honored exactly: the idle floor only applies when *no* event is
398 /// scheduled, it never delays (or hurries) a due event.
399 #[test]
400 fn near_event_is_honored_exactly() {
401 let now = std::time::Instant::now();
402 let soon = now + core::time::Duration::from_millis(50);
403 assert_eq!(
404 next_wakeup(Some(soon), now, MAX_IDLE_SLEEP),
405 soon,
406 "an event sooner than the floor must wake exactly on its deadline"
407 );
408 }
409
410 /// A far-future scheduled event is honored exactly, *not* clamped down to the idle floor: the
411 /// floor exists only to bound the no-event wedge. `next_event` already reports the soonest timer,
412 /// so nothing is due before the deadline — clamping it would just burn ~floor-cadence wakeups on
413 /// an idle tunnel (the battery regression this fix removes). Sleeping to a far real deadline is
414 /// safe precisely because it is finite (never `pending()`).
415 #[test]
416 fn far_event_is_honored_not_clamped() {
417 let now = std::time::Instant::now();
418 let far = now + core::time::Duration::from_secs(3600);
419 assert_eq!(
420 next_wakeup(Some(far), now, MAX_IDLE_SLEEP),
421 far,
422 "a far-off scheduled event must be honored exactly, not clamped to the idle floor"
423 );
424 }
425
426 /// An idle tunnel with a persistent keepalive due in ~25s must sleep ~25s and wake *once*, not
427 /// once per [`MAX_IDLE_SLEEP`] — this is the battery/wakeup regression the fix targets.
428 #[test]
429 fn keepalive_in_25s_sleeps_to_the_deadline_not_the_floor() {
430 let now = std::time::Instant::now();
431 let keepalive_due = now + core::time::Duration::from_secs(25);
432 let woke = next_wakeup(Some(keepalive_due), now, MAX_IDLE_SLEEP);
433 assert_eq!(
434 woke, keepalive_due,
435 "a 25s keepalive deadline must be slept to directly (one wakeup), not capped at the idle floor"
436 );
437 assert!(
438 woke > now + MAX_IDLE_SLEEP,
439 "the wakeup must be well past the idle floor: the floor must not shorten a real deadline"
440 );
441 }
442
443 /// The anti-busy-spin invariant of the wedge fix, stated as a bound: a fully-idle dataplane (no
444 /// scheduled event) must always wake **strictly in the future**, on the coarse floor — never at
445 /// `now` or earlier (which would make `sleep_until` return instantly and turn `step()` into a
446 /// tight, CPU-burning sub-millisecond loop) and never `future::pending()` (the original
447 /// block-forever wedge). Swept over several base instants against the *real* `MAX_IDLE_SLEEP` so
448 /// the production idle cadence itself is what's pinned, not a toy value.
449 ///
450 /// Scope note: this is the deepest layer testable without a runtime. A full `#[tokio::test]`
451 /// driving [`DataPlane::step`] under `tokio::time::pause()` / `advance()` would require tokio's
452 /// `test-util` feature, which `ts_dataplane` does not enable (turning it on is a non-test
453 /// dependency change, out of scope here). [`sleep_until_event`] is a thin wrapper that feeds this
454 /// exact instant straight to `tokio::time::sleep_until`, so the integration-level idle cadence —
455 /// wake every `MAX_IDLE_SLEEP`, never sooner, never never — is fully determined by (and thus
456 /// covered through) this helper's boundedness.
457 #[test]
458 fn idle_wakeup_is_coarse_and_never_busy_spins() {
459 // A zero floor would let an idle step() spin; the production cadence must be positive.
460 assert!(
461 MAX_IDLE_SLEEP > core::time::Duration::ZERO,
462 "the idle floor must be a positive cadence, else step() would busy-spin"
463 );
464
465 let base = std::time::Instant::now();
466 for offset_ms in [0u64, 1, 250, 5_000, 60_000] {
467 let now = base + core::time::Duration::from_millis(offset_ms);
468 let woke = next_wakeup(None, now, MAX_IDLE_SLEEP);
469
470 // Strictly after `now`: an idle wakeup at or before `now` would busy-spin step().
471 assert!(
472 woke > now,
473 "idle wakeup must be strictly after now (no busy-spin); got {woke:?} <= {now:?}"
474 );
475 // Bounded to exactly the coarse floor: never sooner (tight loop), always finite
476 // (never the old `future::pending()` block-forever).
477 assert_eq!(
478 woke,
479 now + MAX_IDLE_SLEEP,
480 "idle wakeup must land on the bounded coarse floor, never sooner and never never"
481 );
482 }
483 }
484}