The tunnel's real bottleneck queue - the UDP receive buffer feeding the
decrypt loop - is invisible to every kernel AQM, so under overload it
regulates ECN-capable flows with tail-drop loss like it's 1993. Sample
SK_MEMINFO once per recvmmsg batch (tunnels.ecn_mark_threshold, fraction
of rcvbuf, 0=off) and treat depth beyond the threshold as an outer CE:
the existing RFC 6040 fold then CE-marks ECT inner packets and senders
back off without loss.
The return lived inside a log-level-gated else-if, so with debug
logging off, control fell out of both switches. Nothing follows the
switch today, making it a silent drop by luck; any future code added
after the switch would have run for oversized Test requests only when
debug logging was disabled. Make the drop a guard clause.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ugV2edVqoz3tBvq9J6yWp
Multi-disciplinary correctness review of the batched tun / GSO-GRO / sendmmsg
rework. Each fix has a regression test; the merged tree builds on
linux/darwin/openbsd/windows/freebsd/netbsd, vets clean, passes the unit and
e2e suites, and is -race clean.
Critical:
- C1 zero-length inner UDP datagram no longer panics the process (remote DoS):
the UDP coalescer routes payLen==0 to passthrough instead of seeding a GSO
slot, and WriteGSO skips empty payload iovecs as defense in depth.
- C2 segmenter no longer corrupts inner headers when gsoSize < headerLen: the
L3+L4 header is snapshotted once and each segment stamped from the copy,
replacing the destructive overlapping in-place slide (SegmentTCP + SegmentUDP).
High:
- H1 applyOuterECN updates the IPv4 header checksum (RFC 1624 incremental) when
folding outer CE into the inner ToS, so passthrough packets are no longer
dropped by the peer stack.
- H2 the GRO reject path caps the borrowed RX segment ([:n:n]) so a reject can
no longer overrun into the next coalesced segment's Nebula header. Note:
oversized ICMPv6 rejects that need >16B beyond the segment are now refused
rather than sent under GRO (safe; see TOFIX.md for the scratch-buffer follow-up).
- H3 WriteBatch falls back to per-packet WriteTo for a chunk when writeSockaddr
fails, so one bad-family destination costs only its own packet, not the batch.
- H4 UserDevice.Readers returns N distinct queue wrappers with private buffers
(sharing the pipes) so concurrent readers no longer race/overwrite borrowed
packet bytes.
- H5 Poll.Close / Offload.Close no longer null t.fd (matching master's
tunFile.Close), removing the data race with a concurrent readOne load.
Medium/Low:
- M1 the UDP GSO 127-segment gate moved from kernel >=5.5 to >=6.9 (the real
UDP_MAX_SEGMENTS 64->128 threshold), avoiding EINVAL + per-packet fallback on
5.5-6.8 kernels.
- M2 NewMultiQueueReader replays the offload mask newTun actually negotiated
instead of the TSO-only mask, so adding a queue no longer disables USO
device-wide; the advertised USO capability derives from the same mask.
- M3 the shutdown eventfd is closed in pollQueueSet.Close / offloadQueueSet.Close
(double-close guarded), fixing the per-lifecycle fd leak.
- M4 dual-stack ECN selects the cmsg by address family, not socket family: RX
parseRecvCmsg reads both IP_TOS and IPV6_TCLASS; TX writeEntryCmsg stamps
IP_TOS for v4/v4-mapped dests and IPV6_TCLASS for v6 (on-host verified).
- L1 newPoll no longer closes the fd on failure (matching newOffload), removing
the double-close on QueueSet.Add error.
handleOutsideRelayPacket filled ViaSender.remoteIdx with relay.RemoteIndex,
an index from the relay peer's index space, but the rescue in
sendHandshakeResponse looks that value up in relayForByIdx, which is keyed
by local index. The lookup could never hit, so a terminal relay entry left
Disestablished by a one-sided teardown stayed Disestablished even after a
valid handshake arrived over it. The responder's first transmit then failed
to find an Established relay, deleted its only relay entry, and every
subsequent send was silently dropped until dead-tunnel detection forced a
re-handshake.
* refactor readOutsidePackets
They layout of this method is confusing and relys on certain parts to
return early for things to work correctly.
Change the ordering of the logic so that we do this:
- Handle unencrypted packets
- Decrypt packet
- Handle encrypted packets
This way, nothing can sneak through unencrypted to where it shouldn't
be.
* fix comment
* code review comments
* check for expected type/subtype
* check header version
* log header
* need to handle TestReply
* clean roaming / connectionManager
* dont need to roam here now, we do it earlier
* cleanup metrics and errors
* rxInvalid
* debug logger checks
* ErrOutOfWindow
* don't send recv errors for packets outside the connection window anymore
* Pull in fix from #1459, add my opinion on maxRecvError
* remove recv_error counter entirely
* firewall: add option to send REJECT replies
This change allows you to configure the firewall to send REJECT packets
when a packet is denied.
firewall:
# Action to take when a packet is not allowed by the firewall rules.
# Can be one of:
# `drop` (default): silently drop the packet.
# `reject`: send a reject reply.
# - For TCP, this will be a RST "Connection Reset" packet.
# - For other protocols, this will be an ICMP port unreachable packet.
outbound_action: drop
inbound_action: drop
These packets are only sent to established tunnels, and only on the
overlay network (currently IPv4 only).
$ ping -c1 192.168.100.3
PING 192.168.100.3 (192.168.100.3) 56(84) bytes of data.
From 192.168.100.3 icmp_seq=2 Destination Port Unreachable
--- 192.168.100.3 ping statistics ---
2 packets transmitted, 0 received, +1 errors, 100% packet loss, time 31ms
$ nc -nzv 192.168.100.3 22
(UNKNOWN) [192.168.100.3] 22 (?) : Connection refused
This change also modifies the smoke test to capture tcpdump pcaps from
both the inside and outside to inspect what is going on over the wire.
It also now does TCP and UDP packet tests using the Nmap version of
ncat.
* calculate seq and ack the same was as the kernel
The logic a bit confusing, so we copy it straight from how the kernel
does iptables `--reject-with tcp-reset`:
- https://github.com/torvalds/linux/blob/v5.19/net/ipv4/netfilter/nf_reject_ipv4.c#L193-L221
* cleanup