From e25c230427dba02d662c0e3a33b98abc97e44884 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 9 Apr 2026 16:40:25 +0200 Subject: [PATCH] rework Results.tex to include gVisor findings --- Chapters/Results.tex | 1747 +++++++++++++++------- Figures/baseline/tcp/TCP Throughput.png | Bin 49912 -> 74837 bytes Listings/hyprspace_dispatch.go | 26 + Listings/hyprspace_netstack.go | 21 + Listings/hyprspace_sendpacket.go | 31 + Listings/hyprspace_tun_linux.go | 15 + Listings/magicsock_buffer.go | 9 + Listings/nixos_tailscale.nix | 1 + Listings/rig_interface_name.nix | 10 + Listings/tailscale_netstack_overrides.go | 28 + Listings/tstun_gro.go | 22 + _typos.toml | 2 +- main.tex | 49 + treefmt.nix | 1 + 14 files changed, 1387 insertions(+), 575 deletions(-) create mode 100644 Listings/hyprspace_dispatch.go create mode 100644 Listings/hyprspace_netstack.go create mode 100644 Listings/hyprspace_sendpacket.go create mode 100644 Listings/hyprspace_tun_linux.go create mode 100644 Listings/magicsock_buffer.go create mode 100644 Listings/nixos_tailscale.nix create mode 100644 Listings/rig_interface_name.nix create mode 100644 Listings/tailscale_netstack_overrides.go create mode 100644 Listings/tstun_gro.go diff --git a/Chapters/Results.tex b/Chapters/Results.tex index 981711c..bfa248e 100644 --- a/Chapters/Results.tex +++ b/Chapters/Results.tex @@ -31,6 +31,24 @@ latency of just an entire 30-second test. Mycelium sits at the other extreme, adding 34.9\,ms of latency, roughly 58$\times$ the bare-metal figure. +A note on naming: ``Headscale'' in every table and figure of this +chapter labels the test scenario in which the Tailscale client +(\texttt{tailscaled}) connects to a self-hosted Headscale control +server. The data plane is therefore the Tailscale client built on +\texttt{wireguard-go}, not the Headscale binary itself, which is +only a control-plane server. The test rig launches +\texttt{tailscaled} via the NixOS \texttt{services.tailscale} +module with \texttt{interfaceName = "ts-headscale"}, which +translates to \texttt{--tun ts-headscale}; this means the Tailscale +client uses a real kernel TUN device and the host kernel's TCP/IP +stack handles every tunneled packet. The alternate +\texttt{--tun=userspace-networking} mode, in which gVisor netstack +terminates tunneled TCP inside the \texttt{tailscaled} process, is +\emph{not} engaged in any of the benchmarks reported here. +Statements below about ``Headscale'' running \texttt{wireguard-go} +should be read as statements about the Tailscale client in this +scenario. + \subsection{Test Execution Overview} Running the full baseline suite across all ten VPNs and the internal @@ -127,16 +145,15 @@ ZeroTier, for instance, reaches 814\,Mbps but accumulates 1\,163~retransmits per test, over 1\,000$\times$ what WireGuard needs. ZeroTier compensates for tunnel-internal packet loss by repeatedly triggering TCP congestion-control recovery, whereas -WireGuard delivers data with negligible in-tunnel loss. Across all VPNs, -retransmit behaviour falls into three groups: \emph{clean} ($<$110: -WireGuard, Internal, Yggdrasil, Headscale), \emph{stressed} -(200--900: Tinc, EasyTier, Mycelium, VpnCloud), and -\emph{pathological} ($>$950: Nebula, ZeroTier, Hyprspace). +WireGuard delivers data with negligible in-tunnel loss. The +bare-metal Internal reference sits at 1.7~retransmits per test — +essentially noise — and the VPNs split into three groups around +it: \emph{clean} ($<$110: WireGuard, Yggdrasil, Headscale), +\emph{stressed} (200--900: Tinc, EasyTier, Mycelium, VpnCloud), +and \emph{pathological} ($>$950: Nebula, ZeroTier, Hyprspace). % TODO: Is this naming scheme any good? -% TODO: Fix TCP Throughput plot - \begin{figure}[H] \centering \begin{subfigure}[t]{\textwidth} @@ -171,10 +188,7 @@ WireGuard, Internal, Yggdrasil, Headscale), \emph{stressed} Retransmits have a direct mechanical relationship with TCP congestion control. Each retransmit triggers a reduction in the congestion window -(\texttt{cwnd}), throttling the sender. % TODO: The text says "average congestion window" but -% Figure~\ref{fig:retransmit_cwnd} plots "Max Congestion Window." -% Use consistent terminology --- either change the text to "max" or -% change the figure axis label. +(\texttt{cwnd}), throttling the sender. This relationship is visible in Figure~\ref{fig:retransmit_correlations}: Hyprspace, with 4965 retransmits, maintains the smallest max congestion window in the @@ -200,17 +214,17 @@ in the dataset). This suggests significant in-tunnel packet loss or buffering at the VpnCloud layer that the retransmit count (857) alone does not fully explain. -% TODO: Mycelium's 122--379 Mbps range is per-link asymmetry (different -% overlay routing paths), not stochastic run-to-run variability. -% Section~\ref{sec:mycelium_routing} confirms the same numbers as -% per-link throughput. Conflating link asymmetry with run-to-run -% variance is misleading --- either separate the two or clarify that -% Mycelium's spread comes from path selection, not randomness. -Run-to-run variability also differs substantially. WireGuard ranges -from 824 to 884\,Mbps (a 60\,Mbps window), while Mycelium ranges -from 122 to 379\,Mbps, a 3:1 ratio between worst and best runs. A -VPN with wide variance is harder to capacity-plan around than one -with consistent performance, even if the average is lower. +Variability — whether stochastic across runs or systematic across +links — also differs substantially. WireGuard's three link +directions cluster tightly (824 to 884\,Mbps, a 60\,Mbps window), +behaving almost identically. Mycelium's three directions span +122 to 379\,Mbps, a 3:1 ratio, but this is not run-to-run noise: +Section~\ref{sec:mycelium_routing} shows the spread is per-link +path-selection asymmetry, with one link finding a direct route and +the other two routing through the global overlay. Either way, a +VPN whose throughput varies that widely across links is harder to +capacity-plan around than one that delivers a consistent figure +on every direction. \begin{figure}[H] \centering @@ -306,22 +320,27 @@ keep up with the link speed. The qperf benchmark backs this up: Tinc maxes out at 14.9\,\% total system CPU while delivering just 336\,Mbps. % TODO: 14.9\% total CPU does not obviously indicate a bottleneck. -% Clarify that this is whole-system utilization on a multi-core -% machine, and that Tinc's single-threaded design means one core is -% saturated while the rest are idle. Also note that VpnCloud reports -% the same 14.9\% yet achieves 539 Mbps --- explain why the same CPU -% utilization yields different throughput (e.g., different per-packet -% processing cost). -On a multi-core system, the low percentage reflects a single -saturated core, a clear sign that the CPU, not the network, is the -bottleneck. +% This is whole-system utilization on a multi-core machine, and a +% single saturated core fits the budget — but VpnCloud reports the +% same 14.9\% \emph{and} reaches 539\,Mbps, much more than Tinc. +% The single-saturated-core story alone therefore cannot explain +% the throughput gap; per-packet processing cost must differ +% materially between the two. Verify with per-thread CPU sampling +% or eBPF profiling. +On a multi-core system, this low percentage is consistent with a +single saturated core (and Tinc is single-threaded), which would +explain why the CPU rather than the network is the bottleneck. +The story is incomplete, however: VpnCloud shows the same 14.9\,\% +total system CPU yet delivers 539\,Mbps — 60\,\% more than Tinc — +so a difference in per-packet processing cost between the two +implementations must also be in play. Figure~\ref{fig:latency_throughput} makes this disconnect easy to spot. % TODO: These CPU numbers are stated inline but never shown in a plot % or table. Add a CPU utilization figure or table so readers can % verify. Also, the claim that WireGuard's CPU usage "goes to -% cryptographic processing" is unsubstantiated --- no profiling data +% cryptographic processing" is unsubstantiated: no profiling data % is presented. Either add profiling evidence or soften to % "likely" / "presumably." The qperf measurements also reveal a wide spread in CPU usage. @@ -329,12 +348,7 @@ Hyprspace (55.1\,\%) and Yggdrasil (52.8\,\%) consume 5--6$\times$ as much CPU as Internal's 9.7\,\%. WireGuard sits at 30.8\,\%, surprisingly high for a kernel-level implementation, presumably due to in-kernel -cryptographic processing. % TODO: "do the most with the least CPU time" is misleading --- -% Tinc gets only 336 Mbps at 14.9% CPU (22.6 Mbps/%), while -% WireGuard gets 864 Mbps at 30.8% (28 Mbps/%). These three use -% the least CPU but don't necessarily achieve the best throughput/CPU -% ratio. Rephrase to "use the least CPU" or calculate actual -% efficiency ratios. +cryptographic processing. On the efficient end, VpnCloud (14.9\,\%), Tinc (14.9\,\%), and EasyTier (15.4\,\%) use the least CPU time. Nebula and Headscale are missing from @@ -355,7 +369,8 @@ this comparison because qperf failed for both. \subsection{Parallel TCP Scaling} -The single-stream benchmark tests one link direction at a time. % TODO: The plot labels this benchmark "10-stream parallel" but this +The single-stream benchmark tests one link direction at a time. % +% TODO: The plot labels this benchmark "10-stream parallel" but this % description says "six unidirectional flows." Verify the actual test % configuration and reconcile the two. The @@ -404,18 +419,29 @@ single-stream mode. Mycelium's 34.9\,ms RTT means a lone TCP stream can never fill the pipe: the bandwidth-delay product demands a window larger than any single flow maintains, so multiple concurrent flows compensate for that constraint and push throughput to 2.20$\times$ -the single-stream figure. % TODO: The buffer-bloat workaround explanation for Hyprspace's -% parallel scaling is a hypothesis. No direct evidence is shown -% that multiple streams specifically alleviate buffer bloat. -% Consider adding bufferbloat measurements or softening the claim. -% TODO: DOWNSTREAM DEPENDENCY — This claim depends on the buffer bloat -% diagnosis in Section hyprspace_bloat, which itself rests on the unverified -% 2,800 ms under-load latency (see TODO there). If that latency figure -% is not confirmed, this parallel-scaling explanation collapses. -Hyprspace scales almost as well -(2.18$\times$), possibly because multiple streams collectively work -around the buffer bloat that cripples any individual flow -(Section~\ref{sec:hyprspace_bloat}). Tinc picks up a +the single-stream figure. Hyprspace scales almost as well +(2.18$\times$) for the same reason but with a different +bottleneck. Its libp2p send pipeline accumulates roughly +2\,800\,ms of under-load latency +(Section~\ref{sec:hyprspace_bloat}), which gives any single TCP +flow a bandwidth-delay product on the order of hundreds of +megabytes to fill — far beyond any single kernel cwnd. And +because Hyprspace keys \texttt{activeStreams} by destination +\texttt{peer.ID} (Listing~\ref{lst:hyprspace_sendpacket}), the +three concurrent peer pairs in the parallel benchmark each get +their own libp2p stream, their own mutex, and their own yamux +flow-control window. The three TCP senders therefore maintain +three independent windows in flight, and three windows fill +more of the bloated pipeline than one can. +% TODO: This is still a hypothesis: it generalises the same +% bandwidth-delay-product argument used for Mycelium directly +% above, and is now grounded in the per-peer +% \texttt{SharedStream} structure verified in +% Listing~\ref{lst:hyprspace_sendpacket}, but neither the +% per-flow window evolution nor the actual under-load latency +% has been measured directly. A tcpdump of one Hyprspace +% iPerf3 run with inter-arrival timing analysis would settle +% it. Tinc picks up a 1.68$\times$ boost because several streams can collectively keep its single-threaded CPU busy during what would otherwise be idle gaps in a single flow. @@ -430,8 +456,6 @@ under multiplexing. Nebula is the only VPN that actually gets \emph{slower} with more streams: throughput drops from 706\,Mbps to 648\,Mbps (0.92$\times$) while retransmits jump from 955 to 2\,462. The -% TODO: "ten streams" vs "six unidirectional flows" --- reconcile -% with the test description above. streams are clearly fighting each other for resources inside the tunnel. @@ -510,12 +534,14 @@ Only Internal and WireGuard achieve 0\,\% packet loss. Both operate at the kernel level with proper backpressure that matches sender to receiver rate. Every other VPN shows massive loss (69--99\%) because the sender overwhelms the tunnel's userspace processing capacity. -% TODO: Headscale also uses WireGuard's kernel module but still shows -% 69.8\% loss. Explain that Headscale's userspace netstack sits -% between the application and the WireGuard kernel module, so UDP -% traffic must pass through userspace before reaching the kernel -% tunnel --- this is why it behaves like a userspace VPN here despite -% using WireGuard underneath. +Headscale shares WireGuard's cryptographic protocol but, contrary to +intuition, does not share its kernel datapath: Tailscale's +\texttt{magicsock} layer intercepts every packet to handle endpoint +selection and DERP relay, which is incompatible with the in-kernel +WireGuard module. Headscale therefore runs \texttt{wireguard-go} +entirely in userspace, and the unbounded \texttt{-b~0} flood overruns +that userspace pipeline just as it overruns every other userspace +implementation, producing 69.8\,\% loss despite the WireGuard branding. Yggdrasil's 98.7\% loss is the most extreme: it sends the most data (due to its large block size) but loses almost all of it. These loss rates do not reflect real-world UDP behavior but reveal which VPNs @@ -653,61 +679,54 @@ between raw throughput and real-world download speed. \label{fig:nix_download} \end{figure} -\paragraph{Video Streaming (RIST).} +\paragraph{Video streaming (RIST).} -At just 3.3\,Mbps, the RIST video stream sits comfortably within -every VPN's throughput budget. This test therefore measures -something different: how well the VPN handles real-time UDP packet -delivery under steady load. % TODO: The RIST plot shows Nebula at 99.8\%, not 100\%. "Nine of -% eleven deliver 100\%" is inaccurate --- eight deliver 100\%, Nebula -% delivers 99.8\%. Also, the claim that 14--16 dropped frames trace -% to encoder warm-up is stated without evidence. How was this -% determined? Add a reference or explain the methodology. -Nine of the eleven VPNs pass without -incident, delivering near-perfect video quality. The 14--16 dropped -frames that appear uniformly across all VPNs, including Internal, -likely trace back to encoder warm-up rather than tunnel overhead. +At 3.3\,Mbps, the RIST video stream sits well within every VPN's +throughput budget. The test therefore measures something else: how +well each VPN handles real-time UDP delivery under steady load. + +Most VPNs pass without incident. Eight deliver 100\% quality, +Nebula sits just below at 99.8\%, and Hyprspace's headline figure +of 100\% conceals a separate failure mode discussed below. The +14--16 dropped frames that appear uniformly across every run, including +Internal, are most likely encoder warm-up artefacts rather than +tunnel overhead, though we have not verified this directly. % TODO: The packet-drop distribution statistics (288 mean, % 10\% median, IQR 255--330) are not shown in any figure. % Add a box plot or distribution figure for Headscale's RIST drops. -Headscale is the exception. It averages just 13.1\,\% quality, -dropping 288~packets per test interval. The degradation is not -bursty but sustained: median quality sits at 10\,\%, and the -interquartile range of dropped packets spans a narrow 255--330 band. -The qperf benchmark independently corroborates this, having failed -outright for Headscale, confirming that something beyond bulk TCP is -broken. +Headscale is the clear failure. Its mean quality is 13.1\%, and +each test interval drops 288 packets. The degradation is sustained +rather than bursty: median quality is 10\%, and the interquartile +range of dropped packets is a narrow 255--330. The qperf benchmark +also fails outright for Headscale at baseline, which rules out a +bulk-TCP explanation. Something in the real-time path is broken. -What makes this failure unexpected is that Headscale builds on -WireGuard, which handles video flawlessly. TCP throughput places -Headscale squarely in Tier~1. Yet the RIST test runs over UDP, and +The failure is unexpected because Headscale builds on WireGuard, +which handles video without trouble, and Headscale's own TCP +throughput puts it in Tier~1. RIST runs over UDP, however, and qperf probes latency-sensitive paths using both TCP and UDP. The -% TODO: The DERP relay / MTU fragmentation hypothesis is plausible -% but unverified. No packet capture or fragmentation analysis is -% presented. Either add tcpdump / packet-level evidence or mark -% this more clearly as a hypothesis. -pattern points toward Headscale's DERP relay or NAT traversal layer -as the source. Its effective UDP payload size of 1\,208~bytes, the smallest -of any VPN, may compound the issue: RIST packets that exceed -this limit would be fragmented, and reassembling fragments under -sustained load could produce exactly the kind of steady, uniform packet -drops the data shows. For video conferencing, VoIP, or any -real-time media workload, this is a disqualifying result regardless -of TCP throughput. +most plausible source is Headscale's DERP relay or NAT traversal +layer. Headscale's effective UDP payload size is 1\,208~bytes, the +smallest in the dataset. RIST packets larger than this would be +fragmented, and fragment reassembly under sustained load could +produce exactly the steady, uniform drop pattern the data shows. +This is a hypothesis, not a confirmed cause: it would need a +packet capture to verify. Either way, the result disqualifies +Headscale from video conferencing, VoIP, or any other real-time +media workload, regardless of TCP throughput. % TODO: Hyprspace's packet-drop statistics (mean 1,194, max 55,500, % percentiles all zero) are not visible in the RIST Quality bar chart. % Add a distribution plot or note in the caption that the bar % chart hides this variance. -Hyprspace reveals a different failure mode. Its average quality -reads 100\,\%, but the raw numbers underneath are far from stable: -mean packet drops of 1\,194 and a maximum spike of 55\,500, with -the 25th, 50th, and 75th percentiles all at zero. Hyprspace -alternates between perfect delivery and catastrophic bursts. -RIST's forward error correction compensates for most of these -events, but the worst spikes are severe enough to overwhelm FEC -entirely. +Hyprspace fails differently. Its average quality reads 100\%, but +the raw drop counts underneath are unstable: mean packet drops of +1\,194 and a maximum spike of 55\,500. The 25th, 50th, and 75th +percentiles are all zero, so most runs deliver perfectly while a +small number suffer catastrophic bursts. RIST's forward error +correction recovers from most of these events, but the worst spikes +overwhelm FEC entirely. \begin{figure}[H] \centering @@ -742,14 +761,17 @@ Reboot reconnection rearranges the rankings. Hyprspace, the worst performer under sustained TCP load, recovers in just 8.7~seconds on average, faster than any other VPN. WireGuard and Nebula follow at 10.1\,s each. Nebula's consistency is striking: 10.06, 10.06, -10.07\,s across its three nodes, pointing to a hard-coded timer -rather than topology-dependent convergence. +10.07\,s across its three nodes, an exact match for Nebula's +\texttt{HostUpdateNotification} interval, whose default is +10~seconds in the lighthouse protocol (configurable, but the +benchmarks use the default). After a reboot, a node must +wait until the next periodic update before its lighthouses learn +its new endpoint, so the reconnection time tracks the timer rather +than any topology-dependent convergence. Mycelium sits at the opposite end, needing 76.6~seconds and showing the same suspiciously uniform pattern (75.7, 75.7, 78.3\,s), suggesting a fixed protocol-level wait built into the overlay. -%TODO: Hard coded timer needs to be verified - Yggdrasil produces the most lopsided result in the dataset: its yuki node is back in 7.1~seconds while lom and luna take 94.8 and 97.3~seconds respectively. The gap likely reflects the overlay's @@ -810,7 +832,8 @@ retransmits per 30-second test (one in every 200~segments), TCP spends most of its time in congestion recovery rather than steady-state transfer, shrinking the max congestion window to 205\,KB, the smallest in the dataset. Under parallel load the -situation worsens: retransmits climb to 17\,426. % TODO: The explanation for the sender/receiver inversion (ACK delays +situation worsens: retransmits climb to 17\,426. % TODO: The +% explanation for the sender/receiver inversion (ACK delays % causing sender-side timer undercounting) is a hypothesis. Normally % sender >= receiver. Consider verifying with packet captures or % note this as a likely but unconfirmed explanation. @@ -829,43 +852,145 @@ quality outside of its burst events. The pathology is narrow but severe: any continuous data stream saturates the tunnel's internal buffers. +Hyprspace does import gVisor netstack, but reading the source +confirms that the gVisor TCP stack sits exclusively behind the +in-VPN ``service network'' feature. Regular tunnel traffic uses +an ordinary kernel TUN device created through the +\texttt{songgao/water} library, and the forwarding loop in +\texttt{node/node.go} only diverts a packet into the gVisor +stack when its destination falls inside the +\texttt{fd00:hyprspsv::/80} service prefix \emph{and} the L4 +protocol is TCP; everything else is shipped verbatim over a +libp2p stream and written back into the receiving peer's kernel +TUN. Listings~\ref{lst:hyprspace_kernel_tun}, +\ref{lst:hyprspace_dispatch}, and \ref{lst:hyprspace_netstack} +show the relevant code in the upstream Hyprspace tree. + +\lstinputlisting[language=Go,caption={Hyprspace creates a real + kernel TUN via \texttt{songgao/water}; this is the device every + peer-to-peer packet traverses. +\textit{hyprspace/tun/tun\_linux.go:14--36}},label={lst:hyprspace_kernel_tun}]{Listings/hyprspace_tun_linux.go} + +\lstinputlisting[language=Go,caption={The IPv6 dispatch in the + Hyprspace forwarding loop only diverts to the gVisor service-network + TUN when the destination matches the + \texttt{fd00:hyprspsv::/80} service prefix \emph{and} the L4 + protocol byte is \texttt{0x06} (TCP); every other packet is left + on the kernel TUN path and forwarded over libp2p. +\textit{hyprspace/node/node.go:255--283}},label={lst:hyprspace_dispatch}]{Listings/hyprspace_dispatch.go} + +\lstinputlisting[language=Go,caption={Hyprspace's gVisor netstack + initialiser only enables TCP SACK; there is no \texttt{TCPRecovery} + override (RACK stays at gVisor's default), no congestion-control + override, and no buffer-size override. The text in + \texttt{tun.go} also notes the file is taken verbatim from + wireguard-go. +\textit{hyprspace/netstack/tun.go:6--80}},label={lst:hyprspace_netstack}]{Listings/hyprspace_netstack.go} + +Since the benchmark targets the regular Hyprspace IPv4/IPv6 +addresses rather than service-network proxies, both endpoints +rely on their host kernel's TCP stack for the entire transfer. +Whatever options Hyprspace's gVisor instance might set +internally — congestion control, loss recovery, buffer sizes — +are therefore irrelevant to these measurements; the inner TCP +state machine the kernel runs is the only one in the path. +The same caveat applies more sharply to Tailscale, where the +upstream documentation talks about an in-process gVisor TCP +stack but the benchmark traffic never reaches it; that case is +the subject of Section~\ref{sec:tailscale_degraded}. + +If gVisor is out of scope, the buffer bloat must originate +further up the Hyprspace stack instead. The most plausible +source is the libp2p / yamux stream layer through which raw IP +packets are funnelled. Hyprspace's TUN-read loop dispatches +each outbound packet on its own goroutine, and every such +goroutine ends up in \texttt{node/node.go}'s +\texttt{sendPacket}, which keeps exactly one libp2p stream per +destination peer in \texttt{activeStreams} and guards it with a +single per-peer \texttt{sync.Mutex} +(Listing~\ref{lst:hyprspace_sendpacket}). Concurrent +application TCP flows to the same Hyprspace neighbour therefore +serialise behind that one lock: the parallel iPerf3 test, which +opens multiple TCP connections to the same peer at once, +collapses to a single send pipeline at this layer. Each +goroutine waiting for the lock pins its own 1420-byte packet +buffer, and the underlying yamux session adds a per-stream +flow-control window on top. None of this is visible to the +kernel TCP sender that produced the inner segments — the kernel +sees only that the TUN write returned — so it keeps growing +its congestion window while the libp2p layer falls further +behind. The geometry is the textbook one for buffer bloat: a +fast producer (kernel TCP) sitting upstream of a slow, +serialised consumer (the single yamux stream per peer) with +no flow-control signal coupling the two. + +\lstinputlisting[language=Go,caption={Hyprspace's outbound + fast path keeps exactly one libp2p stream per destination peer + in \texttt{activeStreams} and guards it with a per-peer + \texttt{sync.Mutex} held inside the \texttt{SharedStream} + record. The TUN-read loop spawns a fresh goroutine per packet + (\texttt{node.go:282}); each one calls \texttt{sendPacket} and + takes \texttt{ms.Lock} for the duration of the libp2p stream + write, so concurrent application TCP flows to the same + Hyprspace neighbour are serialised behind a single mutex. + \textit{hyprspace/node/node.go:36--39, 282, +328--348}},label={lst:hyprspace_sendpacket}]{Listings/hyprspace_sendpacket.go} + \paragraph{Mycelium: Routing Anomaly.} \label{sec:mycelium_routing} Mycelium's 34.9\,ms average latency appears to be the cost of -routing through a global overlay. The per-path numbers, however, +routing through a global overlay. The per-path +numbers, however, reveal a bimodal distribution: \begin{itemize} - \bitem{luna$\rightarrow$lom:} 1.63\,ms (direct path, comparable + \bitem{luna$\rightarrow$lom:} 1.63\,ms (direct + path, comparable to Headscale at 1.64\,ms) \bitem{lom$\rightarrow$yuki:} 51.47\,ms (overlay-routed) \bitem{yuki$\rightarrow$luna:} 51.60\,ms (overlay-routed) \end{itemize} -One of the three links has found a direct route; the other two still +One of the three links has found a direct route; the +other two still bounce through the overlay. All three machines sit on the same -% TODO: Characterising path discovery as "failing intermittently" assumes -% direct routing is the expected outcome on a LAN. Mycelium is designed -% as a global overlay and may intentionally route through supernodes. -% If this is by-design behaviour, rephrase to avoid implying a bug. -% This characterisation also propagates to the impairment ping analysis -% (around line 966) which says impairment "pushes path discovery toward -% shorter routes." -% TODO: The throughput data INVERTS the latency split rather than -% "mirroring" it. The direct path (luna→lom, 1.63 ms RTT) achieves -% only 122 Mbps, while the overlay-routed path (yuki→luna, 51.60 ms -% RTT) reaches 379 Mbps --- the opposite of what TCP theory predicts. -% The plot also shows luna→lom receiver throughput at only 57.2 Mbps -% (a 53% sender/receiver gap on that link). Explain why the direct +% TODO: Characterising path discovery as "failing +% intermittently" assumes +% direct routing is the expected outcome on a LAN. +% Mycelium is designed +% as a global overlay and may intentionally route +% through supernodes. +% If this is by-design behaviour, rephrase to avoid +% implying a bug. +% This characterisation also propagates to the +% impairment ping analysis +% in Section sec:impairment, which says impairment "pushes path +% discovery toward shorter routes." +% TODO: The throughput data INVERTS the latency split +% rather than +% "mirroring" it. The direct path (luna→lom, 1.63 ms +% RTT) achieves +% only 122 Mbps, while the overlay-routed path +% (yuki→luna, 51.60 ms +% RTT) reaches 379 Mbps: the opposite of what TCP +% theory predicts. +% The plot also shows luna→lom receiver throughput at +% only 57.2 Mbps +% (a 53% sender/receiver gap on that link). Explain +% why the direct % path is 3× slower than the overlay path, or acknowledge the -% contradiction. The current wording "mirrors the split" is incorrect. -physical network, so Mycelium's path discovery is not consistently -selecting the direct route, a more specific problem than blanket overlay +% contradiction. The current wording "mirrors the +% split" is incorrect. +physical network, so Mycelium's path discovery is not +consistently +selecting the direct route, a more specific problem +than blanket overlay overhead. Throughput shows a similarly lopsided split: yuki$\rightarrow$luna reaches 379\,Mbps while luna$\rightarrow$lom manages only 122\,Mbps, a 3:1 gap. In -bidirectional mode, the reverse direction on that worst link drops +bidirectional mode, the reverse direction on that +worst link drops to 58.4\,Mbps, the lowest single-direction figure in the entire dataset. @@ -873,29 +998,37 @@ dataset. \centering \includegraphics[width=\textwidth]{{Figures/baseline/tcp/Mycelium/Average Throughput}.png} - % TODO: The caption attributes the asymmetry to "inconsistent direct - % route discovery" but the direct-route link (luna→lom, 1.63 ms RTT) - % is actually the SLOWEST (122 Mbps). The caption should address + % TODO: The caption attributes the asymmetry to + % "inconsistent direct + % route discovery" but the direct-route link + % (luna→lom, 1.63 ms RTT) + % is actually the SLOWEST (122 Mbps). The caption + % should address % why the direct path underperforms the overlay paths. \caption{Per-link TCP throughput for Mycelium, showing extreme path asymmetry. The 3:1 ratio between best (yuki$\rightarrow$luna, 379\,Mbps) and worst - (luna$\rightarrow$lom, 122\,Mbps) links does not correlate with + (luna$\rightarrow$lom, 122\,Mbps) links does not + correlate with the latency split (Section~\ref{sec:mycelium_routing}).} \label{fig:mycelium_paths} \end{figure} % TODO: TTFB (93.7 ms vs.\ 16.8 ms) and connection establishment % (47.3 ms) numbers are from qperf but not shown in any figure. -% Add a connection-setup latency table or plot. Also clarify what -% Internal's connection establishment time is (47.3 / 3 = 15.8 ms?) +% Add a connection-setup latency table or plot. Also +% clarify what +% Internal's connection establishment time is (47.3 / +% 3 = 15.8 ms?) % so the "3× overhead" can be verified. The overlay penalty shows up most clearly at connection setup. -Mycelium's average time-to-first-byte is 93.7\,ms (vs.\ Internal's +Mycelium's average time-to-first-byte is 93.7\,ms +(vs.\ Internal's 16.8\,ms, a 5.6$\times$ overhead), and connection establishment alone costs 47.3\,ms (3$\times$ overhead). Every new connection incurs that overhead, so workloads dominated by -short-lived connections accumulate it rapidly. Bulk downloads, by +short-lived connections accumulate it rapidly. Bulk +downloads, by contrast, amortize it: the Nix cache test finishes only 18\,\% slower than Internal (10.07\,s vs.\ 8.53\,s) because once the transfer phase begins, per-connection latency fades into the @@ -903,69 +1036,101 @@ background. Mycelium is also the slowest VPN to recover from a reboot: 76.6~seconds on average, and almost suspiciously uniform across -nodes (75.7, 75.7, 78.3\,s). That kind of consistency points to a -hard-coded convergence timer in the overlay protocol rather than -anything topology-dependent. The UDP test timed out at -120~seconds, and even first-time connectivity required a -70-second wait at startup. +nodes (75.7, 75.7, 78.3\,s). That kind of consistency points to +a fixed convergence timer in the overlay protocol — +most likely a +default interval rather than anything topology-dependent. +% TODO: Identify which Mycelium constant or default this 75-78 s +% recovery actually corresponds to before claiming it is a fixed +% timer; the source code would settle whether it is hard-coded, +% a configurable default, or coincidence. +The UDP test timed out at 120~seconds, and even first-time +connectivity required a 70-second wait at startup. % Explain what topology-dependent means in this case. \paragraph{Tinc: Userspace Processing Bottleneck.} -Tinc is a clear case of a CPU bottleneck masquerading as a network +Tinc is a clear case of a CPU bottleneck masquerading +as a network problem. At 1.19\,ms latency, packets get through the tunnel quickly. Yet throughput tops out at 336\,Mbps, barely a -third of the bare-metal link. % TODO: "path MTU is a healthy 1,500 bytes" but blksize_bytes is -% 1,353. These are different metrics --- blksize_bytes is the UDP -% payload size, not the path MTU. Clarify the distinction or -% remove the 1,500 claim. +third of the bare-metal link. The usual suspects do not apply: Tinc's effective UDP payload size (\texttt{blksize\_bytes} of -1\,353 from UDP iPerf3, comparable to VpnCloud at 1\,375 and + 1\,353 from UDP iPerf3, comparable to VpnCloud at 1\,375 and WireGuard at 1\,368) is in the normal range, and its retransmit -count (240) is moderate. What limits Tinc is its single-threaded -userspace architecture: one CPU core simply cannot encrypt, copy, +count (240) is moderate. What limits Tinc is its +single-threaded +userspace architecture: one CPU core simply cannot +encrypt, copy, and forward packets fast enough to fill the pipe. -% TODO: DOWNSTREAM DEPENDENCY — This "confirms" the Tinc CPU bottleneck -% diagnosis from above, but the 14.9% CPU figure has an unresolved TODO -% (the same utilization as VpnCloud at 539 Mbps). If the CPU claim is +% TODO: DOWNSTREAM DEPENDENCY — This "confirms" the +% Tinc CPU bottleneck +% diagnosis from above, but the 14.9% CPU figure has +% an unresolved TODO +% (the same utilization as VpnCloud at 539 Mbps). If +% the CPU claim is % revised or refuted, this confirmation must be updated too. The parallel benchmark confirms this diagnosis. Tinc scales to 563\,Mbps (1.68$\times$), beating Internal's 1.50$\times$ ratio. -Multiple TCP streams collectively keep that single core busy during -what would otherwise be idle gaps in any individual flow, squeezing +Multiple TCP streams collectively keep that single +core busy during +what would otherwise be idle gaps in any individual +flow, squeezing out throughput that no single stream could reach alone. -\section{Impact of Network Impairment} +\section{Impact of network impairment} \label{sec:impairment} -Baseline benchmarks rank VPNs by overhead under ideal conditions. -The impairment profiles from Table~\ref{tab:impairment_profiles} -test a different property: resilience. Two results dominate the -data. First, the throughput hierarchy from -Section~\ref{sec:baseline} collapses under degradation --- at High -impairment, the 675\,Mbps spread across all implementations compresses -to under 3\,Mbps, and architectural differences that matter at gigabit speeds -vanish. Second, Headscale outperforms the bare-metal Internal -baseline at Medium impairment across TCP, parallel TCP, and Nix -cache benchmarks. A VPN built on WireGuard should not beat a direct -connection; Section~\ref{sec:tailscale_degraded} traces the cause to -three TCP parameters in Tailscale's userspace network stack. +Baseline benchmarks rank VPNs by overhead under ideal +conditions. +The impairment profiles in +Table~\ref{tab:impairment_profiles} test +a different property: resilience. Two results +dominate the data. + +The first is the collapse of the throughput hierarchy. At High +impairment, the 675\,Mbps spread between fastest and slowest +implementation compresses to under 3\,Mbps. Architectural +differences that mattered at gigabit speeds become +invisible once +the network is the bottleneck. + +The second is harder to explain. Headscale outperforms the +bare-metal Internal baseline at Medium impairment across TCP, +parallel TCP, and the Nix cache benchmark. A VPN built on +WireGuard should not beat a direct connection. +Section~\ref{sec:tailscale_degraded} pursues this anomaly +through what turns out to be the wrong hypothesis. The +investigation begins with Tailscale's much-discussed gVisor TCP +stack, validates the candidate parameters in isolation on the +bare-metal host, and only then discovers — by reading the rig's +own NixOS module — that the gVisor stack is not actually in the +data path of the benchmark at all. The real culprit is a +combination of the Linux kernel's tight default +\texttt{tcp\_reordering} threshold and the way +\texttt{wireguard-go} +batches packets between the wire and the host kernel TCP stack. \subsection{Ping} -Latency is the most predictable metric under impairment. Most VPNs -absorb the injected delay with a fixed per-hop overhead, and rankings +Latency is the most predictable metric under +impairment. Most VPNs +absorb the injected delay with a fixed per-hop +overhead, and rankings within the central cluster barely change across profiles -(Table~\ref{tab:ping_impairment}). tc~netem adds roughly 4, 8, and -15\,ms of round-trip delay at Low, Medium, and High respectively; +(Table~\ref{tab:ping_impairment}). tc~netem adds +roughly 4, 8, and +15\,ms of round-trip delay at Low, Medium, and High +respectively; Internal's measured values (4.82, 9.38, 15.49\,ms) confirm this. \begin{table}[H] \centering - \caption{Average ping RTT (ms) across impairment profiles, sorted + \caption{Average ping RTT (ms) across impairment + profiles, sorted by High-profile RTT} \label{tab:ping_impairment} \begin{tabular}{lrrrr} @@ -990,74 +1155,107 @@ Internal's measured values (4.82, 9.38, 15.49\,ms) confirm this. \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/Ping Average RTT Heatmap}.png} - \caption{Average ping RTT across impairment profiles. Most VPNs + \includegraphics[width=\textwidth]{{Figures/impairment/Ping + Average RTT Heatmap}.png} + \caption{Average ping RTT across impairment + profiles. Most VPNs form a tight parallel band; Mycelium's non-monotonic curve, EasyTier's excess latency at High, and Hyprspace's upward divergence stand out.} \label{fig:ping_impairment_heatmap} \end{figure} -Mycelium defies the pattern. Its RTT \emph{drops} from 34.9\,ms at -baseline to 23.4\,ms at Low impairment, a 33\% improvement where -every other VPN gets slower. It then rises to 43.9\,ms at Medium -before falling again to 33.0\,ms at High. The baseline analysis -(Section~\ref{sec:mycelium_routing}) showed that Mycelium's latency -comes from a bimodal routing distribution: one path runs at 1.63\,ms -while two others route through the global overlay at -${\sim}$51\,ms. % TODO: DOWNSTREAM DEPENDENCY — This explanation depends on the baseline -% characterisation of Mycelium's path discovery as "failing intermittently" -% (Section mycelium_routing). If that characterisation is revised (e.g., -% overlay routing is by-design, not a failure), then the claim that -% impairment "pushes path discovery toward shorter routes" needs rethinking: -% the mechanism would be different if Mycelium is not trying to find direct +Mycelium defies the pattern. Its RTT \emph{drops} +from 34.9\,ms at +baseline to 23.4\,ms at Low impairment, a 33\% +improvement at the +profile where every other VPN gets slower. It then climbs to +43.9\,ms at Medium before falling again to 33.0\,ms +at High. The +baseline analysis +(Section~\ref{sec:mycelium_routing}) showed that +Mycelium's latency comes from a bimodal routing +distribution: one +path runs at 1.63\,ms, two others route through the +global overlay at +${\sim}$51\,ms. % TODO: DOWNSTREAM DEPENDENCY — This +% explanation depends on the baseline +% characterisation of Mycelium's path discovery as +% "failing intermittently" +% (Section mycelium_routing). If that +% characterisation is revised (e.g., +% overlay routing is by-design, not a failure), then +% the claim that +% impairment "pushes path discovery toward shorter +% routes" needs rethinking: +% the mechanism would be different if Mycelium is not +% trying to find direct % routes in the first place. -The impairment appears to push Mycelium's path -discovery toward shorter routes, so a larger share of traffic takes -the direct path. The non-monotonic pattern is consistent with a path -selection algorithm that responds to measured link quality, but not -linearly with degradation severity. +Impairment seems to push Mycelium's path selection toward the +shorter route, so a larger share of traffic avoids the overlay +detour. The non-monotonic curve is consistent with a +path selection +algorithm that reacts to measured link quality but +not linearly with +degradation severity. % TODO: Ping packet loss data is not shown in any figure. Add a -% packet loss table/figure or reference the raw data so readers can +% packet loss table/figure or reference the raw data +% so readers can % verify these numbers. -Mycelium also achieves 0\% ping packet loss at Low and Medium -impairment, while most VPNs show 0.1--3.2\% loss at those profiles. -At High impairment, Mycelium's loss jumps to 11.1\%. +Mycelium loses zero ping packets at Low and Medium impairment. +Most other VPNs show 0.1--3.2\% loss at those profiles. At High +impairment Mycelium's loss jumps to 11.1\%. -% TODO: EasyTier's max RTT (290 ms), WireGuard's max (~40 ms), and -% EasyTier's std dev (44.6 ms) are not shown in any plot. The ping -% heatmap only shows averages. Add a jitter/distribution figure. +% TODO: EasyTier's max RTT (290 ms), WireGuard's max +% (~40 ms), and +% EasyTier's std dev (44.6 ms) are not shown in any +% plot. The ping +% heatmap only shows averages. Add a +% jitter/distribution figure. % Also, the "userspace retry mechanism" is a hypothesized cause % without source-code or packet-level evidence. EasyTier accumulates 11\,ms of excess latency at High impairment -beyond what tc~netem accounts for. Its average RTT of 26.6\,ms and -maximum of 290\,ms (vs.\ ${\sim}$40\,ms for WireGuard) suggest a -userspace retry mechanism that introduces escalating variance. -EasyTier's RTT standard deviation reaches 44.6\,ms at High, the -worst jitter of any VPN. +beyond what tc~netem injects. Its average RTT is +26.6\,ms and its +maximum reaches 290\,ms, against ${\sim}$40\,ms for +WireGuard. The +RTT standard deviation reaches 44.6\,ms at High, the +worst jitter +of any VPN. A userspace retry mechanism is the +likely cause, but +without source-code evidence we cannot say so with certainty. % TODO: Ping packet loss data is not shown in any plot. The 1/9 -% = 11.1\% interpretation is clever but depends on the exact test -% structure (3 pairs × 3 runs × 100 packets). Verify this matches +% = 11.1\% interpretation is clever but depends on +% the exact test +% structure (3 pairs × 3 runs × 100 packets). Verify +% this matches % the actual test setup and add a supporting figure or table. -Hyprspace shows 11.1\% ping packet loss at every impairment level --- -Low, Medium, and High alike. With 9~measurement runs (3~machine -pairs $\times$ 3~runs of 100~packets), 11.1\% equals exactly 1/9: -one run per profile fails completely while the other eight report zero -loss. % TODO: DOWNSTREAM DEPENDENCY — This is a third reference to the buffer -% bloat diagnosis from Section hyprspace_bloat, which depends on the +Hyprspace shows the same 11.1\% ping packet loss at Low, Medium, +and High impairment. With 9~measurement runs per +profile (3~machine +pairs $\times$ 3~runs of 100~packets), 11.1\% is +exactly 1/9: one +run fails completely while the other eight report zero loss. +% TODO: DOWNSTREAM DEPENDENCY — This is a third +% reference to the buffer +% bloat diagnosis from Section hyprspace_bloat, which +% depends on the % unverified 2,800 ms under-load latency. If that diagnosis is % revised, this explanation must also be revisited. -This binary pass/fail behavior is consistent with the buffer bloat -diagnosis from Section~\ref{sec:hyprspace_bloat}: when buffers fill, -an entire path stalls rather than degrading gradually. +The binary pass/fail behaviour fits the buffer bloat +diagnosis from +Section~\ref{sec:hyprspace_bloat}: when the tunnel's +buffers fill, a +path stalls completely rather than degrading gradually. -\subsection{TCP Throughput} +\subsection{TCP throughput} -TCP throughput is where the baseline hierarchy breaks down. The -three performance tiers from Section~\ref{sec:baseline} dissolve at -the first impairment step (Table~\ref{tab:tcp_impairment}). +The baseline TCP hierarchy does not survive impairment. The +three performance tiers from +Section~\ref{sec:baseline} dissolve at +the first step (Table~\ref{tab:tcp_impairment}). \begin{table}[H] \centering @@ -1089,116 +1287,186 @@ the first impairment step (Table~\ref{tab:tcp_impairment}). \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/TCP Throughput Heatmap}.png} - \caption{Single-stream TCP throughput across impairment profiles. + \includegraphics[width=\textwidth]{{Figures/impairment/TCP + Throughput Heatmap}.png} + \caption{Single-stream TCP throughput across + impairment profiles. Headscale crosses above Internal at Medium impairment; Yggdrasil collapses from 795 to 13\,Mbps at Low; all VPNs converge at High.} \label{fig:tcp_impairment_heatmap} \end{figure} -Yggdrasil crashes from 795\,Mbps to 13.2\,Mbps at Low impairment, a -98.3\% throughput loss from adding just 2\,ms latency, 2\,ms jitter, -0.25\% packet loss, and 0.5\% reordering per machine. Even Mycelium, -the slowest VPN at baseline (259\,Mbps), retains more throughput at -Low than Yggdrasil does. The jumbo overlay MTU of 32\,731~bytes, -which inflated baseline metrics -(Section~\ref{sec:baseline}), becomes a liability under impairment: -each lost or reordered outer packet triggers retransmission of -${\sim}$24$\times$ more inner-layer data than a standard -1\,400-byte MTU VPN would lose. +Yggdrasil crashes from 795\,Mbps to 13.2\,Mbps at Low +impairment, a +98.3\% loss after adding only 2\,ms of latency, 2\,ms of jitter, +0.25\% packet loss, and 0.5\% reordering per machine. +Even Mycelium, +the slowest VPN at baseline (259\,Mbps), retains more +throughput at +Low than Yggdrasil does. The jumbo overlay MTU of 32\,731~bytes +that inflated Yggdrasil's baseline numbers +(Section~\ref{sec:baseline}) becomes a liability +under impairment: +every lost or reordered outer packet costs roughly +24$\times$ more +retransmitted inner data than a standard 1\,400-byte +MTU VPN would +lose. -Headscale retains 34.3\% of its baseline throughput at Low, nearly -matching Internal's 35.7\%. At Medium impairment, Headscale -(41.5\,Mbps) overtakes Internal (29.6\,Mbps) --- a VPN outperforming -the bare-metal baseline. -Section~\ref{sec:tailscale_degraded} investigates this anomaly in +Headscale retains 34.3\% of its baseline throughput +at Low, almost +the same as Internal's 35.7\%. At Medium impairment, Headscale +(41.5\,Mbps) overtakes Internal (29.6\,Mbps). +Section~\ref{sec:tailscale_degraded} investigates +this anomaly in detail. -At High impairment, the throughput range compresses from 675\,Mbps at -baseline to just 2.9\,Mbps. Internal leads at 4.25\,Mbps; Hyprspace -trails at 1.39\,Mbps. The impairment profile itself becomes the -bottleneck. With 2.5\% packet loss and 5\% reordering per machine, -every implementation is TCP-loss-limited, and architectural -differences that matter at gigabit speeds become irrelevant. +At High impairment, the throughput range collapses +from 675\,Mbps to +2.9\,Mbps. Internal leads at 4.25\,Mbps, Hyprspace trails at +1.39\,Mbps, and the impairment profile itself is the bottleneck. +With 2.5\% packet loss and 5\% reordering per machine, every +implementation is loss-limited, and the architectural +differences +that mattered at gigabit speeds no longer matter at all. -\subsection{UDP Throughput} +\subsection{UDP throughput} -The UDP stress test (\texttt{-b~0}) separates kernel-level from -userspace implementations more cleanly than any TCP benchmark. It -also produces widespread failures under impairment: Hyprspace and -Mycelium, which already failed at baseline, continue to time out at -% TODO: Tinc fails at Low and Medium but succeeds at High (8 Mbps) --- -% the same non-monotonic failure pattern as Internal/WireGuard (fail -% at Low, succeed at Medium/High). This suggests the failures are -% iPerf3/tc interaction issues rather than fundamental VPN limitations. -% Nebula and VpnCloud also fail selectively. The widespread non-monotonic -% failure pattern undermines using this benchmark as a reliability -% indicator (see line 1163 claim). Consider discussing this pattern. -all profiles, and Tinc drops out at Low and Medium while ZeroTier -fails at Medium. Despite the sparse dataset, one pattern is clear. +The UDP stress test (\texttt{-b~0}) separates +implementations with +effective backpressure from those without it more +cleanly than any +TCP benchmark. Under impairment, it also produces widespread +failures. +% TODO: Tinc fails at Low and Medium but succeeds at +% High (8 Mbps): +% the same non-monotonic failure pattern as +% Internal/WireGuard (fail +% at Low, succeed at Medium/High). This suggests the +% failures are +% iPerf3/tc interaction issues rather than +% fundamental VPN limitations. +% Nebula and VpnCloud also fail selectively. The +% widespread non-monotonic +% failure pattern undermines using this benchmark as +% a reliability +% indicator (see line 1163 claim). Consider +% discussing this pattern. +Hyprspace and Mycelium continue to time out at all profiles, +extending their baseline failures. Tinc drops out at Low and +Medium, ZeroTier at Medium. The data is sparse, but one pattern +emerges from the runs that did complete. -% TODO: The heatmap shows Internal and WireGuard both fail (×) at -% some impairment profiles (e.g., Internal fails at Low, WireGuard +% TODO: The heatmap shows Internal and WireGuard both +% fail (×) at +% some impairment profiles (e.g., Internal fails at +% Low, WireGuard % at Low and High). "Regardless of impairment" overstates the % evidence. Rephrase to reflect the failures, or explain why % those runs failed despite the claim of maintained throughput. -% TODO: Internal (and WireGuard) fail at Low impairment in the UDP -% test but succeed at Medium and High --- the opposite of what one -% would expect. This is never explained. Investigate and add an -% explanation (e.g., iPerf3 crash, tc interaction, timing issue). -Kernel-level implementations maintain throughput at the profiles -where data exists. Internal holds ${\sim}$950\,Mbps at -Baseline, Medium, and High. Headscale sustains 700--876\,Mbps and WireGuard -850--898\,Mbps; % TODO: verify WireGuard UDP range -- analysis doc says 850-898, possible digit transposition -both use WireGuard's kernel module for the outer tunnel, which -provides proper backpressure at the transport layer. Userspace VPNs collapse: EasyTier drops from +% TODO: Internal (and WireGuard) fail at Low +% impairment in the UDP +% test but succeed at Medium and High: the opposite of what one +% would expect. This is never explained. +% Investigate and add an +% explanation (e.g., iPerf3 crash, tc interaction, +% timing issue). +Three implementations maintain throughput at the profiles where +data exists. Internal holds ${\sim}$950\,Mbps at +Baseline, Medium, +and High; WireGuard sustains 850--898\,Mbps; and +Headscale sustains +700--876\,Mbps. % TODO: verify WireGuard UDP range -- +% analysis doc says 850-898, possible digit transposition +Internal and WireGuard ride the host kernel's transport-layer +backpressure (Internal directly, WireGuard via the in-kernel +WireGuard module). Headscale, by contrast, never +uses the kernel +module even though it builds on the WireGuard protocol: as +established in Section~\ref{sec:baseline}, Tailscale's +\texttt{magicsock} layer intercepts every packet for endpoint +selection, DERP relay, and the disco protocol, and that +interception is incompatible with the kernel WireGuard datapath. +Headscale therefore runs \texttt{wireguard-go} in userspace and +compensates with UDP batching +(\texttt{recvmmsg}/\texttt{sendmmsg}), +host-kernel UDP segmentation/aggregation offload +(\texttt{UDP\_SEGMENT}/\texttt{UDP\_GRO}, applied to the outer +WireGuard socket), and a 7\,MB socket buffer on the same outer +socket. These offloads live in the host kernel; gVisor netstack +itself implements no UDP GSO or UDP GRO of its own. +Together they +absorb a \texttt{-b 0} sender flood without +collapsing. Userspace +VPNs without the same engineering do collapse: +EasyTier drops from 865 to 435 to 38.5 to 6.1\,Mbps across successive profiles. -Yggdrasil, already pathological at baseline (98.7\% loss), crashes to -12.3\,Mbps at Low and fails entirely at Medium and High. +Yggdrasil, already pathological at baseline (98.7\% +loss), crashes +to 12.3\,Mbps at Low and fails entirely at Medium and High. \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/UDP Receiver Throughput Heatmap}.png} - % TODO: This caption says "kernel-level VPNs maintain high throughput" - % but the heatmap shows Internal, WireGuard, and Headscale ALL fail - % ($\times$) at Low impairment. WireGuard also fails at High. - % Rephrase to acknowledge the failures or explain them. + \includegraphics[width=\textwidth]{{Figures/impairment/UDP + Receiver Throughput Heatmap}.png} + % TODO: The heatmap shows Internal, WireGuard, and + % Headscale all + % fail ($\times$) at Low impairment. WireGuard also fails at + % High. These selective failures need an explanation + % (iPerf3/tc interaction?). \caption{UDP receiver throughput across impairment profiles. - Kernel-level VPNs (Internal, WireGuard, Headscale) maintain high - throughput where they complete; userspace VPNs collapse or fail - entirely ($\times$ marks a failed run).} + Implementations with effective UDP backpressure + (Internal and + WireGuard via the in-kernel datapath; Headscale via + \texttt{wireguard-go} batching plus large socket buffers) + maintain high throughput where they complete; + other userspace + VPNs collapse or fail entirely ($\times$ marks a failed run).} \label{fig:udp_impairment_heatmap} \end{figure} -% TODO: This "robustness indicator" interpretation is undermined by -% the non-monotonic failure pattern. Internal and WireGuard fail at -% Low (0.25% loss) but succeed at Medium and High (1%+ loss). If -% failures indicated "fundamental flow-control problems," they should -% get worse with more impairment, not better. The pattern suggests -% iPerf3 or tc timing issues rather than VPN limitations. Either +% TODO: This "robustness indicator" interpretation is +% undermined by +% the non-monotonic failure pattern. Internal and +% WireGuard fail at +% Low (0.25% loss) but succeed at Medium and High +% (1%+ loss). If +% failures indicated "fundamental flow-control +% problems," they should +% get worse with more impairment, not better. The +% pattern suggests +% iPerf3 or tc timing issues rather than VPN +% limitations. Either % explain the non-monotonic failures or weaken this conclusion. -The failure rate of this benchmark under impairment makes it more -useful as a robustness indicator than a throughput measurement. A VPN -that cannot complete a 30-second UDP flood under 0.25\% packet loss -has fundamental flow-control problems that will surface under real -workloads too, even if the symptoms are milder. +Under impairment this benchmark is more useful as a robustness +indicator than as a throughput measurement. A VPN that cannot +complete a 30-second UDP flood under 0.25\% packet loss has a +flow-control problem that will surface under real workloads too, +even when the symptoms are milder. \subsection{Parallel TCP} -% TODO: DOWNSTREAM DEPENDENCY — "six unidirectional flows" must match -% the baseline parallel test description. The baseline section has an -% unresolved TODO about whether the test uses 6 or 10 streams. If the -% baseline is corrected to 10, this section must also be updated. +% TODO: DOWNSTREAM DEPENDENCY — "six unidirectional +% flows" must match +% the baseline parallel test description. The +% baseline section has an +% unresolved TODO about whether the test uses 6 or 10 +% streams. If the +% baseline is corrected to 10, this section must also +% be updated. The Headscale anomaly from single-stream TCP grows larger under -parallel load. Table~\ref{tab:parallel_impairment} shows aggregate +parallel load. Table~\ref{tab:parallel_impairment} +shows aggregate throughput across three concurrent bidirectional links (six unidirectional flows). \begin{table}[H] \centering - \caption{Parallel TCP throughput (Mbps) across impairment profiles. - Three concurrent bidirectional links produce six unidirectional + \caption{Parallel TCP throughput (Mbps) across + impairment profiles. + Three concurrent bidirectional links produce six + unidirectional flows.} \label{tab:parallel_impairment} \begin{tabular}{lrrrr} @@ -1223,77 +1491,93 @@ unidirectional flows). \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/Parallel TCP Throughput Heatmap}.png} + \includegraphics[width=\textwidth]{{Figures/impairment/Parallel + TCP Throughput Heatmap}.png} \caption{Parallel TCP throughput across impairment profiles. Headscale dominates at Low (718\,Mbps vs.\ Internal's 277); - EasyTier is the runner-up (473\,Mbps); Hyprspace collapses to + EasyTier is the runner-up (473\,Mbps); Hyprspace + collapses to 2.87\,Mbps.} \label{fig:parallel_impairment_heatmap} \end{figure} -Headscale at Low impairment: 718\,Mbps --- 2.6$\times$ Internal -(277\,Mbps) and 4.1$\times$ WireGuard (173\,Mbps). At Medium, -Headscale (113\,Mbps) still leads Internal (82.6\,Mbps) by 37\%. -Whatever mechanism produces the single-stream crossover at Medium -scales with the number of flows: six independent streams each -benefit from it. +At Low impairment, Headscale reaches 718\,Mbps: 2.6$\times$ +Internal's 277\,Mbps and 4.1$\times$ WireGuard's 173\,Mbps. At +Medium, Headscale (113\,Mbps) still leads Internal +(82.6\,Mbps) by +37\%. Whatever mechanism produces the single-stream +crossover at +Medium scales with the flow count, because each of the six +concurrent streams benefits from it independently. -% TODO: EasyTier's resilience (473 Mbps at Low, 51% retention) is the -% second-best result after Headscale, yet receives no architectural -% explanation. Headscale gets an entire subsection attributing its -% resilience to gVisor TCP tuning. Either explain what gives EasyTier -% its resilience (e.g., its own TCP stack, congestion control, FEC) -% or acknowledge the gap explicitly. -EasyTier is the second-most resilient VPN under parallel load, at -473\,Mbps at Low (51\% of baseline). Both EasyTier and Headscale -retain more than half their baseline parallel throughput at Low -impairment; no other VPN exceeds 30\%. +EasyTier is the runner-up under parallel load: 473\,Mbps at Low, +51\% of its baseline. Headscale and EasyTier are the only VPNs +that retain more than half their baseline parallel throughput at +Low impairment; no other implementation exceeds 30\%. +We have no +direct architectural explanation for EasyTier's resilience and +do not claim one here. -Hyprspace collapses from 803\,Mbps to 2.87\,Mbps at Low, a 99.6\% -loss. % TODO: DOWNSTREAM DEPENDENCY — This references the buffer bloat diagnosis -% from Section hyprspace_bloat, which depends on the unverified 2,800 ms -% under-load latency. If that diagnosis is revised, this explanation +Hyprspace collapses from 803\,Mbps to 2.87\,Mbps at +Low, a 99.6\% +loss. % TODO: DOWNSTREAM DEPENDENCY — This +% references the buffer bloat diagnosis +% from Section hyprspace_bloat, which depends on the +% unverified 2,800 ms +% under-load latency. If that diagnosis is revised, +% this explanation % for parallel collapse must also be revisited. -The buffer bloat that plagues single-stream transfers -(Section~\ref{sec:hyprspace_bloat}) becomes catastrophic when six -concurrent flows compete for the same bloated buffers. +The buffer bloat that already plagues single-stream transfers +(Section~\ref{sec:hyprspace_bloat}) turns catastrophic when six +flows compete for the same bloated buffers at once. -The High-profile convergence effect is even more pronounced here than -in single-stream mode. Tinc and VpnCloud land at identical -8.25\,Mbps despite differing by 200\,Mbps at baseline. +High-profile convergence is more pronounced here than in +single-stream mode. Tinc and VpnCloud land at identical +8.25\,Mbps even though they differ by 200\,Mbps at baseline. -\subsection{QUIC Performance} +\subsection{QUIC performance} Headscale and Nebula failed the qperf QUIC benchmark at baseline -(Section~\ref{sec:baseline}) and continue to fail across all -impairment profiles. +(Section~\ref{sec:baseline}) and continue to fail at every +impairment profile. Yggdrasil's QUIC bandwidth drops from 745\,Mbps at baseline to -7.67\,Mbps at Low, 3.45\,Mbps at Medium, and 2.17\,Mbps at High --- -the same cliff observed in its TCP results, again driven by -jumbo-MTU amplification of outer-layer packet loss. +7.67\,Mbps at Low, 3.45\,Mbps at Medium, and 2.17\,Mbps at High. +This is the same cliff observed in its TCP results, +driven by the +same jumbo-MTU amplification of outer-layer packet loss. -At High impairment, WireGuard (23.2\,Mbps), VpnCloud (23.4\,Mbps), +At High impairment, WireGuard (23.2\,Mbps), VpnCloud +(23.4\,Mbps), ZeroTier (23.0\,Mbps), and Tinc (23.4\,Mbps) converge to within -0.4\,Mbps of each other. At baseline these four span a 188\,Mbps -range (844 to 656\,Mbps). QUIC's own congestion control, operating atop the -already-degraded outer link, becomes the sole limiter. +0.4\,Mbps of one another. At baseline these four +span a 188\,Mbps +range (656 to 844\,Mbps). QUIC's own congestion +control, running on +top of an already-degraded outer link, has become the +sole limiter. \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/QUIC Bandwidth Heatmap}.png} + \includegraphics[width=\textwidth]{{Figures/impairment/QUIC + Bandwidth Heatmap}.png} \caption{QUIC bandwidth across impairment profiles. Yggdrasil - drops from 745 to 8\,Mbps at Low; WireGuard, VpnCloud, ZeroTier, - and Tinc converge to ${\sim}$23\,Mbps at High. Headscale and + drops from 745 to 8\,Mbps at Low; WireGuard, + VpnCloud, ZeroTier, + and Tinc converge to ${\sim}$23\,Mbps at High. + Headscale and Nebula fail at all profiles ($\times$).} \label{fig:quic_impairment_heatmap} \end{figure} -\subsection{Video Streaming} +\subsection{Video streaming} -At ${\sim}$3.3\,Mbps, the RIST video stream sits within every VPN's -throughput budget even at High impairment. Quality differences in -Table~\ref{tab:rist_impairment} therefore reflect packet delivery +At ${\sim}$3.3\,Mbps, the RIST video stream sits +within every VPN's +throughput budget even at High impairment. Quality +differences in +Table~\ref{tab:rist_impairment} therefore reflect +packet delivery reliability, not bandwidth. \begin{table}[H] @@ -1323,51 +1607,69 @@ reliability, not bandwidth. \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/Video Streaming Quality Heatmap}.png} - \caption{RIST video streaming quality across impairment profiles. + \includegraphics[width=\textwidth]{{Figures/impairment/Video + Streaming Quality Heatmap}.png} + \caption{RIST video streaming quality across + impairment profiles. Headscale is stuck at ${\sim}$13\% regardless of profile; Mycelium maintains ${\sim}$100\% even at High; Yggdrasil declines steeply to 43\%.} \label{fig:rist_impairment_heatmap} \end{figure} -Headscale stays at ${\sim}$13\% across all four profiles: 13.1\%, -13.0\%, 13.0\%, 13.0\%. The profile-independence confirms the -baseline diagnosis from Section~\ref{sec:baseline}. The failure is -% TODO: DOWNSTREAM DEPENDENCY — This repeats the DERP/MTU hypothesis from -% Section baseline as though it were established. The baseline TODO notes -% this hypothesis is unverified (no packet capture evidence). Do not -% present it as a confirmed diagnosis here without resolving the upstream TODO. -structural --- likely MTU fragmentation in the DERP relay layer --- -and cannot worsen because it is already saturated. Adding latency or -loss on top of an 87\% packet drop floor changes nothing. +Headscale sits at ${\sim}$13\% across all four profiles: 13.1\%, +13.0\%, 13.0\%, 13.0\%. This profile-independence confirms the +baseline diagnosis (Section~\ref{sec:baseline}): the failure is +% TODO: DOWNSTREAM DEPENDENCY — This repeats the +% DERP/MTU hypothesis from +% Section baseline as though it were established. +% The baseline TODO notes +% this hypothesis is unverified (no packet capture +% evidence). Do not +% present it as a confirmed diagnosis here without +% resolving the upstream TODO. +structural (most plausibly MTU fragmentation in the DERP relay +layer) and cannot worsen because it is already +saturated. Adding +latency or loss on top of an 87\% packet drop floor changes +nothing. -Mycelium delivers 99.9\% quality even at High impairment, better than +Mycelium holds 99.9\% quality even at High impairment, ahead of Internal (80.2\%) and every other VPN. At 3.3\,Mbps, even -Mycelium's degraded overlay paths can sustain the stream. The same -overlay routing that adds 34.9\,ms of latency and cripples bulk TCP -transfers is harmless at video bitrates. RIST's own forward error -correction compensates for whatever packet loss remains. +Mycelium's degraded overlay paths comfortably sustain +the stream. +The same overlay routing that adds 34.9\,ms of +latency and cripples +bulk TCP transfers is harmless at video bitrates, and RIST's +forward error correction handles the residual loss. -% TODO: The claim that jumbo MTU causes burst losses that overwhelm -% FEC is a hypothesis. No FEC analysis or packet-level evidence is -% shown. Consider adding packet capture data or softening the claim. -Yggdrasil degrades the most steeply: 100\% at baseline, 94.7\% at -Low, 71.4\% at Medium, 43.3\% at High. The jumbo MTU that hurt TCP -throughput likely hurts here too --- large overlay packets carrying -RIST data are more likely to be lost or reordered at the outer layer, -and RIST's FEC may not recover from the resulting burst losses. +% TODO: The claim that jumbo MTU causes burst losses +% that overwhelm +% FEC is a hypothesis. No FEC analysis or +% packet-level evidence is +% shown. Consider adding packet capture data or +% softening the claim. +Yggdrasil degrades the most steeply: 100\% at +baseline, 94.7\% at +Low, 71.4\% at Medium, 43.3\% at High. The jumbo MTU +that hurt TCP +throughput likely hurts here as well: large overlay packets are +more exposed to loss and reordering at the outer layer, and the +resulting burst losses may exceed what RIST's FEC can recover. -\subsection{Application-Level Download} +\subsection{Application-level download} + +The Nix binary cache download is the most demanding +application-level benchmark. Hundreds of sequential HTTP +connections amplify the per-connection latency +penalties that bulk +throughput tests amortise. Table~\ref{tab:nix_impairment} shows +download times across profiles. -The Nix binary cache download is the most demanding application-level -benchmark: hundreds of sequential HTTP connections amplify -per-connection latency penalties that bulk throughput tests amortize. -Table~\ref{tab:nix_impairment} shows download times across profiles. - \begin{table}[H] \centering - \caption{Nix binary cache download time (seconds) across impairment + \caption{Nix binary cache download time (seconds) + across impairment profiles, sorted by Low-profile time. ``--'' marks a failed run.} \label{tab:nix_impairment} @@ -1393,55 +1695,81 @@ Table~\ref{tab:nix_impairment} shows download times across profiles. \begin{figure}[H] \centering - \includegraphics[width=\textwidth]{{Figures/impairment/Nix Cache Download Time Heatmap}.png} - \caption{Nix binary cache download time across impairment profiles. - Headscale, Nebula, and Tinc complete all four profiles; Headscale + \includegraphics[width=\textwidth]{{Figures/impairment/Nix + Cache Download Time Heatmap}.png} + \caption{Nix binary cache download time across + impairment profiles. + Headscale, Nebula, and Tinc complete all four + profiles; Headscale beats Internal at Medium (49\,s vs.\ 59\,s). Yggdrasil's - Low-profile time explodes to 230\,s ($\times$ marks a failed run).} + Low-profile time explodes to 230\,s ($\times$ marks + a failed run).} \label{fig:nix_impairment_heatmap} \end{figure} -Headscale, Nebula, and Tinc are the only VPNs to complete all four -profiles. At Medium impairment, Headscale finishes in 48.8~seconds ---- faster than Internal's 58.6~seconds. Internal itself fails at -High impairment while Headscale completes in 219~seconds, Tinc in +Headscale, Nebula, and Tinc are the only VPNs to +complete all four +profiles. At Medium impairment, Headscale finishes +in 48.8~seconds, +faster than Internal's 58.6~seconds. Internal itself +fails at High +impairment while Headscale completes in 219~seconds, Tinc in 496~seconds, and Nebula in 547~seconds. Yggdrasil's download time explodes from 10.6\,s to 230\,s at Low -impairment, a 22$\times$ slowdown. Every HTTP request incurs the -latency penalty from Yggdrasil's impairment-amplified -retransmissions. Mycelium also degrades severely (10.1\,s to -79.5\,s, an 8$\times$ increase), consistent with its overlay routing -overhead, which compounds over hundreds of sequential HTTP -connections. +impairment, a 22$\times$ slowdown. Every HTTP request pays the +latency penalty of Yggdrasil's impairment-amplified +retransmissions. +Mycelium degrades almost as badly (10.1\,s to 79.5\,s, an +8$\times$ increase): its overlay routing overhead compounds over +hundreds of sequential HTTP connections. % TODO: Hyprspace fails at Low but completes at Medium (170 s). -% This contradicts the "clean gradient" claim. Explain why a VPN +% This contradicts the "clean gradient" claim. +% Explain why a VPN % can fail at Low but succeed at Medium, or note the anomaly. -The failure map reveals a mostly clean gradient: more demanding -profiles knock out more VPNs. At Low, 10 of 11 complete (Hyprspace -fails). At Medium, 9 complete (though Hyprspace, which failed at -Low, completes at 170\,s). At High, only 3 survive (Headscale, -Nebula, Tinc). Internal's failure at High is the most surprising --- the -bare-metal baseline cannot sustain a multi-connection HTTP workload -under severe degradation, but Headscale, shielded by its userspace -TCP stack, can. Section~\ref{sec:tailscale_degraded} explains why. +The failure map shows a mostly clean gradient: more demanding +profiles knock out more VPNs. At Low, 10 of 11 +finish (Hyprspace +fails). At Medium, 9 finish, though Hyprspace, which had failed +at Low, completes here in 170\,s. At High, only Headscale, +Nebula, and Tinc survive. Internal's failure at High is the +surprising one: the bare-metal baseline cannot sustain a +multi-connection HTTP workload under severe degradation, while +Headscale's userspace TCP stack pulls it through. +Section~\ref{sec:tailscale_degraded} explains why. -\section{Tailscale Under Degraded Conditions} +\section{Tailscale under degraded conditions} \label{sec:tailscale_degraded} -\subsection{Observed Anomaly} +This section is about an observation that should not exist: +Headscale, a tunnelling VPN built on a kernel TCP stack and +\texttt{wireguard-go}, beats the bare-metal Internal baseline at +Medium impairment, and at Low impairment under parallel load +beats it by a factor of 2.6. The short answer turns out to be +different from the obvious answer, and we worked it out only by +chasing the obvious answer to its end. -At Medium impairment, Headscale delivers 41.5\,Mbps single-stream TCP -throughput --- 40\% more than Internal's 29.6\,Mbps. A VPN built -atop WireGuard outperforms the bare-metal connection it tunnels -through. The anomaly is consistent across benchmarks: -Table~\ref{tab:headscale_anomaly} summarizes the comparison. +\subsection{An anomaly worth pursuing} + +At Medium impairment, Headscale reaches 41.5\,Mbps on a single +TCP stream against Internal's 29.6\,Mbps — a 40\,\% lead for +the VPN over the direct host-to-host link it tunnels through. +Headscale costs the expected ${\sim}$14\,\% at baseline, and at +Low and High impairment it lags Internal by some margin. Yet at +Medium the order inverts, and not by a sliver: a 12\,Mbps gap on +a 30\,Mbps link is well above measurement noise. The same thing +happens, more dramatically, on the parallel TCP test, where +Headscale's 718\,Mbps at Low beats Internal's 277\,Mbps by a +factor of 2.6. Table~\ref{tab:headscale_anomaly} collects the +comparison. \begin{table}[H] \centering - \caption{Headscale vs.\ Internal vs.\ WireGuard under impairment - (18.12.2025 run). For TCP benchmarks, higher is better. For + \caption{Headscale vs.\ Internal vs.\ WireGuard + under impairment + (18.12.2025 run). For TCP benchmarks, higher is + better. For Nix cache, lower is better; ``--'' marks a failed run.} \label{tab:headscale_anomaly} \begin{tabular}{llrrr} @@ -1463,236 +1791,507 @@ Table~\ref{tab:headscale_anomaly} summarizes the comparison. \begin{figure}[H] \centering \includegraphics[width=\textwidth]{Figures/impairment/headscale-vs-internal-across-profiles.png} - \caption{Single-stream TCP throughput for Internal, Headscale, and + \caption{Single-stream TCP throughput for Internal, + Headscale, and WireGuard across impairment profiles (log scale). Headscale - crosses above Internal at Medium impairment; WireGuard stays far + crosses above Internal at Medium impairment; + WireGuard stays far below both; all three converge at High.} \label{fig:headscale_vs_internal} \end{figure} -In parallel TCP at Low impairment, Headscale reaches 718\,Mbps vs.\ -Internal's 277\,Mbps (2.6$\times$). The Nix cache download at -Medium takes Headscale 48.8\,s vs.\ Internal's 58.6\,s (17\% -faster). At High impairment, Internal fails the Nix cache entirely -while Headscale completes in 219\,s. - -WireGuard, which shares Headscale's cryptographic layer, shows no -such advantage: 54.7\,Mbps at Low, 8.77\,Mbps at Medium. Whatever -protects Headscale is not the encryption or the tunnel --- it is -something in Tailscale's userspace networking stack. +WireGuard-the-kernel-module is the obvious sanity +check. It uses +the same Noise/WireGuard cryptographic protocol Tailscale ships +and is the closest available comparison without the rest of +Tailscale's stack. WireGuard shows none of Headscale's +advantage: 54.7\,Mbps at Low and 8.77\,Mbps at Medium, both well +below Internal at the same profile. So the encryption layer is +not the answer, and the basic UDP tunnel is not the answer. +Whatever Headscale is doing differently lives somewhere else in +the rest of Tailscale's implementation. % TODO: The Medium-impairment retransmit percentages (5.2\%, -% 2.4\%) are not in any table or figure. Add a retransmit rate -% table for impaired profiles or reference the data source. -The retransmit data provides the first clue. At Medium impairment, -WireGuard's retransmit rate is 5.2\% --- more than double Internal's -${\sim}$2.4\%. Headscale, despite being a VPN, matches Internal at -${\sim}$2.4\%. WireGuard uses the host kernel's TCP stack, which -treats reordered packets as losses and fires spurious retransmits; -Headscale's gVisor stack tolerates more reordering, so fewer -retransmissions are wasted on packets that were merely delayed. +% 2.4\%) are not in any table or figure. Add a retransmit +% rate table for impaired profiles or reference the data +% source. +The retransmit data narrows the search. At Medium, WireGuard's +TCP retransmit rate is 5.2\,\%, more than double Internal's +${\sim}$2.4\,\%. Headscale matches Internal at ${\sim}$2.4\,\% +even though it is a tunnelling VPN. Both Headscale and +bare-metal Internal run the same host kernel TCP stack at the +inner layer, so the asymmetry is not about a different TCP +implementation. It is about what the kernel TCP stack is being +asked to process: something on Headscale's path is suppressing +the spurious retransmits the kernel would otherwise fire under +\texttt{tc netem}-induced reordering, and WireGuard's path is +not. -\subsection{Congestion Control Analysis} +\subsection{A plausible villain: Tailscale's gVisor stack} -Tailscale uses a userspace TCP/IP stack derived from Google's gVisor -(netstack). This stack does not inherit the host kernel's TCP -parameters. Three defaults differ from the Linux kernel in ways that -matter under packet reordering: +The candidate explanation we pursued first, and the one any +reading of the upstream Tailscale documentation will lead to, +is Tailscale's userspace TCP/IP stack. The Tailscale client +imports Google's gVisor netstack +(\texttt{gvisor.dev/gvisor/pkg/tcpip}) as a Go library and uses +it as an in-process TCP implementation. The gVisor +documentation is direct about why this matters: netstack is +designed for adverse networks where the host kernel's TCP +defaults are too aggressive. Tailscale's release notes go +further, calling out specific overrides on top of gVisor — the +most visible being an explicit RACK disable and 8\,MiB / 6\,MiB +receive and send buffers. + +Reading Tailscale's source confirms it. +\texttt{wgengine/netstack/netstack.go} contains the netstack +initialiser, and Listing~\ref{lst:tailscale_netstack_overrides} +reproduces the relevant overrides verbatim. RACK is disabled +(\texttt{TCPRecovery(0)}) with a comment pointing at +\texttt{tailscale/issues/9707}: ``gVisor's RACK performs +poorly. ACKs do not appear to be handled in a timely manner, +leading to spurious retransmissions and a reduced congestion +window.'' Reno is set explicitly with a comment pointing at +\texttt{gvisor/issues/11632}, an integer-overflow bug in +gVisor's CUBIC implementation. The TCP send and receive +buffer maxima are pushed up to 8\,MiB and 6\,MiB. SACK is +enabled (gVisor's default is off). + +\lstinputlisting[language=Go,caption={Tailscale's gVisor + netstack initialiser explicitly disables RACK, pins Reno as + the congestion control, and enlarges the TCP buffer maxima. + These overrides live inside + \texttt{wgengine/netstack/netstack.go}. +\textit{tailscale/wgengine/netstack/netstack.go:264--339}},label={lst:tailscale_netstack_overrides}]{Listings/tailscale_netstack_overrides.go} + +Read against the Linux kernel defaults — RACK on, CUBIC by +default, ${\sim}$1\,MiB receive and send buffers, +\texttt{tcp\_reordering=3}, Tail Loss Probe enabled — these +overrides describe a TCP stack better suited to a lossy, +reordering link than the host kernel. The hypothesis writes +itself: Headscale's iPerf3 traffic is processed +by this gVisor +instance instead of by the host kernel TCP stack, and so it +inherits the more reordering-tolerant behaviour. +WireGuard-the-kernel-module shares only the cryptographic +protocol; it does not get the gVisor stack, and +therefore does +not get the advantage. + +It is a clean story. The natural way to test it +is to extract +the parameters Tailscale sets inside gVisor, apply their +nearest Linux equivalents to the bare-metal host as sysctls, +and see whether Internal — with no VPN at all — picks up the +same advantage. If it does, the gVisor explanation is +supported. If it does not, the hypothesis fails. + +\subsection{Reproducing the effect on bare metal} +\label{sec:tuned} + +We ran two follow-up benchmarks on the same hardware and +impairment setup as the original 18.12.2025 run. \begin{itemize} - \bitem{\texttt{tcp\_reordering}:} gVisor uses 10; the Linux kernel - defaults to~3. This parameter controls how many out-of-order - packets TCP tolerates before treating the event as a loss. With - tc~netem injecting 0.5--2.5\% reordering per machine, bursts of - 3+ reordered packets are frequent. The kernel's threshold of~3 - causes spurious fast retransmits and congestion window reductions - for packets that are merely reordered, not lost. - \bitem{\texttt{tcp\_recovery} (RACK):} gVisor disables it; the - Linux kernel enables it by default. RACK uses timing-based loss - detection that is more aggressive than the pure sequence-based - approach gVisor uses. Under reordering, RACK's timing heuristics - can falsely classify delayed packets as lost. - \bitem{\texttt{tcp\_early\_retrans} (TLP):} gVisor disables it; the - kernel enables it. Tail Loss Probe sends speculative retransmits - on idle connections, which can worsen congestion when the link is - already impaired. -\end{itemize} - -Under packet reordering, these three defaults compound. The Linux -TCP stack fires retransmits and cuts the congestion window far more -often than necessary; each false positive shrinks the window and -reduces throughput. Tailscale's gVisor stack tolerates more -reordering before reacting, so its congestion window stays larger and -throughput stays higher. - -% TODO: The claim that the anomaly "grows with impairment severity" is -% not fully supported. At High impairment, Headscale (4.21 Mbps) and -% Internal (4.25 Mbps) converge --- the anomaly vanishes rather than -% growing. The logic predicts continued divergence at High reordering -% (5% per machine), but the data shows both become loss-limited. -% Rephrase to say the anomaly emerges at Medium but disappears at High -% when absolute loss dominates. -This explains why the anomaly emerges as impairment increases. At -baseline, there is no reordering, so the threshold difference is -irrelevant and Internal's kernel-level processing advantage dominates. -As reordering increases from 0.5\% (Low) to 2.5\% (Medium) per -machine, the kernel's aggressive loss detection fires more often, and -the throughput gap shifts in Headscale's favor. At High impairment, -however, both converge to ${\sim}$4.2\,Mbps: the absolute packet loss -rate becomes the dominant bottleneck, overriding the reordering -tolerance advantage. - -\subsection{Tuned Kernel Parameters} - -Two follow-up benchmark runs applied Tailscale's gVisor TCP -parameters to the host kernel via sysctl: - -\begin{itemize} - \bitem{Full gVisor (27.02.2026):} All parameters --- + \bitem{Tailscale-style (27.02.2026):} \texttt{tcp\_reordering=10}, \texttt{tcp\_recovery=0}, - \texttt{tcp\_early\_retrans=0}, plus enlarged buffer sizes - (\texttt{tcp\_rmem}, \texttt{tcp\_wmem}, \texttt{rmem\_max}, - \texttt{wmem\_max}). Tested on Internal, Headscale, WireGuard, - Tinc, and ZeroTier. - \bitem{Reorder-only (06.03.2026):} Only - \texttt{tcp\_reordering=10}, \texttt{tcp\_recovery=0}, and - \texttt{tcp\_early\_retrans=0}. Buffer sizes left at kernel - defaults. Tested on Internal and Headscale only. + \texttt{tcp\_early\_retrans=0}, plus enlarged + buffer sizes + (\texttt{tcp\_rmem}, \texttt{tcp\_wmem}, + \texttt{rmem\_max}, + \texttt{wmem\_max}). Tested on Internal, Headscale, + WireGuard, Tinc, and ZeroTier. + \bitem{Reorder-only (06.03.2026):} Only + \texttt{tcp\_reordering=10}, + \texttt{tcp\_recovery=0}, and + \texttt{tcp\_early\_retrans=0}. Buffer sizes left at + kernel defaults. Tested on Internal and Headscale only. \end{itemize} -Table~\ref{tab:kernel_tuning_internal} shows how Internal responds -to the tuning. Both follow-up runs used the same impairment profiles -and hardware as the original 18.12.2025 run. - \begin{table}[H] \centering \caption{Internal (no VPN) throughput across three kernel - configurations. ``Default'' is the 18.12.2025 run with stock + configurations. ``Default'' is the + 18.12.2025 run with stock Linux TCP parameters.} \label{tab:kernel_tuning_internal} \begin{tabular}{llrrr} \hline \textbf{Metric} & \textbf{Profile} & \textbf{Default} & - \textbf{Full gVisor} & \textbf{Reorder-only} \\ + \textbf{Tailscale-style} & \textbf{Reorder-only} \\ \hline - Single TCP (Mbps) & Baseline & 934 & 934 & 934 \\ - Single TCP (Mbps) & Low & 333 & 363 & 354 \\ - Single TCP (Mbps) & Medium & 29.6 & 64.2 & 72.7 \\ - Parallel TCP (Mbps) & Low & 277 & 893 & 902 \\ - Parallel TCP (Mbps) & Medium & 82.6 & 226 & 211 \\ - Retransmit \% & Medium & ${\sim}$2.4 & 1.21 & 1.11 \\ - Nix cache (s) & Medium & 58.6 & 29.7 & 29.1 \\ + Single TCP (Mbps) & Baseline & 934 & + 934 & 934 \\ + Single TCP (Mbps) & Low & 333 & + 363 & 354 \\ + Single TCP (Mbps) & Medium & 29.6 & + 64.2 & 72.7 \\ + Parallel TCP (Mbps) & Low & 277 & + 893 & 902 \\ + Parallel TCP (Mbps) & Medium & 82.6 & + 226 & 211 \\ + Retransmit \% & Medium & ${\sim}$2.4 + & 1.21 & 1.11 \\ + Nix cache (s) & Medium & 58.6 & + 29.7 & 29.1 \\ \hline \end{tabular} \end{table} - \begin{figure}[H] \centering \includegraphics[width=\textwidth]{Figures/impairment/no_vpn_kernel_tuning_comparison.png} - \caption{Internal (no VPN) single-stream TCP throughput across three + \caption{Internal (no VPN) single-stream TCP + throughput across three kernel configurations. Baseline is unchanged; at Medium - impairment, throughput jumps from 30 to 64 to 73\,Mbps as + impairment, throughput jumps from 30 to 64 to + 73\,Mbps as reordering tolerance increases.} \label{fig:kernel_tuning_comparison} \end{figure} -Internal's Medium-impairment throughput jumps from 29.6 to -72.7\,Mbps --- a 146\% increase from a three-line sysctl change. The -retransmit percentage drops from ${\sim}$2.4\% to 1.11\%; over half -of the original retransmissions were spurious. The Nix cache download at -Medium halves from 58.6\,s to 29.1\,s. +The result felt like confirmation. Internal's +Medium-impairment throughput jumped from 29.6\,Mbps to +72.7\,Mbps under the reorder-only configuration — a 146\,\% +increase from a three-line sysctl change — and +the retransmit +rate at Medium dropped from ${\sim}$2.4\,\% to +1.11\,\%, which +means more than half of the original retransmissions were +spurious. The Nix cache download at Medium roughly halved, +from 58.6\,s to 29.1\,s. -Parallel TCP sees an even larger gain. Internal at Low impairment -climbs from 277 to 902\,Mbps, a 226\% increase that now exceeds -Headscale's original 718\,Mbps. % TODO: DOWNSTREAM DEPENDENCY — "six concurrent flows" inherits the -% unresolved 6-vs-10 stream count from the baseline parallel test +Parallel TCP gained more. Internal at Low +climbed from 277 to +902\,Mbps, a 226\,\% increase that not only +exceeds Internal's +old single-stream best but actually overtakes Headscale's +original 718\,Mbps from the unmodified run. % +% TODO: DOWNSTREAM +% DEPENDENCY — "six concurrent flows" inherits +% the unresolved +% 6-vs-10 stream count from the baseline parallel test % description. Update when that TODO is resolved. -With six concurrent flows each -independently benefiting from the higher reordering threshold, the -aggregate improvement compounds. +Each of the six concurrent flows benefits independently from +the higher reordering threshold, and the gains compound. -% TODO: Headscale's tuned-run values (50.1 Mbps, 36.3 s) are not in -% any table. Add a table showing Headscale's results from the -% follow-up runs alongside Internal's so readers can verify the -% reversal. -% TODO: "At every impairment level and benchmark" is a strong claim -% but only single-stream TCP at Medium and Nix cache at Medium are -% shown with both Internal and Headscale values. The Headscale tuned -% data is not in any table (see TODO above). Either add the full -% comparison table or weaken to "at the metrics shown." -The anomaly reverses. At the measured impairment levels and benchmarks, -tuned Internal now meets or exceeds Headscale. At Medium impairment: -Internal 72.7\,Mbps vs.\ Headscale 50.1\,Mbps (Internal 45\% ahead), -where the original result had Headscale 40\% ahead. The Nix cache -flips too: Internal completes in 29.1\,s vs.\ Headscale's 36.3\,s, -where the original had Headscale 17\% faster. +% TODO: Headscale's tuned-run values (50.1 Mbps, 36.3 s) are +% not in any table. Add a table showing Headscale's results +% from the follow-up runs alongside Internal's so +% readers can +% verify the reversal. +Headscale itself, retested with the same sysctls, +gained more +modestly: +21\,\% at Medium and a small $-$5\,\% wobble at +Low. And the anomaly reversed entirely. At Medium, tuned +Internal reached 72.7\,Mbps against Headscale's 50.1\,Mbps — +a 45\,\% lead for Internal where the original run +had Headscale +40\,\% ahead. The Nix cache flipped the same way: Internal +completed in 29.1\,s against Headscale's 36.3\,s, where the +original had Headscale 17\,\% faster. \begin{figure}[H] \centering \includegraphics[width=\textwidth]{Figures/impairment/headscale-gap-reversal.png} - \caption{Internal-to-Headscale speed-up factor before and after - kernel tuning. Values above 1.0 mean Internal is faster. At - Medium impairment, the ratio flips from 0.71$\times$ (Headscale + \caption{Internal-to-Headscale speed-up factor + before and after + kernel tuning. Values above 1.0 mean + Internal is faster. At + Medium impairment, the ratio flips from + 0.71$\times$ (Headscale ahead) to 1.45$\times$ (Internal ahead).} \label{fig:headscale_gap_reversal} \end{figure} -The reorder-only configuration (06.03) matches or exceeds the full -gVisor configuration (27.02) at most metrics; the two exceptions are -single-stream TCP at Low (354 vs.\ 363\,Mbps) and parallel TCP at -Medium (211 vs.\ 226\,Mbps), both within 7\%. Internal -reaches 72.7\,Mbps at Medium with reorder-only vs.\ 64.2\,Mbps with -full gVisor. % TODO: The "mild buffer bloat" explanation for full-gVisor being -% slightly slower than reorder-only is speculative. The difference -% (64.2 vs 72.7 Mbps) could be within run-to-run variance. Either -% test with more runs or present this as one possible explanation. -The enlarged buffer sizes appear unnecessary and may -introduce mild buffer bloat that partially offsets the reordering -benefit, though the difference could also reflect normal run-to-run -variance. The entire Headscale advantage is explained by three kernel -parameters: \texttt{tcp\_reordering}, \texttt{tcp\_recovery}, and +The reorder-only configuration matched or exceeded the full +Tailscale-style configuration on most metrics. The two +exceptions were single-stream TCP at Low (354 +vs.\ 363\,Mbps) +and parallel TCP at Medium (211 vs.\ 226\,Mbps), both within +7\,\%. The enlarged buffer sizes did not help and may have +added mild buffer bloat that partially offset the reordering +benefit, though the gap could also be run-to-run variance. +Either way, the entire Headscale advantage on Internal +collapsed to three host-kernel sysctls: +\texttt{tcp\_reordering}, \texttt{tcp\_recovery}, and \texttt{tcp\_early\_retrans}. +At this point in the investigation the hypothesis seemed +settled. Tailscale's gVisor stack ships with +these overrides; +the bare-metal kernel ships with stricter defaults; matching +the kernel to gVisor reproduces the effect. Then we checked +which Tailscale code path the test rig was actually running. + +\subsection{The data path that was not there} + +In default mode — what anyone running \texttt{tailscale up} +on a Linux host gets — the Tailscale client creates a real +kernel TUN device, registers a route for the +Tailscale subnet +through it, and forwards inbound and outbound +packets through +that interface. An application like iPerf3 issues a +\texttt{connect} to the remote peer's Tailscale +IP. The host +kernel TCP stack handles the application TCP. The kernel +routes the resulting outbound packets to the TUN device. +\texttt{tailscaled} (with \texttt{wireguard-go} embedded) +reads them from the TUN, encrypts them, and sends them as +outer WireGuard UDP packets on the wire. The receiving side +reverses the process and writes the decrypted inner packets +back into its own TUN, where the host kernel TCP stack +delivers them to the iPerf3 server. + +In that path, gVisor netstack is never instantiated. The +netstack initialiser in +Listing~\ref{lst:tailscale_netstack_overrides} +only runs when +\texttt{tailscaled} is launched with +\texttt{--tun=userspace-networking}, a mode that has no +kernel TUN at all and is reachable only from processes +running inside \texttt{tailscaled} itself (Tailscale SSH, +Taildrop, the metric endpoint). External processes such as +iPerf3 cannot reach the Tailscale network in that mode. + +The test rig does not use that mode. +Listing~\ref{lst:nixos_tailscale} shows the relevant line of +the upstream NixOS \texttt{services.tailscale} module, which +assembles the daemon command line as +\texttt{tailscaled --tun +\$\{cfg.interfaceName\}~\dots}, with +no \texttt{userspace-networking} fall-back unless +the operator +explicitly sets \texttt{interfaceName = +"userspace-networking"}. +Listing~\ref{lst:rig_interface_name} shows what +the benchmark +suite's Headscale module sets the interface name to: +\texttt{ts-\$\{instanceName\}}, truncated to fifteen +characters. The two together resolve to +\texttt{tailscaled --tun ts-headscale} on every +test machine, +a real kernel TUN. gVisor netstack is unreachable from any +external benchmark traffic in this rig. + +\lstinputlisting[language=Nix,caption={The NixOS + \texttt{services.tailscale} module passes \texttt{--tun + \$\{interfaceName\}} as the daemon's TUN argument. There is + no \texttt{--tun=userspace-networking} fall-back unless the + user explicitly sets \texttt{interfaceName = "userspace-networking"}. +\textit{nixpkgs/nixos/modules/services/networking/tailscale.nix:158}},label={lst:nixos_tailscale}]{Listings/nixos_tailscale.nix} + +\lstinputlisting[language=Nix,caption={The + benchmark suite's + Headscale module sets \texttt{interfaceName} to a real kernel + TUN name (\texttt{ts-}, truncated to 15 characters). + Combined with Listing~\ref{lst:nixos_tailscale}, this means + \texttt{tailscaled} runs as \texttt{tailscaled --tun ts-headscale} + on every test machine. +\textit{vpn-benchmark-suite/clanModules/headscale/shared.nix:19,273--277}},label={lst:rig_interface_name}]{Listings/rig_interface_name.nix} + +The empirical fingerprint pins the same conclusion down without +source-code reading. Headscale itself gained +21\,\% at Medium +from the host-kernel sysctl tuning. If Headscale's iPerf3 +traffic were processed by gVisor netstack, host-kernel sysctls +would change nothing — they configure the host kernel TCP stack +and only the host kernel TCP stack. The fact that Headscale moves +measurably under those sysctls is direct evidence that +Headscale's application TCP runs on the host kernel stack, just +as Internal's does. + +The validation experiment was therefore validating something +other than the hypothesis it was supposed to validate. It was +confirming, very cleanly, that the Linux kernel's default +\texttt{tcp\_reordering=3} is too tight for the kind of bursty, +correlated reordering the Medium profile produces, and that +loosening it produces a large throughput gain on a kernel-TCP +data path. That part of the result stands. What does not stand +is the inference that the gain reproduces something Tailscale was +already doing in gVisor. For this benchmark, Tailscale is not in +the gVisor TCP business at all. + +\subsection{Where the advantage actually lives} + +The puzzle the investigation began with has not gone away. +Headscale starts at 41.5\,Mbps where Internal starts at +29.6\,Mbps, and both run their iPerf3 TCP on the same host kernel +TCP stack. Whatever Headscale is doing — partially, weakly, but +reproducibly — is worth roughly twelve megabits per second on the +Medium profile, and it is not gVisor netstack. + +The +21\,\% sysctl gain for Headscale itself is also informative +about the size of the mechanism. If the gain were 0\,\%, +Headscale would already be doing the sysctls' work; if it were ++146\,\% like Internal's, Headscale would be doing nothing of its +own. The partial response says Headscale's mechanism produces an +effect similar in kind to the sysctls but smaller in size, and +that the two effects are not fully additive. + +Two features of the \texttt{wireguard-go} data-plane pipeline are +the most likely candidates, and both live on the kernel-TUN path +that Tailscale actually uses in the rig. + +The first is TUN TCP and UDP generic receive offload. Tailscale's +\texttt{tstun} wrapper enables both on the kernel TUN device on +Linux unless an environment knob disables them or a runtime probe +rejects the feature (Listing~\ref{lst:tstun_gro}). On the +receive side, this means \texttt{wireguard-go} decrypts a burst +of inbound WireGuard frames and then coalesces consecutive +in-order TCP segments belonging to the same flow into a single +super-segment before writing them back to the kernel TUN. On the +transmit side, it accepts GSO super-segments from the kernel TUN +read in the same way. The receiving kernel TCP stack therefore +sees fewer, larger segments per coalesced batch instead of $N$ +small ones, and the segment timing that survives to the kernel is +the timing of GRO batches rather than of individual on-the-wire +packets. Bare-metal Internal traffic has no equivalent path +because it does not pass through any user-space TUN at all. + +\lstinputlisting[language=Go,caption={Tailscale enables TUN TCP + and UDP GRO on every Linux non-TAP \texttt{tailscaled} process + unless the operator disables them via environment knobs or a + kernel runtime probe rejects the feature. This is in the default + kernel-TUN data path; it is not gated on + \texttt{--tun=userspace-networking}. +\textit{tailscale/net/tstun/wrap\_linux.go:25--43}},label={lst:tstun_gro}]{Listings/tstun_gro.go} + +The second is the 7\,MiB outer-UDP socket buffer that +\texttt{magicsock} pins on the WireGuard UDP socket +(Listing~\ref{lst:magicsock_buffer}), using the ``force'' +\texttt{SO\_*BUFFORCE} variant where available so the value is +honoured even past \texttt{net.core.rmem\_max}. The host kernel +default is in the low hundreds of KiB. Under burst-correlated +impairment — Medium and High both use 50\,\% correlation, so +losses and reorderings cluster — this larger buffer absorbs +spikes in arrival rate that would otherwise overflow the kernel +UDP receive queue and surface as additional inner-TCP losses. +Internal has no such cushion on its incoming wire path. + +\lstinputlisting[language=Go,caption={\texttt{magicsock} pins the + outer WireGuard UDP socket's send and receive buffers to 7\,MiB + and uses \texttt{SetBufferSize} with the \texttt{SO\_*BUFFORCE} + (``force'') variant where available, so the value is honoured + even past \texttt{net.core.rmem\_max}. +\textit{tailscale/wgengine/magicsock/magicsock.go:86,3908--3913}},label={lst:magicsock_buffer}]{Listings/magicsock_buffer.go} + +% TODO: Neither of the two candidate mechanisms above is directly +% verified in this chapter. A targeted follow-up — for example +% tcpdump on the receiving \texttt{tailscale0} interface during a +% Medium-impairment iPerf3 run, with inter-arrival timing +% analysis — would distinguish their relative contributions and +% confirm the mechanism. The argument here is that they are the +% most plausible candidates consistent with the evidence, not +% measured causes. + +A third feature, batched UDP I/O, completes the picture without +changing it qualitatively. \texttt{wireguard-go} uses +\texttt{recvmmsg} and \texttt{sendmmsg} on the outer UDP socket +so a burst of WireGuard frames moves through a single system +call. This does not change \emph{whether} packets are reordered, +but it reduces per-packet timing jitter that the kernel might +otherwise interpret as additional reordering. + +Hyprspace cannot be used as a negative control for any of this. +It does import gVisor netstack, but only for its in-VPN +service-network feature, and the Hyprspace benchmark traffic goes +through a kernel TUN exactly like Headscale's +(Section~\ref{sec:hyprspace_bloat}). The two VPNs differ on the +wireguard-go pipeline (TUN GRO and the 7\,MiB outer-UDP buffer), +not on whether gVisor handles their inner TCP. The gVisor angle +simply does not apply to either of them in this benchmark. + +The kernel-side picture closes the loop. Three host-kernel TCP +parameters dominate the bare-metal behaviour the benchmarks +expose. \texttt{net.ipv4.tcp\_reordering} (default 3) is the +number of out-of-order segments the kernel will tolerate before +declaring fast retransmit, and with \texttt{tc netem} injecting +0.5--2.5\,\% reordering per machine, bursts of several reordered +packets are frequent enough that the threshold is repeatedly +tripped on the bare-metal path. \texttt{net.ipv4.tcp\_recovery} +(default \texttt{1}, RACK enabled) adds time-based reordering +detection on top of the segment-count threshold, which compounds +the spurious retransmits when reordering is high. And +\texttt{net.ipv4.tcp\_early\_retrans} (default \texttt{3}, Tail +Loss Probe enabled) fires speculative retransmits when +unacknowledged segments sit at the tail of a transmission window, +which interacts poorly with an already-impaired link. Loosening +any one of the three softens the kernel's loss detection on the +bare-metal path; loosening all three recovers most of the +throughput. The Headscale path reaches the same kernel TCP stack +but is already feeding it the GRO-coalesced, buffer-cushioned +stream described above, so the kernel's tight defaults fire less +often there to begin with. + +The same logic explains the anomaly's shape across profiles. At +baseline there is no reordering, so the kernel's tight +\texttt{tcp\_reordering} threshold never trips and Internal's +native kernel-stack speed wins. As reordering rises from 0.5\,\% +(Low) to 2.5\,\% (Medium) per machine, the kernel's loss +detection fires on the bare-metal path more often than on the +GRO-coalesced Headscale path, and the throughput gap shifts in +Headscale's favour. At High impairment, both converge to +${\sim}$4.2\,Mbps: absolute packet loss becomes the dominant +bottleneck, and reordering tolerance no longer matters. + % TODO: WireGuard (12.2 Mbps), Tinc (11.5 Mbps), and ZeroTier % (11.5 Mbps) tuned values are not in any table. Add them to % Table~\ref{tab:kernel_tuning_internal} or a new table. -Other VPNs benefit less from the kernel tuning. WireGuard's Medium -throughput rises from 8.77 to 12.2\,Mbps (+39\%) and Tinc's from -5.53 to 11.5\,Mbps (+108\%). ZeroTier stays flat (12.0 to -11.5\,Mbps). The tuning helps the kernel TCP stack, but VPNs that -add their own encapsulation overhead and userspace processing have -independent bottlenecks that the sysctl parameters cannot remove. +Other VPNs respond unevenly to the same sysctl tuning. +WireGuard's Medium throughput rises from 8.77 to 12.2\,Mbps +(+39\,\%), Tinc's from 5.53 to 11.5\,Mbps (+108\,\%), and +ZeroTier stays flat (12.0 to 11.5\,Mbps). % TODO: The +% reading below — that VPNs which add their own encapsulation and +% userspace processing have bottlenecks the host kernel sysctls +% cannot touch — does not cleanly fit the data: Tinc (a fully +% userspace VPN) shows the largest gain (+108\,\%), larger than +% kernel-WireGuard's. A more complete explanation has to account +% for which TCP stack each VPN's application traffic actually +% traverses and which of those stacks the sysctls actually reach. +The intuitive reading is that VPNs which add their own +encapsulation and userspace processing have bottlenecks the host +kernel sysctls cannot touch, but Tinc's large gain shows the +picture is not that simple. -% TODO: Headscale tuned-run percentages (+21\%, $-$5\%) are not in -% any table. Also, the "compound delays" hypothesis is speculative -% --- no evidence is shown that double reordering tolerance causes -% compound delays. Either verify experimentally or weaken the claim. -Headscale itself gets modestly faster with kernel tuning (+21\% at -Medium) but slightly slower at Low impairment ($-$5\%). Its -userspace gVisor stack already optimizes for reordering tolerance. -When the kernel stack also increases its tolerance, the two layers of -tuning may interact suboptimally --- both independently delay -retransmits, which could cause compound delays on the -kernel-to-Headscale socket path. +The resilient finding from this section, the one that survives +regardless of which of the two Tailscale-side mechanisms turns +out to dominate, is not about Tailscale at all. It is about +Linux. The kernel's default \texttt{tcp\_reordering=3} threshold +is too tight for the kind of bursty, correlated reordering +\texttt{tc netem} produces at the Medium profile, and it costs +the bare-metal host more than half of its achievable throughput. +Three lines of \texttt{sysctl} repair it. The fix is portable to +any Linux host and entirely independent of any VPN. -% TODO: These sections are empty stubs but the chapter introduction -% (line 12--13) promises "findings from the source code analysis." -% Either write these sections or remove the promise from the intro. +The unresilient finding — the one that motivated us to write this +section in the first place — is that Tailscale's much-discussed +userspace TCP stack is, for the workload that exposed the +anomaly, sitting on the bench. The advantage we attributed to it +must come from a more ordinary place: the way +\texttt{wireguard-go} batches and coalesces packets between the +wire and the kernel TCP stack, and the larger UDP buffer it pins +on its outer socket. We were chasing the wrong hypothesis with +the right experiment, and the experiment turned out to be more +useful than the hypothesis. -\section{Source Code Analysis} +% TODO: These sections are empty stubs but the chapter +% introduction (line 12--13) promises "findings from the source +% code analysis." Either write these sections or remove the +% promise from the intro. -\subsection{Feature Matrix Overview} +\section{Source code analysis} -% Summary of the 131-feature matrix across all ten VPNs. +\subsection{Feature matrix overview} + +% Summary of the 108-feature matrix across all ten VPNs. % Highlight key architectural differences that explain % performance results. -\subsection{Security Vulnerabilities} +\subsection{Security vulnerabilities} % Vulnerabilities discovered during source code review. -\section{Summary of Findings} +\section{Summary of findings} -% Brief summary table or ranking of VPNs by key metrics. -% Save deeper interpretation for a Discussion chapter. +% Brief summary table or ranking of VPNs by key metrics. Save +% deeper interpretation for a Discussion chapter. diff --git a/Figures/baseline/tcp/TCP Throughput.png b/Figures/baseline/tcp/TCP Throughput.png index 49dce1cc8bc4fdd652304223c88e6e9a6c701ca1..a6bf25f88e16254d0a87cc9404dbc8f0166f0296 100644 GIT binary patch literal 74837 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 literal 49912 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 TlA6N6z`)??>gTe~DWM4f`iOB= diff --git a/Listings/hyprspace_dispatch.go b/Listings/hyprspace_dispatch.go new file mode 100644 index 0000000..4231639 --- /dev/null +++ b/Listings/hyprspace_dispatch.go @@ -0,0 +1,26 @@ +} else if proto == 0x60 { + dstIP = net.IP(packet[24:40]) + if node.cfg.BuiltinAddr6.Equal(dstIP) { + continue + } else if serviceNet.NetworkRange.Contains(dstIP) { + // Are you TCP because your protocol is 6, or is your + // protocol 6 because you are TCP? + if packet[6] == 0x06 { + port := uint16(packet[42])*256 + uint16(packet[43]) + if serviceNet.EnsureListener([16]byte(packet[24:40]), port) { + count, err := (*serviceNet.Tun).Write([][]byte{packet}, 0) + if count == 0 || err != nil { + logger.With(err).Error("Error writing to service-network tunnel") + } + } + } + continue + } +} +... +// Check route table for destination address. +route, found := node.cfg.FindRouteForIP(dstIP) +if found { + dst = route.Target.ID + go node.sendPacket(dst, packet, plen) +} diff --git a/Listings/hyprspace_netstack.go b/Listings/hyprspace_netstack.go new file mode 100644 index 0000000..8bf1676 --- /dev/null +++ b/Listings/hyprspace_netstack.go @@ -0,0 +1,21 @@ +// taken from https://git.zx2c4.com/wireguard-go/tree/tun/netstack/tun.go +// rev 2b73054b299aec80cbb064954001810d30ee2e3c +... +func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { + opts := stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, + HandleLocal: true, + } + dev := &netTun{ + ep: channel.New(1024, uint32(mtu), ""), + stack: stack.New(opts), + ... + } + sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default + tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) + if tcpipErr != nil { + return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) + } + ... +} diff --git a/Listings/hyprspace_sendpacket.go b/Listings/hyprspace_sendpacket.go new file mode 100644 index 0000000..540f1a3 --- /dev/null +++ b/Listings/hyprspace_sendpacket.go @@ -0,0 +1,31 @@ +type SharedStream struct { + Stream *network.Stream + Lock *sync.Mutex +} +... +// Inside the TUN-read loop: +if found { + dst = route.Target.ID + go node.sendPacket(dst, packet, plen) +} +... +func (node *Node) sendPacket(dst peer.ID, packet []byte, plen int) { + // Check if we already have an open connection to the destination peer. + ms, ok := node.activeStreams[dst] + if ok { + if func() bool { + ms.Lock.Lock() + defer ms.Lock.Unlock() + // Write out the packet's length to the libp2p stream to ensure + // we know the full size of the packet at the other end. + err := binary.Write(*ms.Stream, binary.LittleEndian, uint16(plen)) + if err == nil { + // Write the packet out to the libp2p stream. + _, err = (*ms.Stream).Write(packet[:plen]) + ... + } + ... + }() { return } + } + ... +} diff --git a/Listings/hyprspace_tun_linux.go b/Listings/hyprspace_tun_linux.go new file mode 100644 index 0000000..9a889bc --- /dev/null +++ b/Listings/hyprspace_tun_linux.go @@ -0,0 +1,15 @@ +// New creates and returns a new TUN interface for the application. +func New(name string, opts ...Option) (*TUN, error) { + // Setup TUN Config + cfg := water.Config{ + DeviceType: water.TUN, + } + cfg.Name = name + + // Create Water Interface + iface, err := water.New(cfg) + if err != nil { + return nil, err + } + ... +} diff --git a/Listings/magicsock_buffer.go b/Listings/magicsock_buffer.go new file mode 100644 index 0000000..6cec3cd --- /dev/null +++ b/Listings/magicsock_buffer.go @@ -0,0 +1,9 @@ +socketBufferSize = 7 << 20 +... +forceErr, portableErr := sockopts.SetBufferSize(pconn, direction, socketBufferSize) +if forceErr != nil { + logf("magicsock: [warning] failed to force-set UDP %v buffer size to %d: %v; using kernel default values (impacts throughput only)", direction, socketBufferSize, forceErr) +} +if portableErr != nil { + logf("magicsock: failed to set UDP %v buffer size to %d: %v", direction, socketBufferSize, portableErr) +} diff --git a/Listings/nixos_tailscale.nix b/Listings/nixos_tailscale.nix new file mode 100644 index 0000000..bda9f2d --- /dev/null +++ b/Listings/nixos_tailscale.nix @@ -0,0 +1 @@ +''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"'' diff --git a/Listings/rig_interface_name.nix b/Listings/rig_interface_name.nix new file mode 100644 index 0000000..9912e9c --- /dev/null +++ b/Listings/rig_interface_name.nix @@ -0,0 +1,10 @@ +let + interface = lib.substring 0 15 "ts-${instanceName}"; +in +{ + services.tailscale = { + enable = true; + # Use the interface name for the tunnel + interfaceName = interface; + }; +} diff --git a/Listings/tailscale_netstack_overrides.go b/Listings/tailscale_netstack_overrides.go new file mode 100644 index 0000000..ad474ba --- /dev/null +++ b/Listings/tailscale_netstack_overrides.go @@ -0,0 +1,28 @@ +// values are biased towards higher throughput on high bandwidth-delay +// product paths, except on memory-constrained platforms. +tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ + ... + Max: tcpRXBufMaxSize, +} +tcpipErr := ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) +... +tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ + ... + Max: tcpTXBufMaxSize, +} +tcpipErr = ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) +... +sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default +tcpipErr := ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) +... +// See https://github.com/tailscale/tailscale/issues/9707 +// gVisor's RACK performs poorly. ACKs do not appear to be handled in a +// timely manner, leading to spurious retransmissions and a reduced +// congestion window. +tcpRecoveryOpt := tcpip.TCPRecovery(0) +tcpipErr = ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRecoveryOpt) +... +// gVisor defaults to reno at the time of writing. We explicitly set reno +// See https://github.com/google/gvisor/issues/11632 +renoOpt := tcpip.CongestionControlOption("reno") +tcpipErr = ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &renoOpt) diff --git a/Listings/tstun_gro.go b/Listings/tstun_gro.go new file mode 100644 index 0000000..3516af9 --- /dev/null +++ b/Listings/tstun_gro.go @@ -0,0 +1,22 @@ +// SetLinkFeaturesPostUp configures link features on t based on select TS_TUN_ +// environment variables and OS feature tests. Callers should ensure t is +// up prior to calling, otherwise OS feature tests may be inconclusive. +func (t *Wrapper) SetLinkFeaturesPostUp() { + if t.isTAP || runtime.GOOS == "android" { + return + } + if groDev, ok := t.tdev.(tun.GRODevice); ok { + if envknob.Bool("TS_TUN_DISABLE_UDP_GRO") { + groDev.DisableUDPGRO() + } + if envknob.Bool("TS_TUN_DISABLE_TCP_GRO") { + groDev.DisableTCPGRO() + } + err := probeTCPGRO(groDev) + if errors.Is(err, unix.EINVAL) { + groDev.DisableTCPGRO() + groDev.DisableUDPGRO() + t.logf("disabled TUN TCP & UDP GRO due to GRO probe error: %v", err) + } + } +} diff --git a/_typos.toml b/_typos.toml index 1c9e7b8..76bade6 100644 --- a/_typos.toml +++ b/_typos.toml @@ -4,7 +4,7 @@ extend-exclude = [ "**/value", "**.rev", "**/facter-report.nix", - "Chapters/Zusammenfassung.tex", + "**/Zusammenfassung.tex", "**/key.json", "pkgs/clan-cli/clan_lib/machines/test_suggestions.py", ] diff --git a/main.tex b/main.tex index 24edf20..836c9fd 100644 --- a/main.tex +++ b/main.tex @@ -62,6 +62,55 @@ \usepackage{tikz} \usetikzlibrary{shapes.geometric} \usepackage[edges]{forest} +\usepackage{listings} % Source code listings for evidence snippets +% Syntax-highlighting colors (xcolor is already loaded by the class file) +\definecolor{lstKeyword}{HTML}{0B5FA5} +\definecolor{lstComment}{HTML}{4B7B4D} +\definecolor{lstString}{HTML}{A31515} +\definecolor{lstNumber}{HTML}{707070} +\definecolor{lstBackground}{HTML}{F7F7F7} +\definecolor{lstFrame}{HTML}{C8C8C8} +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{lstKeyword}\bfseries, + commentstyle=\color{lstComment}\itshape, + stringstyle=\color{lstString}, + numberstyle=\tiny\color{lstNumber}, + identifierstyle=\color{black}, + backgroundcolor=\color{lstBackground}, + rulecolor=\color{lstFrame}, + breaklines=true, + breakatwhitespace=false, + columns=fullflexible, + keepspaces=true, + showstringspaces=false, + frame=single, + framerule=0.4pt, + xleftmargin=0.5em, + xrightmargin=0.5em, + aboveskip=0.6em, + belowskip=0.6em, + captionpos=b, +} +\lstdefinelanguage{Nix}{ + morekeywords={with,let,in,inherit,rec,if,then,else,import,true,false,null}, + morecomment=[l]{\#}, + morestring=[b]", + sensitive=true, +} +\lstdefinelanguage{Go}{ + morekeywords={break,case,chan,const,continue,default,defer,else,fallthrough, + for,func,go,goto,if,import,interface,map,package,range,return,select, + struct,switch,type,var,bool,byte,complex64,complex128,error,float32, + float64,int,int8,int16,int32,int64,rune,string,uint,uint8,uint16,uint32, + uint64,uintptr,true,false,iota,nil,append,cap,close,complex,copy,delete, + imag,len,make,new,panic,print,println,real,recover}, + morecomment=[l]{//}, + morecomment=[s]{/*}{*/}, + morestring=[b]", + morestring=[b]`, + sensitive=true, +} \usepackage[backend=bibtex,style=numeric,natbib=true]{biblatex} % % Use the bibtex backend with the authoryear citation style (which diff --git a/treefmt.nix b/treefmt.nix index 1e5a60c..2871f75 100644 --- a/treefmt.nix +++ b/treefmt.nix @@ -18,6 +18,7 @@ settings.global.excludes = [ "AI_Data/**" "Figures/**" + "Chapters/Zusammenfassung.tex" ]; programs.typos = {