1017 Commits
Author SHA1 Message Date
Zoltan Papp 8bf8fa968f Keep timer paths non-blocking and bound staged packets per peer, kernel style (#21)
The keepalive deadlock fixed in #20 was one instance of a class the kernel
module rules out by construction. This commit adopts the same two rules
instead of working around the symptom.

1. Nothing reached from a timer callback may wait. In the kernel the
   callback runs in softirq: alloc_skb is GFP_ATOMIC and gives up, and
   wg_queue_enqueue_per_device_and_peer fails with -ENOSPC/-EPIPE and the
   packet is dropped or marked PACKET_STATE_DEAD. #20 made the keepalive
   allocation non-blocking but left the rest of the path waiting: a
   container allocation on the nonce-overflow branch and the two bounded
   queue sends in SendStagedPackets. Any of them, parked while holding
   the timer's runningLock, wedges Timer.DelSync and with it Peer.Stop,
   RemovePeer and Device.Close. SendKeepalive now runs
   sendStagedPackets(wait=false): a full pool or queue drops the batch;
   if the sequential sender already holds the batch it is emptied and
   unlocked so the sender passes over it. The public SendStagedPackets
   keeps waiting, so the TUN reader keeps its backpressure.

2. A peer holds at most MAX_STAGED_PACKETS (128) packets while waiting
   for a handshake; wg_xmit drops the oldest before adding a batch. Our
   staged queue was bounded in batches, not packets, and a GSO batch can
   carry 128 packets, so one peer that never completes its handshake
   could pin thousands of buffers and drain a capped pool for every other
   peer. StagePackets now enforces the packet bound with a per-peer
   counter, dropping the oldest batches first. That, not the handshake
   retry policy, is how the kernel keeps a dead peer from hurting the
   rest of the device.

With the bound in place the handshakeAttempts change from #20 is not
needed and is reverted: outbound traffic resets the counter again, as it
does upstream and in wg_packet_send_queued_handshake_initiation, and
SendHandshakeInitiation goes back to the upstream isRetry signature so
these lines no longer diverge on merges.

The revert also removes a problem that change introduced. It relied on
the give-up branch of expiredRetransmitHandshake to release a dead
peer's buffers, and that branch only runs after MaxTimerHandshakes
failed retries - about 90 seconds. Until then the pool stayed drained,
the TUN reader stayed parked and no peer on the device could send. And
once the branch had run, nothing reset the counter again while traffic
kept flowing: the next packet sent one initiation and re-armed the
retransmit timer, which found the counter already past the limit and
gave up after a single try, flushing the staged queue, deleting the
keepalive timer and logging "giving up" every five seconds for as long
as the peer was down and had traffic. Upstream and the kernel give each
burst of new traffic a full 90-second budget instead, and now so do we.

Tests: TestStagedSendKeepsHandshakeAttempts is replaced by
TestStagedSendResetsHandshakeAttempts; TestStagePacketsBoundedPerPeer
checks the packet bound and that dropped buffers return to the pool;
TestSendKeepaliveWithFullOutboundQueue wedges the sequential sender and
checks the keepalive still returns.
2026-09-14 14:31:47 +02:00
Riccardo Manfrin f6c41ca4d7 Fix keepalive pool block (#20)
* device: add non-blocking pool accessors

TryGet, and the Device-level wrappers around it, report failure instead of
waiting on the pool's condition variable. Nothing calls them yet: this is a
pure addition so the change of behavior in the keepalive path lands on its own.

* device: never block on the buffer pool while sending a keepalive

SendKeepalive took buffers with the blocking pool accessors. It runs inside a
keepalive timer callback, which holds the timer's runningLock, and Timer.DelSync
waits on that lock. So on a Device whose pool is capped and exhausted -
PreallocatedBuffersPerPool non-zero, plus a peer that stages packets while it
waits for a handshake that never completes - the callback parks in WaitPool.Get
and Peer.Stop can never finish. Peer removal, and therefore Device.Close, are
stuck for the lifetime of the process, and the buffers those operations would
have freed are the ones the callback is waiting for.

Use the non-blocking accessors and skip the keepalive when the pool is dry.

* device: name the handshake-initiation flag after what it does

isRetry only ever decided whether handshakeAttempts is cleared; it said nothing
about the packet or the timers. Rename it to resetAttempts and invert every
call site accordingly. Behavior is unchanged: each caller keeps the same effect
on the counter it had before.

* device: let a peer give up on a handshake it keeps failing

expiredRetransmitHandshake gives up after MaxTimerHandshakes attempts, flushes
the staged queue and stops the keepalive timer. That is the mechanism that frees
the buffers held for a peer which never completes a handshake, and it never ran:
SendStagedPackets asked for a counter reset, so every packet aimed at the down
peer put the attempt count back to zero and, with traffic still arriving, the
limit was never reached.

Stop resetting from the data path. The counter is still cleared by
timersHandshakeComplete, timersStart and the rekey paths, which are the events
that actually mean the peer is reachable.

* device: cover the capped-pool keepalive deadlock

Two regression tests, both failing before the previous two commits:
SendKeepalive on a device whose pool is drained never returns, and
SendStagedPackets resets handshakeAttempts so the give-up-and-flush path is
never reached. Plus unit coverage for TryGet at capacity and on an uncapped
pool.

* device: reproduce the deadlock on a live device pair

A functional test next to the unit ones: two devices with a completed
handshake, a third peer that can never handshake, real traffic and real timers.

The shape matters and the first attempt at it did not reproduce anything. A
peer whose staged queue is full never allocates for a keepalive - SendKeepalive
returns early - so the peer that drains the pool is not the peer that gets
stuck. The victim is one with a live session and an empty staged queue: its
keepalive does allocate, parks in the exhausted pool holding the timer's
runningLock, and removing that peer then waits on DelSync forever.

Without the keepalive fix this fails with "removing a peer never completed"
after the 10s guard; with it the removal returns and the test takes ~2.5s. The
cap is lifted in a cleanup so a failing run can still close the devices instead
of hanging the package.
2026-09-11 09:37:36 +02:00
Maycon Santos e5c60e1195 Make netTun.Close idempotent (#19)
Close closes its channels unguarded, so a second call panics the process with
close of closed channel rather than failing the one caller that raced.

Nothing above this layer can promise a single call. Device.Close closes the
device it was handed and is reached both from an ordinary teardown and from
RoutineReadFromTUN, while embedders hold their own references and cannot
coordinate. Guarding per instance above the tun, as netbird does with a
sync.Once in its device filter, cannot stop two wrappers closing one tun.

TestNetTunCloseTwice panics without this change. The concurrent test covers the
teardown against RoutineReadFromTUN race, and the third checks the guard still
closes the channels rather than skipping teardown.
2026-09-04 20:49:57 +02:00
Viktor Liu 8ec1ad3288 Disable gVisor TCP RACK loss detection on Windows (#18) 2026-07-17 09:12:48 +02:00
Zoltan Papp 2834bebf6c Send immediate handshake when endpoint address changes (#15)
* device: send immediate handshake when endpoint address changes

When a peer's endpoint is changed via UAPI to a new, non-nil address
(e.g. an external signal learned a now-reachable address) while a
handshake retry cycle is still mid-RekeyTimeout, force a handshake
initiation right away instead of waiting out the remaining ~5s.

* device: avoid throttle hack when initiating handshake on endpoint change

Extract the initiation send into sendHandshakeInitiation() and have the
endpoint-change path call it directly with a real lastSentHandshake
timestamp, instead of back-dating lastSentHandshake to bypass the
RekeyTimeout throttle.

* device: only initiate handshake on endpoint change when no usable session

Guard the endpoint-change handshake with needsHandshake() so an address
change on a live, working session does not trigger an unnecessary rekey.
2026-06-28 12:29:22 +02:00
Viktor Liu 4b4a4e3601 Make PreallocatedBuffersPerPool runtime-configurable (#14) 2026-05-23 17:53:12 +09:00
Viktor Liu 33b7c9d03d Sync upstream (#12) 2026-01-07 11:09:53 +01:00
Viktor Liu cc2acf4607 Add IPv6 support to receiver creator 2025-07-09 12:43:00 +02:00
Viktor Liu 3247b60668 Use existing interface 2025-07-09 12:18:33 +02:00
Viktor Liu 8c9a58056a Add IPv6 support to receiver creator 2025-07-09 11:50:34 +02:00
Viktor Liu 505e232447 Add CreateOutboundPacket method for direct packet injection (#8)
Add CreateOutboundPacket method for direct packet injection
2025-01-30 19:54:45 +01:00
Viktor Liu 6a676aebaa Add CreateOutboundPacket method for direct packet injection 2024-12-30 13:03:07 +01:00
Zoltan Papp f9cdce5e32 Merge pull request #7 from netbirdio/export-control
Export list of control functions
2024-11-25 16:01:34 +01:00
Zoltan Papp 9c57ff2702 Export list of control functions 2024-11-22 17:43:26 +01:00
Zoltan Papp 57d8513b5f Merge pull request #5 from netbirdio/export-offload
Export offload flag for custom receiver
2024-11-07 16:28:27 +01:00
Zoltán Papp b7cf0b868b Export offload flag for custom receiver 2024-10-30 18:13:35 +01:00
pascal-fischer 6c340dd55a Merge pull request #4 from netbirdio/upstream-sync
Add custom ReceiverCreator
2024-01-05 19:22:36 +01:00
Zoltan Papp a1815f7d31 Add custom ReceiverCreator 2024-01-05 17:38:36 +01:00
Martin Basovnik 12269c2761 device: fix possible deadlock in close method
There is a possible deadlock in `device.Close()` when you try to close
the device very soon after its start. The problem is that two different
methods acquire the same locks in different order:

1. device.Close()
 - device.ipcMutex.Lock()
 - device.state.Lock()

2. device.changeState(deviceState)
 - device.state.Lock()
 - device.ipcMutex.Lock()

Reproducer:

    func TestDevice_deadlock(t *testing.T) {
    	d := randDevice(t)
    	d.Close()
    }

Problem:

    $ go clean -testcache && go test -race -timeout 3s -run TestDevice_deadlock ./device | grep -A 10 sync.runtime_SemacquireMutex
    sync.runtime_SemacquireMutex(0xc000117d20?, 0x94?, 0x0?)
            /usr/local/opt/go/libexec/src/runtime/sema.go:77 +0x25
    sync.(*Mutex).lockSlow(0xc000130518)
            /usr/local/opt/go/libexec/src/sync/mutex.go:171 +0x213
    sync.(*Mutex).Lock(0xc000130518)
            /usr/local/opt/go/libexec/src/sync/mutex.go:90 +0x55
    golang.zx2c4.com/wireguard/device.(*Device).Close(0xc000130500)
            /Users/martin.basovnik/git/basovnik/wireguard-go/device/device.go:373 +0xb6
    golang.zx2c4.com/wireguard/device.TestDevice_deadlock(0x0?)
            /Users/martin.basovnik/git/basovnik/wireguard-go/device/device_test.go:480 +0x2c
    testing.tRunner(0xc00014c000, 0x131d7b0)
    --
    sync.runtime_SemacquireMutex(0xc000130564?, 0x60?, 0xc000130548?)
            /usr/local/opt/go/libexec/src/runtime/sema.go:77 +0x25
    sync.(*Mutex).lockSlow(0xc000130750)
            /usr/local/opt/go/libexec/src/sync/mutex.go:171 +0x213
    sync.(*Mutex).Lock(0xc000130750)
            /usr/local/opt/go/libexec/src/sync/mutex.go:90 +0x55
    sync.(*RWMutex).Lock(0xc000130750)
            /usr/local/opt/go/libexec/src/sync/rwmutex.go:147 +0x45
    golang.zx2c4.com/wireguard/device.(*Device).upLocked(0xc000130500)
            /Users/martin.basovnik/git/basovnik/wireguard-go/device/device.go:179 +0x72
    golang.zx2c4.com/wireguard/device.(*Device).changeState(0xc000130500, 0x1)

Signed-off-by: Martin Basovnik <martin.basovnik@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:38:47 +01:00
Jason A. Donenfeld 542e565baa device: do atomic 64-bit add outside of vector loop
Only bother updating the rxBytes counter once we've processed a whole
vector, since additions are atomic.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:35:57 +01:00
Jordan Whited 7c20311b3d device: reduce redundant per-packet overhead in RX path
Peer.RoutineSequentialReceiver() deals with packet vectors and does not
need to perform timer and endpoint operations for every packet in a
given vector. Changing these per-packet operations to per-vector
improves throughput by as much as 10% in some environments.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:34:09 +01:00
Jordan WhitedandJames Tucker 4ffa9c2032 device: change Peer.endpoint locking to reduce contention
Access to Peer.endpoint was previously synchronized by Peer.RWMutex.
This has now moved to Peer.endpoint.Mutex. Peer.SendBuffers() is now the
sole caller of Endpoint.ClearSrc(), which is signaled via a new bool,
Peer.endpoint.clearSrcOnTx. Previous Callers of Endpoint.ClearSrc() now
set this bool, primarily via peer.markEndpointSrcForClearing().
Peer.SetEndpointFromPacket() clears Peer.endpoint.clearSrcOnTx when an
updated conn.Endpoint is stored. This maintains the same event order as
before, i.e. a conn.Endpoint received after peer.endpoint.clearSrcOnTx
is set, but before the next Peer.SendBuffers() call results in the
latest conn.Endpoint source being used for the next packet transmission.

These changes result in throughput improvements for single flow,
parallel (-P n) flow, and bidirectional (--bidir) flow iperf3 TCP/UDP
tests as measured on both Linux and Windows. Latency under load improves
especially for high throughput Linux scenarios. These improvements are
likely realized on all platforms to some degree, as the changes are not
platform-specific.

Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:34:09 +01:00
Jordan Whited d0bc03c707 tun: implement UDP GSO/GRO for Linux
Implement UDP GSO and GRO for the Linux tun.Device, which is made
possible by virtio extensions in the kernel's TUN driver starting in
v6.2.

secnetperf, a QUIC benchmark utility from microsoft/msquic@8e1eb1a, is
used to demonstrate the effect of this commit between two Linux
computers with i5-12400 CPUs. There is roughly ~13us of round trip
latency between them. secnetperf was invoked with the following command
line options:
-stats:1 -exec:maxtput -test:tput -download:10000 -timed:1 -encrypt:0

The first result is from commit 2e0774f without UDP GSO/GRO on the TUN.

[conn][0x55739a144980] STATS: EcnCapable=0 RTT=3973 us
SendTotalPackets=55859 SendSuspectedLostPackets=61
SendSpuriousLostPackets=59 SendCongestionCount=27
SendEcnCongestionCount=0 RecvTotalPackets=2779122
RecvReorderedPackets=0 RecvDroppedPackets=0
RecvDuplicatePackets=0 RecvDecryptionFailures=0
Result: 3654977571 bytes @ 2922821 kbps (10003.972 ms).

The second result is with UDP GSO/GRO on the TUN.

[conn][0x56493dfd09a0] STATS: EcnCapable=0 RTT=1216 us
SendTotalPackets=165033 SendSuspectedLostPackets=64
SendSpuriousLostPackets=61 SendCongestionCount=53
SendEcnCongestionCount=0 RecvTotalPackets=11845268
RecvReorderedPackets=25267 RecvDroppedPackets=0
RecvDuplicatePackets=0 RecvDecryptionFailures=0
Result: 15574671184 bytes @ 12458214 kbps (10001.222 ms).

Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:27:22 +01:00
Jordan Whited 1cf89f5339 tun: fix Device.Read() buf length assumption on Windows
The length of a packet read from the underlying TUN device may exceed
the length of a supplied buffer when MTU exceeds device.MaxMessageSize.

Reviewed-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-12-11 16:20:49 +01:00
Jason A. Donenfeld 2e0774f246 device: ratchet up max segment size on android
GRO requires big allocations to be efficient. This isn't great, as there
might be Android memory usage issues. So we should revisit this commit.
But at least it gets things working again.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-10-22 02:12:13 +02:00