Compare commits

...
Sign in to create a new pull request.

59 commits

Author SHA1 Message Date
Leadaxe
3909adbf87 lx: early rebind and wake nudge for provably dead sessions (sing-box-lx SPEC 041 v2)
The v1 give-up rebind heals a dead 5-tuple only at ~90s, while users probe
within the first seconds after device wake. Two new triggers over the same
action and shared debounce window:

- early: >=3 unanswered initiations against a provably dead session (no live
  keypair, or last handshake older than RejectAfterTime) rebind at ~15s from
  the retry branch; a live session keeps byte-for-byte upstream behaviour;
- nudge: public Device.RebindIfSessionStale() lets the consumer report a
  device wake-up and heal stale peers immediately, without traffic demand.

The whole mechanism now lives in device/lx_giveup_rebind.go (moved from
device.go to keep the upstream file delta minimal); the rebind log line
carries the trigger label.
2026-08-05 16:56:12 +03:00
Leadaxe
c4e0bcf768 lx: rebind socket on handshake give-up (sing-box-lx SPEC 041 self-heal)
After ~90s of unanswered handshake initiations (the give-up branch of
expiredRetransmitHandshake) the socket's 5-tuple is proven dead (expired
NAT mapping / poisoned DPI flow entry after device sleep) and upstream
retries into it forever; only a manual reconnect healed the peer.

Reopen the bind once per give-up cycle (fresh ephemeral port unless the
user pinned listen_port), then re-initiate immediately. Debounced via CAS;
no timers or goroutines while healthy; a rebind racing Down()/Close()
degrades to a no-op inside BindUpdate. Red/green e2e + unit tests.
2026-08-05 16:56:04 +03:00
Leadaxe
37bc7b9f55 test(awg): pin IpcGet introspection parity for all 16 obfuscation params
i1..i5 are emitted by an `i%d=` loop rather than literal per-key sendf calls,
which made a grep-based audit conclude they were missing from the get path.
Live IpcSet -> IpcGet round-trip proves all 16 are reported; unset I-slots
(i2/i4) stay absent. See SPECS/TASKS/031-AWG_PARITY_AUDIT_ADVANCED_SECURITY.
2026-08-05 16:55:41 +03:00
Leadaxe
9a23f36748 lx: re-graft egress-provider API onto AWG2 base (upstream 6f5e8b1947)
Upstream wireguard-go added an EgressProvider hook to StdNetBind (egress
anchoring for TUN auto-redirect): EgressProvider interface, egressProvider
field, SetEgressProvider, plus egress branches in Open/Send/Close and a
standardEndpoint cast hoist in Send. sing-box's endpoint-listen refactor
now calls StdNetBind.SetEgressProvider, so our AWG2 fork (which was one
wireguard-go revision behind) failed to build.

Applied the upstream delta to conn/bind_std.go verbatim, then re-applied
SPEC 026: gate BOTH reserved-clear sites behind hasReserved() —

  - main receiveIP path (msg.N > 3 && s.hasReserved())    [existing]
  - NEW egress receive path (dataLength > 3 && s.hasReserved())

Upstream's egress hook re-introduced an UNCONDITIONAL common.ClearArray(
bufs[0][1:4]) — the exact anti-pattern SPEC 026 fixed. Gating it keeps a
small-padding AmneziaWG magic (bytes 1-3) intact when no WARP reserved
value is set, and STILL fires for WARP-over-egress: the egress bind is
only created on the isUDPListener path, where SetReservedForEndpoint is
called for every peer, so hasReserved() is true in that scenario. Verified:
WARP egress unaffected, AWG magic survives, conn tests green.

reservedForEndpoint is populated before receive goroutines start and never
mutated after (same lock-free invariant SPEC 026 already relies on).
2026-08-05 16:55:31 +03:00
Leadaxe
1e787bb3e0 lx: gate reserved-byte clear on receive so AmneziaWG magic survives
The Cloudflare "reserved" bytes (1-3) were zeroed unconditionally on
every received datagram across all StdNetBind/WinRingBind receive paths.
AmneziaWG reads its magic header as LittleEndian.Uint32(packet[padding:])
where padding is s1/s2/s4; with small padding (0-3) the magic overlaps
bytes 1-3, so clearing them collapses it out of the ranged h1-h4 window
and every packet is dropped (handshake included) — the AWG endpoint
never comes up. Plain WG (types 1-4, bytes 1-3 already zero) and large
padding are unaffected, which is why it went unnoticed.

Gate all five receive clears (bind_std receiveIP, msgx_darwin
receiveSingle + makeReceiveMsgX, bind_windows receiveIPv4/v6) behind a
new hasReserved() so bytes 1-3 are only touched when a WARP reserved
value is actually configured. Send paths already gate on a per-endpoint
loaded/non-zero check, so they are left unchanged. The reserved map is
populated before the receive goroutines start and never mutated after,
so the lock-free read is safe.

Tests: awg_stdnetbind_reserved_lx_test.go brings up two Devices over
StdNetBind with zero padding (magic in bytes 0-3) and asserts delivery
(red before the fix, green after); reserved_gate_lx_test.go pins the
hasReserved() gate.
2026-08-05 16:55:02 +03:00
Leadaxe
ee7ff1b77f lx: fix transport padding buffer overrun + harden AWG config guards
Transport padding (s4) crashed the whole process with
"index out of range" in RoutineSequentialSender on the first data
packet: InputPacket/InputPackets sized elem.buffer without headroom
for the in-buffer right-shift that prepends the random prefix.

- send.go: reserve paddings.transport in both injection-path
  allocLength computations; replace the manual backward byte loop
  with an overlap-safe copy; defensively grow the buffer (pool-backed)
  if it still lacks headroom, dropping packets that cannot fit a
  single WG message instead of overrunning.
- receive.go: drop the rxBytes/timers block duplicated by the AWG
  re-graft (rx accounting was doubled, keepKeyFreshReceiving fired
  twice per batch).
- send.go: swap jmin/jmax when configured inverted (UAPI validates
  the fields only individually; a swapped pair panicked rand.Int
  with a non-positive bound on the first handshake).
- obf*.go: bound obfuscator length args to [0, MaxMessageSize]
  (negative panicked slice bounds, huge ones OOMed the handshake).
- magic-header.go: widen to int64 before end-start+1 so a full-range
  header cannot wrap to a zero rand.Int bound.

Tests: transport_padding_test.go reproduces the on-device crash
byte-for-byte (red on the previous commit, green now) across both
injection paths and the tun path; obf_guards_test.go pins the
config-value guards.
2026-08-05 16:55:02 +03:00
Leadaxe
831d483366 lx: re-graft AmneziaWG 2.0 obfuscation onto sagernet/wireguard-go v0.0.5
Rebase of the AWG obf graft (was e5feca7 on v0.0.3) onto v0.0.5
(2c27bbf4f9, 'Add L3 forwarding support'). 15 of 16 graft files
applied clean via 3-way; only send.go conflicted, on a single line
(upstream queuedOutboundPackets backpressure decrement vs a graft
blank line — took upstream).

Key invariant preserved: MessageEncapsulatingTransportSize=0 (graft
zeroes the sagernet encapsulating headroom; AWG obfuscation composes
the prefix itself via SendBuffers, not Bind.Send prepend). Upstream's
InputPacket/InputPackets and the new size-based outbound buffer pool
(GetOutboundBuffer/PutOutboundBuffer) are taken verbatim; the graft's
RoutineEncryption (header at buffer start) and transport-padding shift
in RoutineSequentialSender re-woven around them.

Builds clean on linux/android/windows/darwin (device/conn/tun).
2026-08-05 16:53:12 +03:00
世界
f39689ad35
Fix input packets peer lookup
InputPacket/InputPackets used the deprecated trie-only AllowedIPs.Lookup. With tailscale v1.102 a PeerByIPPacketFunc is installed and the trie is no longer populated, so every injected packet was unmatched. Use LookupFromPacket, and size the sequential sender scratch for full input batches instead of capping containers at the device batch size.
2026-08-05 12:29:41 +08:00
世界
da8671622b
Fix InputPackets exceeding device batch size 2026-08-05 10:47:57 +08:00
Jordan Whited
7a66fbee4a
device: add priority message transmission around session establishment
Add SetPriorityMessageOnEstablishmentFunc, which registers a
PeerPriorityMessageFunc callback invoked when a peer's session keypair
is established or re-keyed for forward data transmission. The bytes it
returns are transmitted to the peer as a transport message.

The message is "priority" in two senses: it bypasses the staged packet
queue entirely, so it cannot be evicted by TUN-sourced packets, and it
is enqueued ahead of the keepalive/staged packets that follow keypair
establishment.

Updates tailscale/tailscale#20081

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-08-04 19:18:01 +08:00
Alex Valiushko
15b912c1c0
device: fix TOCTOU race during session state update (#77)
API introduced in a927a66e has two cases of state determination
happening out of critical section for the state value:

(1) expiredSession loads sessionExpiresNano, then releases all locks
and calls noteSessionState(Expired). So a concurrent refresh that
lands in that gap gets clobbered by a stale Expired -- and sticks
until the next re-key.

(2) Likewise in noteSessionHandshakeStopped, hasKeyMaterial check
happens out of the session state lock and races with ZeroAndFlushAll.

Both lead to a wrong state emitted via the device.sessionState.fn,
but are otherwise benign.

This moves the expiry timestamp under a lock to address the former,
and provides a noteSessionStateLocked helper for the latter.
Also changes API semantics to serialize events per-peer, to avoid
sharing a single lock for all timestamps.

Updates tailscale/corp#42874

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
Change-Id: Iee2cdf135375519e58a8e84362349d966a6a6964
2026-08-04 19:18:01 +08:00
Alex Valiushko
35a60acb84
device: set peer to expire unconditionally (#73)
e3ac4a0afb4e introduced a lightweight API that can be used instead of UAPI
to reconfigure peers. Peer state created via the new PeerLookupFunc
is not set to expire until the handshake succeeds, making device leak two
goroutines and a set of buffers for each failed handshake.

This change arms the expiry timer before the handshake gets to proceed.

Updates tailscale/tailscale#20183

Change-Id: Ibc0abb6eec97aca0a10f50515dea9e0d6a6a6964
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-08-04 19:18:01 +08:00
Simon Law
cd7ac13b86
device: convert runtime.SetFinalizer to AddCleanup (#71)
In PR #66, we tried to address a memory leak by avoiding
runtime.SetFinalizer for autodrainingInboundQueue and
autodrainingOutboundQueue unless there was something to do.

However, when there is work to be done, these finalizers still leak
memory because they’re still holding on to a cyclical reference to q.
This applies to any platform that relies on a bounded device.WaitPool,
like Android and iOS which both declare PreallocatedBuffersPerPool.

This patch converts this logic to runtime.AddCleanup which is designed
to avoid this problem.

Updates tailscale/corp#42776

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
2ad9837e6c
device: refactor container locking for lock-order clarity
Device-side portion of upstream tailscale/wireguard-go e3ac4a0
(device, cmd/check-lockorder: add static analysis tool for lock
ordering); the analyzer itself is not carried in this fork.
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
7c3a736cbe
device: add peer session state callback
Add a minimal callback API for observing WireGuard peer session state
changes.

Updates tailscale/corp#42874
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
09268b375c
device: avoid cycle-leaky runtime.SetFinalizer when unnecessary
In tailscale/wireguard-go#65, @lkosewsk reproduced a memory leak seen
in prod with lots of wireguard-go instances being created and
destroyed, where they were still being retained forever due to cycles
in the runtime.SetFinalizer reference graph.

Really we shouldn't be using runtime.SetFinalizer anywhere. But we
still use it on mobile platforms in WaitPool. But those platforms
don't have thousands of tsnet.Server instances coming & going, so this
is a half fix: avoid the finalizer registration on Linux, etc where
the queue doesn't need to be drained and there's no WaitPool
accounting. Just let GC handle it, without adding finalizer cycle
complexity.

Updates tailscale/corp#42776

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
010dd5c6f2
device: fix some lock ordering violations, add a test for a deadlock we hit
Discovered by a tool + test that will come in a future change.

Updates tailscale/tailscale#19513

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
f69b24781e
device: further add, revise API for on-demand configuration of peers
Updates tailscale/tailscale#17858
Updates tailscale/corp#35603

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 19:18:01 +08:00
Brad Fitzpatrick
e924a91e99
device: add API for on-demand configuration of peers
Updates tailscale/tailscale#17858

Signed-off-by: Brad Fitzpatrick <brad@danga.com>
2026-08-04 19:06:30 +08:00
Brad Fitzpatrick
70b09a6edd
device: put AllowedIPs mutex before what it guards, unexport fields
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 19:04:35 +08:00
世界
6f5e8b1947
Add EgressProvider 2026-07-17 10:48:47 +08:00
世界
2c27bbf4f9
FIx batched InputPackets 2026-07-06 23:38:56 +08:00
世界
57baac9504
Add batched UDP I/O on Darwin via sendmsg_x/recvmsg_x
On a connected socket sendmsg_x sends a whole batch in one syscall;
msg_name is unsupported there, so batched sends require connecting to
the peer, which loses roaming and is therefore opt-in via
SetSinglePeerMode for single fixed-peer binds. recvmsg_x fills in
per-message source addresses, so batched receive works on unconnected
sockets too. Any unexpected errno permanently falls back to the generic
paths. iOS is excluded: in the Network Extension recvmsg_x on
unconnected UDP sockets delivers no data, and connected sockets stop
passing traffic after a rebind.
2026-07-06 21:06:55 +08:00
世界
fcbb7c473b
Coalesce UDP GSO segments as iovecs
coalesceMessages copied every additional datagram into the spare
capacity of the first buffer, which no longer exists now that element
buffers are sized to their packet; append the datagrams as iovecs
instead, which also removes the copy.
2026-07-06 21:06:45 +08:00
世界
8403cdb937
Rework outbound buffer management
Outbound element buffers now come from the sing allocator sized to the
actual packet instead of the bounded MaxMessageSize pool, element and
container pools become plain sync.Pools, and the bounded message buffer
pool serves only the receive path. Packets injected via
InputPacket/InputPackets are dropped before they are copied once a peer
has 2048 packets queued: injection runs on the caller's read loop, which
must never block on pool exhaustion, and the queues are bounded in
containers, so a flood was buffered instead of dropped.
2026-07-06 21:05:53 +08:00
世界
9de6dc32df
Add batched InputPackets 2026-07-06 14:17:42 +08:00
世界
19b0d35877
Fix reserved bytes offset in StdNetBind.Send 2026-05-17 20:36:46 +08:00
世界
fa73d0f1ae
Fix input packet 2026-05-17 20:36:46 +08:00
世界
73f8c6542b
Export std net bind 2026-05-17 20:36:46 +08:00
世界
a71256d250
Add device.InputPacket 2026-05-17 20:36:46 +08:00
世界
7be452de15
Add pause support 2026-05-17 20:36:45 +08:00
世界
b4db0692d3
Add custom worker size params
(cherry picked from commit 7c2acadba17cadf8a1df957c49e1333130d460ad)
(cherry picked from commit a7bac1754e7717e1d4009d1ffd2d13330067d631)
(cherry picked from commit 7a2f11c693b49e784318bbf987173095c67b563d)
2026-05-17 20:36:35 +08:00
世界
824e7573c0
Apply Tailscale TUN offload APIs 2026-05-17 20:36:35 +08:00
世界
414291f6d6
Apply Tailscale endpoint awareness 2026-05-17 20:36:35 +08:00
世界
749db0015c
Apply Tailscale bind send headroom 2026-05-17 20:36:35 +08:00
世界
45cd03b8a1
Downgrade dependencies 2026-05-17 20:36:34 +08:00
世界
75d0f348d5
Rename module 2026-05-17 20:09:28 +08:00
世界
f853bfc5c5
Remove unused 2026-05-17 20:04:34 +08:00
世界
e620c55272
Add module rename script 2026-05-17 20:02:24 +08:00
世界
680e63a4f8
Add remove unused script 2026-05-17 20:02:24 +08:00
世界
905a805bc6
Update .gitignore 2026-05-17 20:02:24 +08:00
Jason A. Donenfeld
f333402bd9 version: bump snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-22 01:45:02 +02:00
Jason A. Donenfeld
c92064f1ce conn: don't enable GRO on Linux < 5.12
Kernels below 5.12 are missing this:

    commit 98184612aca0a9ee42b8eb0262a49900ee9eef0d
    Author: Norman Maurer <norman_maurer@apple.com>
    Date:   Thu Apr 1 08:59:17 2021

        net: udp: Add support for getsockopt(..., ..., UDP_GRO, ..., ...);

        Support for UDP_GRO was added in the past but the implementation for
        getsockopt was missed which did lead to an error when we tried to
        retrieve the setting for UDP_GRO. This patch adds the missing switch
        case for UDP_GRO

        Fixes: e20cf8d3f1f7 ("udp: implement GRO for plain UDP sockets.")
        Signed-off-by: Norman Maurer <norman_maurer@apple.com>
        Reviewed-by: David Ahern <dsahern@kernel.org>
        Signed-off-by: David S. Miller <davem@davemloft.net>

That means we can't set the option and then read it back later. Given
how buggy UDP_GRO is in general on odd kernels, just disable it on older
kernels all together.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-22 01:43:39 +02:00
Alexander Yastrebov
264889f0bb device: optimize message encoding
Optimize message encoding by eliminating binary.Write (which internally
uses reflection) in favour of hand-rolled encoding.

This is companion to 9e7529c3d2.

Synthetic benchmark:

    var packetSink []byte
    func BenchmarkMessageInitiationMarshal(b *testing.B) {
        var msg MessageInitiation
        b.Run("binary.Write", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                var buf [MessageInitiationSize]byte
                writer := bytes.NewBuffer(buf[:0])
                _ = binary.Write(writer, binary.LittleEndian, msg)
                packetSink = writer.Bytes()
            }
        })
        b.Run("binary.Encode", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                packet := make([]byte, MessageInitiationSize)
                _, _ = binary.Encode(packet, binary.LittleEndian, msg)
                packetSink = packet
            }
        })
        b.Run("marshal", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                packet := make([]byte, MessageInitiationSize)
                _ = msg.marshal(packet)
                packetSink = packet
            }
        })
    }

Results:
                                             │      -      │
                                             │   sec/op    │
    MessageInitiationMarshal/binary.Write-8    1.337µ ± 0%
    MessageInitiationMarshal/binary.Encode-8   1.242µ ± 0%
    MessageInitiationMarshal/marshal-8         53.05n ± 1%

                                             │     -      │
                                             │    B/op    │
    MessageInitiationMarshal/binary.Write-8    368.0 ± 0%
    MessageInitiationMarshal/binary.Encode-8   160.0 ± 0%
    MessageInitiationMarshal/marshal-8         160.0 ± 0%

                                             │     -      │
                                             │ allocs/op  │
    MessageInitiationMarshal/binary.Write-8    3.000 ± 0%
    MessageInitiationMarshal/binary.Encode-8   1.000 ± 0%
    MessageInitiationMarshal/marshal-8         1.000 ± 0%

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-21 00:09:36 +02:00
Jason A. Donenfeld
256bcbd70d device: add support for removing allowedips individually
This pairs with the recent change in wireguard-tools.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-20 23:03:06 +02:00
Jason A. Donenfeld
1571e0fbae version: bump snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:54:03 +02:00
Jason A. Donenfeld
842888ac5c device: make unmarshall length checks exact
This is already enforced in receive.go, but if these unmarshallers are
to have error return values anyway, make them as explicit as possible.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:48:14 +02:00
Alexander Yastrebov
9e7529c3d2 device: reduce RoutineHandshake allocations
Reduce allocations by eliminating byte reader, hand-rolled decoding and
reusing message structs.

Synthetic benchmark:

    var msgSink MessageInitiation
    func BenchmarkMessageInitiationUnmarshal(b *testing.B) {
        packet := make([]byte, MessageInitiationSize)
        reader := bytes.NewReader(packet)
        err := binary.Read(reader, binary.LittleEndian, &msgSink)
        if err != nil {
            b.Fatal(err)
        }
        b.Run("binary.Read", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                reader := bytes.NewReader(packet)
                _ = binary.Read(reader, binary.LittleEndian, &msgSink)
            }
        })
        b.Run("unmarshal", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                _ = msgSink.unmarshal(packet)
            }
        })
    }

Results:
                                         │      -      │
                                         │   sec/op    │
MessageInitiationUnmarshal/binary.Read-8   1.508µ ± 2%
MessageInitiationUnmarshal/unmarshal-8     12.66n ± 2%

                                         │      -       │
                                         │     B/op     │
MessageInitiationUnmarshal/binary.Read-8   208.0 ± 0%
MessageInitiationUnmarshal/unmarshal-8     0.000 ± 0%

                                         │      -       │
                                         │  allocs/op   │
MessageInitiationUnmarshal/binary.Read-8   2.000 ± 0%
MessageInitiationUnmarshal/unmarshal-8     0.000 ± 0%

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:42:06 +02:00
Kurnia D Win
436f7fdc16 rwcancel: fix wrong poll event flag on ReadyWrite
It should be POLLIN because closeFd is read-only file.

Signed-off-by: Kurnia D Win <kurnia.d.win@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
Tom Holford
0e4482a086 device: use rand.NewSource instead of rand.Seed
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
Tom Holford
77b6c824a8 global: replaced unused function params with _
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
ruokeqx
bc30fee374 tun: darwin: fetch flags and mtu from if_msghdr directly
Signed-off-by: ruokeqx <ruokeqx@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
Tu Dinh Ngoc
b82c016264 tun: use add-with-carry in checksumNoFold()
Use parallel summation with native byte order per RFC 1071.
add-with-carry operation is used to add 4 words per operation.  Byteswap
is performed before and after checksumming for compatibility with old
`checksumNoFold()`.  With this we get a 30-80% speedup in `checksum()`
depending on packet sizes.

Add unit tests with comparison to a per-word implementation.

**Intel(R) Xeon(R) Silver 4210R CPU @ 2.40GHz**

| Size | OldTime | NewTime | Speedup  |
|------|---------|---------|----------|
| 64   | 12.64   | 9.183   | 1.376456 |
| 128  | 18.52   | 12.72   | 1.455975 |
| 256  | 31.01   | 18.13   | 1.710425 |
| 512  | 54.46   | 29.03   | 1.87599  |
| 1024 | 102     | 52.2    | 1.954023 |
| 1500 | 146.8   | 81.36   | 1.804326 |
| 2048 | 196.9   | 102.5   | 1.920976 |
| 4096 | 389.8   | 200.8   | 1.941235 |
| 8192 | 767.3   | 413.3   | 1.856521 |
| 9000 | 851.7   | 448.8   | 1.897727 |
| 9001 | 854.8   | 451.9   | 1.891569 |

**AMD EPYC 7352 24-Core Processor**

| Size | OldTime | NewTime | Speedup  |
|------|---------|---------|----------|
| 64   | 9.159   | 6.949   | 1.318031 |
| 128  | 13.59   | 10.59   | 1.283286 |
| 256  | 22.37   | 14.91   | 1.500335 |
| 512  | 41.42   | 24.22   | 1.710157 |
| 1024 | 81.59   | 45.05   | 1.811099 |
| 1500 | 120.4   | 68.35   | 1.761522 |
| 2048 | 162.8   | 90.14   | 1.806079 |
| 4096 | 321.4   | 180.3   | 1.782585 |
| 8192 | 650.4   | 360.8   | 1.802661 |
| 9000 | 706.3   | 398.1   | 1.774177 |
| 9001 | 712.4   | 398.2   | 1.789051 |

Signed-off-by: Tu Dinh Ngoc <dinhngoc.tu@irit.fr>
[Jason: simplified and cleaned up unit tests]
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
Jason A. Donenfeld
45916071ba tun/netstack: cleanup network stack at closing time
Colin's commit went a step further and protected tun.incomingPacket with
a lock on shutdown, but let's see if the tun.stack.Close() call actually
solves that on its own.

Suggested-by: kshangx <hikeshang@hotmail.com>
Suggested-by: Colin Adler <colin1adler@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:09:09 +02:00
Jason A. Donenfeld
e3c1354d27 tun/netstack: remove usage of pkt.IsNil()
Since 3c75945fd ("netstack: remove PacketBuffer.IsNil()") this has been
invalid. Follow the replacement pattern of that commit.

The old definition inlined to the same code anyway:

 func (pk *PacketBuffer) IsNil() bool {
 	return pk == nil
 }

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:05:35 +02:00
Jason A. Donenfeld
32546a15a8 mod: bump deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:05:35 +02:00
Jason A. Donenfeld
9eb3221f1d global: bump copyright notice
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:05:35 +02:00
Jordan Whited
867a4c4a3f device: fix missed return of QueueOutboundElementsContainer to its WaitPool
Fixes: 3bb8fec ("conn, device, tun: implement vectorized I/O plumbing")
Reviewed-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-04 18:11:00 +02:00
Jordan Whited
113c8f1340 device: fix WaitPool sync.Cond usage
The sync.Locker used with a sync.Cond must be acquired when changing
the associated condition, otherwise there is a window within
sync.Cond.Wait() where a wake-up may be missed.

Fixes: 4846070 ("device: use a waiting sync.Pool instead of a channel")
Reviewed-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-04 18:11:00 +02:00
127 changed files with 5039 additions and 6999 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
wireguard-go wireguard-go
/.idea/
.DS_Store

View file

@ -1,77 +0,0 @@
# Go Implementation of [WireGuard](https://www.wireguard.com/)
This is an implementation of WireGuard in Go.
## Usage
Most Linux kernel WireGuard users are used to adding an interface with `ip link add wg0 type wireguard`. With wireguard-go, instead simply run:
```
$ wireguard-go wg0
```
This will create an interface and fork into the background. To remove the interface, use the usual `ip link del wg0`, or if your system does not support removing interfaces directly, you may instead remove the control socket via `rm -f /var/run/wireguard/wg0.sock`, which will result in wireguard-go shutting down.
To run wireguard-go without forking to the background, pass `-f` or `--foreground`:
```
$ wireguard-go -f wg0
```
When an interface is running, you may use [`wg(8)`](https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8) to configure it, as well as the usual `ip(8)` and `ifconfig(8)` commands.
To run with more logging you may set the environment variable `LOG_LEVEL=debug`.
## Platforms
### Linux
This will run on Linux; however you should instead use the kernel module, which is faster and better integrated into the OS. See the [installation page](https://www.wireguard.com/install/) for instructions.
### macOS
This runs on macOS using the utun driver. It does not yet support sticky sockets, and won't support fwmarks because of Darwin limitations. Since the utun driver cannot have arbitrary interface names, you must either use `utun[0-9]+` for an explicit interface name or `utun` to have the kernel select one for you. If you choose `utun` as the interface name, and the environment variable `WG_TUN_NAME_FILE` is defined, then the actual name of the interface chosen by the kernel is written to the file specified by that variable.
### Windows
This runs on Windows, but you should instead use it from the more [fully featured Windows app](https://git.zx2c4.com/wireguard-windows/about/), which uses this as a module.
### FreeBSD
This will run on FreeBSD. It does not yet support sticky sockets. Fwmark is mapped to `SO_USER_COOKIE`.
### OpenBSD
This will run on OpenBSD. It does not yet support sticky sockets. Fwmark is mapped to `SO_RTABLE`. Since the tun driver cannot have arbitrary interface names, you must either use `tun[0-9]+` for an explicit interface name or `tun` to have the program select one for you. If you choose `tun` as the interface name, and the environment variable `WG_TUN_NAME_FILE` is defined, then the actual name of the interface chosen by the kernel is written to the file specified by that variable.
## Building
This requires an installation of the latest version of [Go](https://go.dev/).
```
$ git clone https://git.zx2c4.com/wireguard-go
$ cd wireguard-go
$ make
```
## License
Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
@ -16,13 +16,20 @@ import (
"sync" "sync"
"syscall" "syscall"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
) )
var ( type EgressProvider interface {
_ Bind = (*StdNetBind)(nil) SetEgressPort(port uint16) bool
) LookupEgress(destination netip.AddrPort) *net.UDPConn
ReceiveEgress(buffer []byte) (int, netip.AddrPort, error)
}
var _ Bind = (*StdNetBind)(nil)
// StdNetBind implements Bind for all platforms. While Windows has its own Bind // StdNetBind implements Bind for all platforms. While Windows has its own Bind
// (see bind_windows.go), it may fall back to StdNetBind. // (see bind_windows.go), it may fall back to StdNetBind.
@ -30,11 +37,17 @@ var (
// methods for sending and receiving multiple datagrams per-syscall. See the // methods for sending and receiving multiple datagrams per-syscall. See the
// proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564. // proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564.
type StdNetBind struct { type StdNetBind struct {
externalControl control.Func
egressProvider EgressProvider
reservedForEndpoint map[netip.AddrPort][3]uint8
mu sync.Mutex // protects all fields except as specified mu sync.Mutex // protects all fields except as specified
ipv4 *net.UDPConn ipv4 *net.UDPConn
ipv6 *net.UDPConn ipv6 *net.UDPConn
ipv4PC *ipv4.PacketConn // will be nil on non-Linux ipv4PC *ipv4.PacketConn // will be nil on non-Linux
ipv6PC *ipv6.PacketConn // will be nil on non-Linux ipv6PC *ipv6.PacketConn // will be nil on non-Linux
ipv4RC syscall.RawConn // will be nil on non-Darwin
ipv6RC syscall.RawConn // will be nil on non-Darwin
ipv4TxOffload bool ipv4TxOffload bool
ipv4RxOffload bool ipv4RxOffload bool
ipv6TxOffload bool ipv6TxOffload bool
@ -44,12 +57,17 @@ type StdNetBind struct {
udpAddrPool sync.Pool udpAddrPool sync.Pool
msgsPool sync.Pool msgsPool sync.Pool
msgx msgXState
blackhole4 bool blackhole4 bool
blackhole6 bool blackhole6 bool
} }
func NewStdNetBind() Bind { func NewStdNetBind(externalControl control.Func) Bind {
return &StdNetBind{ return &StdNetBind{
externalControl: externalControl,
reservedForEndpoint: make(map[netip.AddrPort][3]uint8),
udpAddrPool: sync.Pool{ udpAddrPool: sync.Pool{
New: func() any { New: func() any {
return &net.UDPAddr{ return &net.UDPAddr{
@ -119,8 +137,29 @@ func (e *StdNetEndpoint) DstToString() string {
return e.AddrPort.String() return e.AddrPort.String()
} }
func listenNet(network string, port int) (*net.UDPConn, int, error) { func listenNet(externalControl control.Func, network string, port int) (*net.UDPConn, int, error) {
conn, err := listenConfig().ListenPacket(context.Background(), network, ":"+strconv.Itoa(port)) var listenerAddr string
if network == "udp6" {
listenerAddr = "[::]:" + strconv.Itoa(port)
} else {
listenerAddr = ":" + strconv.Itoa(port)
}
var listener net.ListenConfig
listener.Control = func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range controlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
if externalControl != nil {
return externalControl(network, address, conn)
} else {
return nil
}
}
conn, err := listener.ListenPacket(context.Background(), network, listenerAddr)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -147,6 +186,7 @@ func (s *StdNetBind) Open(uport uint16) ([]ReceiveFunc, uint16, error) {
if s.ipv4 != nil || s.ipv6 != nil { if s.ipv4 != nil || s.ipv6 != nil {
return nil, 0, ErrBindAlreadyOpen return nil, 0, ErrBindAlreadyOpen
} }
s.msgx.reset()
// Attempt to open ipv4 and ipv6 listeners on the same port. // Attempt to open ipv4 and ipv6 listeners on the same port.
// If uport is 0, we can retry on failure. // If uport is 0, we can retry on failure.
@ -156,13 +196,13 @@ again:
var v4pc *ipv4.PacketConn var v4pc *ipv4.PacketConn
var v6pc *ipv6.PacketConn var v6pc *ipv6.PacketConn
v4conn, port, err = listenNet("udp4", port) v4conn, port, err = listenNet(s.externalControl, "udp4", port)
if err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) { if err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) {
return nil, 0, err return nil, 0, err
} }
// Listen on the same port as we're using for ipv4. // Listen on the same port as we're using for ipv4.
v6conn, port, err = listenNet("udp6", port) v6conn, port, err = listenNet(s.externalControl, "udp6", port)
if uport == 0 && errors.Is(err, syscall.EADDRINUSE) && tries < 100 { if uport == 0 && errors.Is(err, syscall.EADDRINUSE) && tries < 100 {
v4conn.Close() v4conn.Close()
tries++ tries++
@ -179,7 +219,22 @@ again:
v4pc = ipv4.NewPacketConn(v4conn) v4pc = ipv4.NewPacketConn(v4conn)
s.ipv4PC = v4pc s.ipv4PC = v4pc
} }
fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload)) if supportsMsgX {
var receiveFn ReceiveFunc
receiveFn, err = s.makeReceiveMsgX(v4conn, false)
if err != nil {
v4conn.Close()
return nil, 0, err
}
s.ipv4RC, err = v4conn.SyscallConn()
if err != nil {
v4conn.Close()
return nil, 0, err
}
fns = append(fns, receiveFn)
} else {
fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload))
}
s.ipv4 = v4conn s.ipv4 = v4conn
} }
if v6conn != nil { if v6conn != nil {
@ -188,20 +243,57 @@ again:
v6pc = ipv6.NewPacketConn(v6conn) v6pc = ipv6.NewPacketConn(v6conn)
s.ipv6PC = v6pc s.ipv6PC = v6pc
} }
fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload)) if supportsMsgX {
var receiveFn ReceiveFunc
receiveFn, err = s.makeReceiveMsgX(v6conn, true)
if err != nil {
v6conn.Close()
return nil, 0, err
}
s.ipv6RC, err = v6conn.SyscallConn()
if err != nil {
v6conn.Close()
return nil, 0, err
}
fns = append(fns, receiveFn)
} else {
fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload))
}
s.ipv6 = v6conn s.ipv6 = v6conn
} }
if len(fns) == 0 { if len(fns) == 0 {
return nil, 0, syscall.EAFNOSUPPORT return nil, 0, syscall.EAFNOSUPPORT
} }
if s.egressProvider != nil {
s.egressProvider.SetEgressPort(uint16(port))
fns = append(fns, func(bufs [][]byte, sizes []int, endpoints []Endpoint) (int, error) {
dataLength, source, err := s.egressProvider.ReceiveEgress(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = dataLength
if dataLength > 3 && s.hasReserved() { // lx: SPEC 026 — gate reserved-clear on the egress receive path too, so a small-padding AmneziaWG magic in bytes 1-3 survives when no WARP reserved value is set
common.ClearArray(bufs[0][1:4])
}
endpoints[0] = &StdNetEndpoint{AddrPort: source}
return 1, nil
})
}
return fns, uint16(port), nil return fns, uint16(port), nil
} }
func (s *StdNetBind) SetEgressProvider(provider EgressProvider) {
s.egressProvider = provider
}
func (s *StdNetBind) putMessages(msgs *[]ipv6.Message) { func (s *StdNetBind) putMessages(msgs *[]ipv6.Message) {
for i := range *msgs { for i := range *msgs {
(*msgs)[i].OOB = (*msgs)[i].OOB[:0] buffers := (*msgs)[i].Buffers
(*msgs)[i] = ipv6.Message{Buffers: (*msgs)[i].Buffers, OOB: (*msgs)[i].OOB} for j := range buffers {
buffers[j] = nil
}
(*msgs)[i] = ipv6.Message{Buffers: buffers[:1], OOB: (*msgs)[i].OOB[:0]}
} }
s.msgsPool.Put(msgs) s.msgsPool.Put(msgs)
} }
@ -210,10 +302,8 @@ func (s *StdNetBind) getMessages() *[]ipv6.Message {
return s.msgsPool.Get().(*[]ipv6.Message) return s.msgsPool.Get().(*[]ipv6.Message)
} }
var ( // If compilation fails here these are no longer the same underlying type.
// If compilation fails here these are no longer the same underlying type. var _ ipv6.Message = ipv4.Message{}
_ ipv6.Message = ipv4.Message{}
)
type batchReader interface { type batchReader interface {
ReadBatch([]ipv6.Message, int) (int, error) ReadBatch([]ipv6.Message, int) (int, error)
@ -269,8 +359,10 @@ func (s *StdNetBind) receiveIP(
if sizes[i] == 0 { if sizes[i] == 0 {
continue continue
} }
addrPort := msg.Addr.(*net.UDPAddr).AddrPort() if msg.N > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation common.ClearArray(bufs[i][1:4])
}
ep := &StdNetEndpoint{AddrPort: M.AddrPortFromNet(msg.Addr)} // TODO: remove allocation
getSrcFromControl(msg.OOB[:msg.NN], ep) getSrcFromControl(msg.OOB[:msg.NN], ep)
eps[i] = ep eps[i] = ep
} }
@ -295,6 +387,9 @@ func (s *StdNetBind) BatchSize() int {
if runtime.GOOS == "linux" || runtime.GOOS == "android" { if runtime.GOOS == "linux" || runtime.GOOS == "android" {
return IdealBatchSize return IdealBatchSize
} }
if supportsMsgX {
return msgXBatchSize
}
return 1 return 1
} }
@ -302,6 +397,9 @@ func (s *StdNetBind) Close() error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if s.egressProvider != nil {
s.egressProvider.SetEgressPort(0)
}
var err1, err2 error var err1, err2 error
if s.ipv4 != nil { if s.ipv4 != nil {
err1 = s.ipv4.Close() err1 = s.ipv4.Close()
@ -338,14 +436,22 @@ func (e ErrUDPGSODisabled) Unwrap() error {
return e.RetryErr return e.RetryErr
} }
func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error {
for len(bufs) > IdealBatchSize {
err := s.Send(bufs[:IdealBatchSize], endpoint, offset)
if err != nil {
return err
}
bufs = bufs[IdealBatchSize:]
}
standardEndpoint := endpoint.(*StdNetEndpoint)
s.mu.Lock() s.mu.Lock()
blackhole := s.blackhole4 blackhole := s.blackhole4
conn := s.ipv4 conn := s.ipv4
offload := s.ipv4TxOffload offload := s.ipv4TxOffload
br := batchWriter(s.ipv4PC) br := batchWriter(s.ipv4PC)
is6 := false is6 := false
if endpoint.DstIP().Is6() { if standardEndpoint.DstIP().Is6() {
blackhole = s.blackhole6 blackhole = s.blackhole6
conn = s.ipv6 conn = s.ipv6
br = s.ipv6PC br = s.ipv6PC
@ -366,22 +472,42 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error {
ua := s.udpAddrPool.Get().(*net.UDPAddr) ua := s.udpAddrPool.Get().(*net.UDPAddr)
defer s.udpAddrPool.Put(ua) defer s.udpAddrPool.Put(ua)
if is6 { if is6 {
as16 := endpoint.DstIP().As16() as16 := standardEndpoint.DstIP().As16()
copy(ua.IP, as16[:]) copy(ua.IP, as16[:])
ua.IP = ua.IP[:16] ua.IP = ua.IP[:16]
} else { } else {
as4 := endpoint.DstIP().As4() as4 := standardEndpoint.DstIP().As4()
copy(ua.IP, as4[:]) copy(ua.IP, as4[:])
ua.IP = ua.IP[:4] ua.IP = ua.IP[:4]
} }
ua.Port = int(endpoint.(*StdNetEndpoint).Port()) ua.Port = int(standardEndpoint.Port())
var ( var (
retried bool retried bool
err error err error
) )
for _, buf := range bufs {
if len(buf) > offset+3 {
reserved, loaded := s.reservedForEndpoint[standardEndpoint.AddrPort]
if loaded {
copy(buf[offset+1:offset+4], reserved[:])
}
}
}
if s.egressProvider != nil {
memberConn := s.egressProvider.LookupEgress(standardEndpoint.AddrPort)
if memberConn != nil {
for _, buf := range bufs {
_, err = memberConn.WriteToUDPAddrPort(buf[offset:], standardEndpoint.AddrPort)
if err != nil {
return err
}
}
return nil
}
}
retry: retry:
if offload { if offload {
n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, *msgs, setGSOSize) n := coalesceMessages(ua, standardEndpoint, bufs, offset, *msgs, setGSOSize)
err = s.send(conn, br, (*msgs)[:n]) err = s.send(conn, br, (*msgs)[:n])
if err != nil && offload && errShouldDisableUDPGSO(err) { if err != nil && offload && errShouldDisableUDPGSO(err) {
offload = false offload = false
@ -398,8 +524,8 @@ retry:
} else { } else {
for i := range bufs { for i := range bufs {
(*msgs)[i].Addr = ua (*msgs)[i].Addr = ua
(*msgs)[i].Buffers[0] = bufs[i] (*msgs)[i].Buffers[0] = bufs[i][offset:]
setSrcControl(&(*msgs)[i].OOB, endpoint.(*StdNetEndpoint)) setSrcControl(&(*msgs)[i].OOB, standardEndpoint)
} }
err = s.send(conn, br, (*msgs)[:len(bufs)]) err = s.send(conn, br, (*msgs)[:len(bufs)])
} }
@ -409,6 +535,24 @@ retry:
return err return err
} }
func (s *StdNetBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) {
s.reservedForEndpoint[destination] = reserved
}
// lx: hasReserved reports whether any Cloudflare "reserved" value is set. The
// receive path must only zero bytes 1-3 when a reserved value exists (WARP);
// otherwise an AmneziaWG magic header that lands in bytes 1-3 (small s1/s2/s4
// padding) would be corrupted and the packet dropped. The send path already
// gates its stamp on a per-endpoint `loaded` check, so no change is needed there.
func (s *StdNetBind) hasReserved() bool {
for _, reserved := range s.reservedForEndpoint {
if reserved != [3]uint8{} {
return true
}
}
return false
}
func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error { func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error {
var ( var (
n int n int
@ -424,6 +568,12 @@ func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message
start += n start += n
} }
} else { } else {
if supportsMsgX {
handled, sendErr := s.sendMsgX(conn, msgs)
if handled {
return sendErr
}
}
for _, msg := range msgs { for _, msg := range msgs {
_, _, err = conn.WriteMsgUDP(msg.Buffers[0], msg.OOB, msg.Addr.(*net.UDPAddr)) _, _, err = conn.WriteMsgUDP(msg.Buffers[0], msg.OOB, msg.Addr.(*net.UDPAddr))
if err != nil { if err != nil {
@ -447,10 +597,11 @@ const (
type setGSOFunc func(control *[]byte, gsoSize uint16) type setGSOFunc func(control *[]byte, gsoSize uint16)
func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs []ipv6.Message, setGSO setGSOFunc) int { func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, offset int, msgs []ipv6.Message, setGSO setGSOFunc) int {
var ( var (
base = -1 // index of msg we are currently coalescing into base = -1 // index of msg we are currently coalescing into
gsoSize int // segmentation size of msgs[base] gsoSize int // segmentation size of msgs[base]
totalLen int // length of all dgrams coalesced into msgs[base]
dgramCnt int // number of dgrams coalesced into msgs[base] dgramCnt int // number of dgrams coalesced into msgs[base]
endBatch bool // tracking flag to start a new batch on next iteration of bufs endBatch bool // tracking flag to start a new batch on next iteration of bufs
) )
@ -459,16 +610,17 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs
maxPayloadLen = maxIPv6PayloadLen maxPayloadLen = maxIPv6PayloadLen
} }
for i, buf := range bufs { for i, buf := range bufs {
buf = buf[offset:]
if i > 0 { if i > 0 {
msgLen := len(buf) msgLen := len(buf)
baseLenBefore := len(msgs[base].Buffers[0]) if msgLen+totalLen <= maxPayloadLen &&
freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore
if msgLen+baseLenBefore <= maxPayloadLen &&
msgLen <= gsoSize && msgLen <= gsoSize &&
msgLen <= freeBaseCap &&
dgramCnt < udpSegmentMaxDatagrams && dgramCnt < udpSegmentMaxDatagrams &&
!endBatch { !endBatch {
msgs[base].Buffers[0] = append(msgs[base].Buffers[0], buf...) // Coalesce as an additional iovec instead of copying: element
// buffers are sized to their packet and have no spare capacity.
msgs[base].Buffers = append(msgs[base].Buffers, buf)
totalLen += msgLen
if i == len(bufs)-1 { if i == len(bufs)-1 {
setGSO(&msgs[base].OOB, uint16(gsoSize)) setGSO(&msgs[base].OOB, uint16(gsoSize))
} }
@ -489,8 +641,9 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs
endBatch = false endBatch = false
base++ base++
gsoSize = len(buf) gsoSize = len(buf)
totalLen = len(buf)
setSrcControl(&msgs[base].OOB, ep) setSrcControl(&msgs[base].OOB, ep)
msgs[base].Buffers[0] = buf msgs[base].Buffers = append(msgs[base].Buffers[:0], buf)
msgs[base].Addr = addr msgs[base].Addr = addr
dgramCnt = 1 dgramCnt = 1
} }

View file

@ -1,250 +0,0 @@
package conn
import (
"encoding/binary"
"net"
"testing"
"golang.org/x/net/ipv6"
)
func TestStdNetBindReceiveFuncAfterClose(t *testing.T) {
bind := NewStdNetBind().(*StdNetBind)
fns, _, err := bind.Open(0)
if err != nil {
t.Fatal(err)
}
bind.Close()
bufs := make([][]byte, 1)
bufs[0] = make([]byte, 1)
sizes := make([]int, 1)
eps := make([]Endpoint, 1)
for _, fn := range fns {
// The ReceiveFuncs must not access conn-related fields on StdNetBind
// unguarded. Close() nils the conn-related fields resulting in a panic
// if they violate the mutex.
fn(bufs, sizes, eps)
}
}
func mockSetGSOSize(control *[]byte, gsoSize uint16) {
*control = (*control)[:cap(*control)]
binary.LittleEndian.PutUint16(*control, gsoSize)
}
func Test_coalesceMessages(t *testing.T) {
cases := []struct {
name string
buffs [][]byte
wantLens []int
wantGSO []int
}{
{
name: "one message no coalesce",
buffs: [][]byte{
make([]byte, 1, 1),
},
wantLens: []int{1},
wantGSO: []int{0},
},
{
name: "two messages equal len coalesce",
buffs: [][]byte{
make([]byte, 1, 2),
make([]byte, 1, 1),
},
wantLens: []int{2},
wantGSO: []int{1},
},
{
name: "two messages unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
},
wantLens: []int{3},
wantGSO: []int{2},
},
{
name: "three messages second unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
make([]byte, 2, 2),
},
wantLens: []int{3, 2},
wantGSO: []int{2, 0},
},
{
name: "three messages limited cap coalesce",
buffs: [][]byte{
make([]byte, 2, 4),
make([]byte, 2, 2),
make([]byte, 2, 2),
},
wantLens: []int{4, 2},
wantGSO: []int{2, 0},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
addr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1").To4(),
Port: 1,
}
msgs := make([]ipv6.Message, len(tt.buffs))
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0, 2)
}
got := coalesceMessages(addr, &StdNetEndpoint{AddrPort: addr.AddrPort()}, tt.buffs, msgs, mockSetGSOSize)
if got != len(tt.wantLens) {
t.Fatalf("got len %d want: %d", got, len(tt.wantLens))
}
for i := 0; i < got; i++ {
if msgs[i].Addr != addr {
t.Errorf("msgs[%d].Addr != passed addr", i)
}
gotLen := len(msgs[i].Buffers[0])
if gotLen != tt.wantLens[i] {
t.Errorf("len(msgs[%d].Buffers[0]) %d != %d", i, gotLen, tt.wantLens[i])
}
gotGSO, err := mockGetGSOSize(msgs[i].OOB)
if err != nil {
t.Fatalf("msgs[%d] getGSOSize err: %v", i, err)
}
if gotGSO != tt.wantGSO[i] {
t.Errorf("msgs[%d] gsoSize %d != %d", i, gotGSO, tt.wantGSO[i])
}
}
})
}
}
func mockGetGSOSize(control []byte) (int, error) {
if len(control) < 2 {
return 0, nil
}
return int(binary.LittleEndian.Uint16(control)), nil
}
func Test_splitCoalescedMessages(t *testing.T) {
newMsg := func(n, gso int) ipv6.Message {
msg := ipv6.Message{
Buffers: [][]byte{make([]byte, 1<<16-1)},
N: n,
OOB: make([]byte, 2),
}
binary.LittleEndian.PutUint16(msg.OOB, uint16(gso))
if gso > 0 {
msg.NN = 2
}
return msg
}
cases := []struct {
name string
msgs []ipv6.Message
firstMsgAt int
wantNumEval int
wantMsgLens []int
wantErr bool
}{
{
name: "second last split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(3, 1),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 3,
wantMsgLens: []int{1, 1, 1, 0},
wantErr: false,
},
{
name: "second last no split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 1,
wantMsgLens: []int{1, 0, 0, 0},
wantErr: false,
},
{
name: "second last no split last no split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(1, 0),
},
firstMsgAt: 2,
wantNumEval: 2,
wantMsgLens: []int{1, 1, 0, 0},
wantErr: false,
},
{
name: "second last no split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(3, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(2, 1),
newMsg(2, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last no split last split overflow",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(4, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := splitCoalescedMessages(tt.msgs, 2, mockGetGSOSize)
if err != nil && !tt.wantErr {
t.Fatalf("err: %v", err)
}
if got != tt.wantNumEval {
t.Fatalf("got to eval: %d want: %d", got, tt.wantNumEval)
}
for i, msg := range tt.msgs {
if msg.N != tt.wantMsgLens[i] {
t.Fatalf("msg[%d].N: %d want: %d", i, msg.N, tt.wantMsgLens[i])
}
}
})
}
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
@ -15,9 +15,12 @@ import (
"sync/atomic" "sync/atomic"
"unsafe" "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/wireguard-go/conn/winrio"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/conn/winrio"
) )
const ( const (
@ -72,18 +75,26 @@ type afWinRingBind struct {
// WinRingBind uses Windows registered I/O for fast ring buffered networking. // WinRingBind uses Windows registered I/O for fast ring buffered networking.
type WinRingBind struct { type WinRingBind struct {
externalControl control.Func
reservedForEndpoint map[WinRingEndpoint][3]uint8
v4, v6 afWinRingBind v4, v6 afWinRingBind
mu sync.RWMutex mu sync.RWMutex
isOpen atomic.Uint32 // 0, 1, or 2 isOpen atomic.Uint32 // 0, 1, or 2
} }
func NewDefaultBind() Bind { return NewWinRingBind() } func NewDefaultBind(externalControl control.Func) Bind {
return NewWinRingBind(externalControl)
}
func NewWinRingBind() Bind { func NewWinRingBind(externalControl control.Func) Bind {
if !winrio.Initialize() { if !winrio.Initialize() {
return NewStdNetBind() return NewStdNetBind(externalControl)
}
return &WinRingBind{
externalControl: externalControl,
reservedForEndpoint: make(map[WinRingEndpoint][3]uint8),
} }
return new(WinRingBind)
} }
type WinRingEndpoint struct { type WinRingEndpoint struct {
@ -239,7 +250,7 @@ func (ring *ringBuffer) Open() error {
return nil return nil
} }
func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sockaddr, error) { func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr, externalControl control.Func) (windows.Sockaddr, error) {
var err error var err error
bind.sock, err = winrio.Socket(family, windows.SOCK_DGRAM, windows.IPPROTO_UDP) bind.sock, err = winrio.Socket(family, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
if err != nil { if err != nil {
@ -257,6 +268,19 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock
if err != nil { if err != nil {
return nil, err return nil, err
} }
var network string
switch family {
case windows.AF_INET:
network = "udp4"
case windows.AF_INET6:
network = "udp6"
}
if externalControl != nil {
err = externalControl(network, M.AddrPortFromSockaddr(sa).String(), &fakeRawConn{bind.sock})
if err != nil {
return nil, err
}
}
err = windows.Bind(bind.sock, sa) err = windows.Bind(bind.sock, sa)
if err != nil { if err != nil {
return nil, err return nil, err
@ -268,6 +292,23 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock
return sa, nil return sa, nil
} }
type fakeRawConn struct {
socket windows.Handle
}
func (c *fakeRawConn) Control(f func(fd uintptr)) error {
f(uintptr(c.socket))
return nil
}
func (c *fakeRawConn) Read(f func(fd uintptr) (done bool)) error {
panic("not implemented")
}
func (c *fakeRawConn) Write(f func(fd uintptr) (done bool)) error {
panic("not implemented")
}
func (bind *WinRingBind) Open(port uint16) (recvFns []ReceiveFunc, selectedPort uint16, err error) { func (bind *WinRingBind) Open(port uint16) (recvFns []ReceiveFunc, selectedPort uint16, err error) {
bind.mu.Lock() bind.mu.Lock()
defer bind.mu.Unlock() defer bind.mu.Unlock()
@ -280,11 +321,11 @@ func (bind *WinRingBind) Open(port uint16) (recvFns []ReceiveFunc, selectedPort
return nil, 0, ErrBindAlreadyOpen return nil, 0, ErrBindAlreadyOpen
} }
var sa windows.Sockaddr var sa windows.Sockaddr
sa, err = bind.v4.Open(windows.AF_INET, &windows.SockaddrInet4{Port: int(port)}) sa, err = bind.v4.Open(windows.AF_INET, &windows.SockaddrInet4{Port: int(port)}, bind.externalControl)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
sa, err = bind.v6.Open(windows.AF_INET6, &windows.SockaddrInet6{Port: sa.(*windows.SockaddrInet4).Port}) sa, err = bind.v6.Open(windows.AF_INET6, &windows.SockaddrInet6{Port: sa.(*windows.SockaddrInet4).Port}, bind.externalControl)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -420,6 +461,9 @@ func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint)
bind.mu.RLock() bind.mu.RLock()
defer bind.mu.RUnlock() defer bind.mu.RUnlock()
n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen) n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen)
if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
common.ClearArray(bufs[0][1:4])
}
sizes[0] = n sizes[0] = n
eps[0] = ep eps[0] = ep
return 1, err return 1, err
@ -429,6 +473,9 @@ func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint)
bind.mu.RLock() bind.mu.RLock()
defer bind.mu.RUnlock() defer bind.mu.RUnlock()
n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen) n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen)
if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
common.ClearArray(bufs[0][1:4])
}
sizes[0] = n sizes[0] = n
eps[0] = ep eps[0] = ep
return 1, err return 1, err
@ -486,7 +533,7 @@ func (bind *afWinRingBind) Send(buf []byte, nend *WinRingEndpoint, isOpen *atomi
return winrio.SendEx(bind.rq, dataBuffer, 1, nil, addressBuffer, nil, nil, 0, 0) return winrio.SendEx(bind.rq, dataBuffer, 1, nil, addressBuffer, nil, nil, 0, 0)
} }
func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint) error { func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error {
nend, ok := endpoint.(*WinRingEndpoint) nend, ok := endpoint.(*WinRingEndpoint)
if !ok { if !ok {
return ErrWrongEndpointType return ErrWrongEndpointType
@ -494,6 +541,13 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint) error {
bind.mu.RLock() bind.mu.RLock()
defer bind.mu.RUnlock() defer bind.mu.RUnlock()
for _, buf := range bufs { for _, buf := range bufs {
buf = buf[offset:]
if len(buf) > 3 {
reserved, loaded := bind.reservedForEndpoint[*endpoint.(*WinRingEndpoint)]
if loaded {
copy(buf[1:4], reserved[:])
}
}
switch nend.family { switch nend.family {
case windows.AF_INET: case windows.AF_INET:
if bind.v4.blackhole { if bind.v4.blackhole {
@ -514,6 +568,26 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint) error {
return nil return nil
} }
func (bind *WinRingBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) {
endpoint, err := bind.ParseEndpoint(destination.String())
if err != nil {
panic(E.Cause(err, "parse destination as WinRingEndpoint"))
}
bind.reservedForEndpoint[*endpoint.(*WinRingEndpoint)] = reserved
}
// lx: hasReserved reports whether any Cloudflare "reserved" value is set. See
// the StdNetBind.hasReserved comment — the unconditional receive clear would
// corrupt an AmneziaWG magic header sitting in bytes 1-3 (small padding).
func (bind *WinRingBind) hasReserved() bool {
for _, reserved := range bind.reservedForEndpoint {
if reserved != [3]uint8{} {
return true
}
}
return false
}
func (s *StdNetBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error { func (s *StdNetBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()

View file

@ -1,136 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package bindtest
import (
"fmt"
"math/rand"
"net"
"net/netip"
"os"
"golang.zx2c4.com/wireguard/conn"
)
type ChannelBind struct {
rx4, tx4 *chan []byte
rx6, tx6 *chan []byte
closeSignal chan bool
source4, source6 ChannelEndpoint
target4, target6 ChannelEndpoint
}
type ChannelEndpoint uint16
var (
_ conn.Bind = (*ChannelBind)(nil)
_ conn.Endpoint = (*ChannelEndpoint)(nil)
)
func NewChannelBinds() [2]conn.Bind {
arx4 := make(chan []byte, 8192)
brx4 := make(chan []byte, 8192)
arx6 := make(chan []byte, 8192)
brx6 := make(chan []byte, 8192)
var binds [2]ChannelBind
binds[0].rx4 = &arx4
binds[0].tx4 = &brx4
binds[1].rx4 = &brx4
binds[1].tx4 = &arx4
binds[0].rx6 = &arx6
binds[0].tx6 = &brx6
binds[1].rx6 = &brx6
binds[1].tx6 = &arx6
binds[0].target4 = ChannelEndpoint(1)
binds[1].target4 = ChannelEndpoint(2)
binds[0].target6 = ChannelEndpoint(3)
binds[1].target6 = ChannelEndpoint(4)
binds[0].source4 = binds[1].target4
binds[0].source6 = binds[1].target6
binds[1].source4 = binds[0].target4
binds[1].source6 = binds[0].target6
return [2]conn.Bind{&binds[0], &binds[1]}
}
func (c ChannelEndpoint) ClearSrc() {}
func (c ChannelEndpoint) SrcToString() string { return "" }
func (c ChannelEndpoint) DstToString() string { return fmt.Sprintf("127.0.0.1:%d", c) }
func (c ChannelEndpoint) DstToBytes() []byte { return []byte{byte(c)} }
func (c ChannelEndpoint) DstIP() netip.Addr { return netip.AddrFrom4([4]byte{127, 0, 0, 1}) }
func (c ChannelEndpoint) SrcIP() netip.Addr { return netip.Addr{} }
func (c *ChannelBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
c.closeSignal = make(chan bool)
fns = append(fns, c.makeReceiveFunc(*c.rx4))
fns = append(fns, c.makeReceiveFunc(*c.rx6))
if rand.Uint32()&1 == 0 {
return fns, uint16(c.source4), nil
} else {
return fns, uint16(c.source6), nil
}
}
func (c *ChannelBind) Close() error {
if c.closeSignal != nil {
select {
case <-c.closeSignal:
default:
close(c.closeSignal)
}
}
return nil
}
func (c *ChannelBind) BatchSize() int { return 1 }
func (c *ChannelBind) SetMark(mark uint32) error { return nil }
func (c *ChannelBind) makeReceiveFunc(ch chan []byte) conn.ReceiveFunc {
return func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) {
select {
case <-c.closeSignal:
return 0, net.ErrClosed
case rx := <-ch:
copied := copy(bufs[0], rx)
sizes[0] = copied
eps[0] = c.target6
return 1, nil
}
}
}
func (c *ChannelBind) Send(bufs [][]byte, ep conn.Endpoint) error {
for _, b := range bufs {
select {
case <-c.closeSignal:
return net.ErrClosed
default:
bc := make([]byte, len(b))
copy(bc, b)
if ep.(ChannelEndpoint) == c.target4 {
*c.tx4 <- bc
} else if ep.(ChannelEndpoint) == c.target6 {
*c.tx6 <- bc
} else {
return os.ErrInvalid
}
}
}
return nil
}
func (c *ChannelBind) ParseEndpoint(s string) (conn.Endpoint, error) {
addr, err := netip.ParseAddrPort(s)
if err != nil {
return nil, err
}
return ChannelEndpoint(addr.Port()), nil
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
// Package conn implements WireGuard's network connections. // Package conn implements WireGuard's network connections.
@ -45,9 +45,11 @@ type Bind interface {
// This mark is passed to the kernel as the socket option SO_MARK. // This mark is passed to the kernel as the socket option SO_MARK.
SetMark(mark uint32) error SetMark(mark uint32) error
// Send writes one or more packets in bufs to address ep. The length of // Send writes one or more packets in bufs to address ep. A nonzero offset
// bufs must not exceed BatchSize(). // can be used to instruct the Bind on where packet data begins in each
Send(bufs [][]byte, ep Endpoint) error // element of the bufs slice. Space preceding offset is free to use for
// additional encapsulation. The length of bufs must not exceed BatchSize().
Send(bufs [][]byte, ep Endpoint, offset int) error
// ParseEndpoint creates a new endpoint from a string. // ParseEndpoint creates a new endpoint from a string.
ParseEndpoint(s string) (Endpoint, error) ParseEndpoint(s string) (Endpoint, error)
@ -55,6 +57,8 @@ type Bind interface {
// BatchSize is the number of buffers expected to be passed to // BatchSize is the number of buffers expected to be passed to
// the ReceiveFuncs, and the maximum expected to be passed to SendBatch. // the ReceiveFuncs, and the maximum expected to be passed to SendBatch.
BatchSize() int BatchSize() int
SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte)
} }
// BindSocketToInterface is implemented by Bind objects that support being // BindSocketToInterface is implemented by Bind objects that support being
@ -84,6 +88,40 @@ type Endpoint interface {
SrcIP() netip.Addr SrcIP() netip.Addr
} }
// InitiationAwareEndpoint is an optional [Endpoint] specialization for
// integrations that want to know when a WireGuard handshake initiation
// message has been received, enabling just-in-time peer configuration before
// attempted decryption.
//
// It's most useful when used in combination with [PeerAwareEndpoint], enabling
// JIT peer configuration and post-decryption peer verification from a single
// implementer.
type InitiationAwareEndpoint interface {
// InitiationMessagePublicKey is called when a handshake initiation message
// has been received, and the sender's public key has been identified, but
// BEFORE an attempt has been made to verify it.
InitiationMessagePublicKey(peerPublicKey [32]byte)
}
// PeerAwareEndpoint is an optional Endpoint specialization for
// integrations that want to know about the outcome of Cryptokey Routing
// identification.
//
// If they receive a packet from a source they had not pre-identified,
// to learn the identification WireGuard can derive from the session
// or handshake.
//
// A [PeerAwareEndpoint] may be installed as the [conn.Endpoint] following
// successful decryption unless endpoint roaming has been disabled for
// the peer.
type PeerAwareEndpoint interface {
// FromPeer is called at least once per successfully Cryptokey Routing ID'd
// [ReceiveFunc] packets batch for a given node key. wireguard-go will
// always call it for the latest/tail packet in the batch, only ever
// suppressing calls for older packets.
FromPeer(peerPublicKey [32]byte)
}
var ( var (
ErrBindAlreadyOpen = errors.New("bind is already open") ErrBindAlreadyOpen = errors.New("bind is already open")
ErrWrongEndpointType = errors.New("endpoint type does not correspond with bind type") ErrWrongEndpointType = errors.New("endpoint type does not correspond with bind type")

View file

@ -1,24 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package conn
import (
"testing"
)
func TestPrettyName(t *testing.T) {
var (
recvFunc ReceiveFunc = func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) { return }
)
const want = "TestPrettyName"
t.Run("ReceiveFunc.PrettyName", func(t *testing.T) {
if got := recvFunc.PrettyName(); got != want {
t.Errorf("PrettyName() = %v, want %v", got, want)
}
})
}

View file

@ -1,13 +1,12 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
import ( import (
"net" "github.com/sagernet/sing/common/control"
"syscall"
) )
// UDP socket read/write buffer size (7MB). The value of 7MB is chosen as it is // UDP socket read/write buffer size (7MB). The value of 7MB is chosen as it is
@ -17,27 +16,6 @@ import (
// around this limitation) // around this limitation)
const socketBufferSize = 7 << 20 const socketBufferSize = 7 << 20
// controlFn is the callback function signature from net.ListenConfig.Control.
// It is used to apply platform specific configuration to the socket prior to
// bind.
type controlFn func(network, address string, c syscall.RawConn) error
// controlFns is a list of functions that are called from the listen config // controlFns is a list of functions that are called from the listen config
// that can apply socket options. // that can apply socket options.
var controlFns = []controlFn{} var controlFns []control.Func
// listenConfig returns a net.ListenConfig that applies the controlFns to the
// socket prior to bind. This is used to apply socket buffer sizing and packet
// information OOB configuration for sticky sockets.
func listenConfig() *net.ListenConfig {
return &net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
for _, fn := range controlFns {
if err := fn(network, address, c); err != nil {
return err
}
}
return nil
},
}
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
@ -13,6 +13,35 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// Taken from go/src/internal/syscall/unix/kernel_version_linux.go
func kernelVersion() (major, minor int) {
var uname unix.Utsname
if err := unix.Uname(&uname); err != nil {
return
}
var (
values [2]int
value, vi int
)
for _, c := range uname.Release {
if '0' <= c && c <= '9' {
value = (value * 10) + int(c-'0')
} else {
// Note that we're assuming N.N.N here.
// If we see anything else, we are likely to mis-parse it.
values[vi] = value
vi++
if vi >= len(values) {
break
}
value = 0
}
}
return values[0], values[1]
}
func init() { func init() {
controlFns = append(controlFns, controlFns = append(controlFns,
@ -60,6 +89,17 @@ func init() {
// Attempt to enable UDP_GRO // Attempt to enable UDP_GRO
func(network, address string, c syscall.RawConn) error { func(network, address string, c syscall.RawConn) error {
// Kernels below 5.12 are missing 98184612aca0 ("net:
// udp: Add support for getsockopt(..., ..., UDP_GRO,
// ..., ...);"), which means we can't read this back
// later. We could pipe the return value through to
// the rest of the code, but UDP_GRO is kind of buggy
// anyway, so just gate this here.
major, minor := kernelVersion()
if major < 5 || (major == 5 && minor < 12) {
return nil
}
c.Control(func(fd uintptr) { c.Control(func(fd uintptr) {
_ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
}) })

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,9 +2,13 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
func NewDefaultBind() Bind { return NewStdNetBind() } import "github.com/sagernet/sing/common/control"
func NewDefaultBind(externalControl control.Func) Bind {
return NewStdNetBind(externalControl)
}

View file

@ -2,11 +2,11 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
func errShouldDisableUDPGSO(err error) bool { func errShouldDisableUDPGSO(_ error) bool {
return false return false
} }

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -3,13 +3,13 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn
import "net" import "net"
func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { func supportsUDPOffload(_ *net.UDPConn) (txOffload, rxOffload bool) {
return return
} }

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

325
conn/msgx_darwin.go Normal file
View file

@ -0,0 +1,325 @@
// On iOS both directions misbehave in the Network Extension (recvmsg_x on
// unconnected UDP sockets delivers no data, connected sockets stop passing
// traffic after a rebind), so msgx is macOS only until it can be debugged
// on a device.
//go:build darwin && !ios
package conn
import (
"net"
"net/netip"
"sync"
"sync/atomic"
"syscall"
"unsafe"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/ipv6"
"golang.org/x/sys/unix"
)
const supportsMsgX = true
const msgXBatchSize = IdealBatchSize
// msghdrX mirrors XNU's struct msghdr_x used by sendmsg_x/recvmsg_x.
// Per bsd/sys/socket_private.h, sendmsg_x supports neither addresses nor
// ancillary data (msg_name and msg_control must be zero), so batched sends
// require a connected socket. recvmsg_x does fill in per-message source
// addresses (copyout_maddr in uipc_syscalls.c). utun cannot use the send
// side at all (no ctl_send_list in if_utun.c).
type msghdrX struct {
Msg unix.Msghdr
DataLen uint32
}
type msgXState struct {
singlePeer atomic.Bool
disabled atomic.Bool // permanent fallback to the generic paths
connected4 atomic.Bool
connected6 atomic.Bool
endpoint atomic.Pointer[StdNetEndpoint]
connectLock sync.Mutex
}
// reset clears per-socket state; must be called when the bind (re)opens,
// as the connected state belongs to the previous sockets.
func (m *msgXState) reset() {
m.disabled.Store(false)
m.connected4.Store(false)
m.connected6.Store(false)
m.endpoint.Store(nil)
}
func (m *msgXState) connectedFlag(isV6 bool) *atomic.Bool {
if isV6 {
return &m.connected6
}
return &m.connected4
}
// SetSinglePeerMode enables connected-socket sendmsg_x batching. Only safe
// when the bind serves exactly one peer with a fixed endpoint: the kernel
// will drop datagrams from any other source, so peer roaming stops working.
func (s *StdNetBind) SetSinglePeerMode() {
s.msgx.singlePeer.Store(true)
}
func sockaddrFromAddrPort(addrPort netip.AddrPort, storage4 *unix.RawSockaddrInet4, storage6 *unix.RawSockaddrInet6) (unsafe.Pointer, uint32) {
port := addrPort.Port()<<8 | addrPort.Port()>>8
if addrPort.Addr().Unmap().Is4() {
*storage4 = unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Port: port,
Addr: addrPort.Addr().Unmap().As4(),
}
return unsafe.Pointer(storage4), unix.SizeofSockaddrInet4
}
*storage6 = unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Port: port,
Addr: addrPort.Addr().As16(),
}
return unsafe.Pointer(storage6), unix.SizeofSockaddrInet6
}
// ensureConnected connects the family socket to the single peer on first
// use, and permanently falls back if a second endpoint shows up.
func (s *StdNetBind) ensureConnected(rawConn syscall.RawConn, isV6 bool, destination netip.AddrPort) bool {
if s.msgx.disabled.Load() || !s.msgx.singlePeer.Load() {
return false
}
connected := s.msgx.connectedFlag(isV6)
if connected.Load() {
if s.msgx.endpoint.Load().AddrPort == destination {
return true
}
s.msgx.connectLock.Lock()
defer s.msgx.connectLock.Unlock()
if s.msgx.disabled.Load() {
return false
}
s.msgx.disabled.Store(true)
var disconnectErr error
controlErr := rawConn.Control(func(fd uintptr) {
addr := unix.RawSockaddrAny{}
addr.Addr.Family = unix.AF_UNSPEC
//nolint:staticcheck
_, _, errno := unix.Syscall(unix.SYS_CONNECT, fd, uintptr(unsafe.Pointer(&addr)), unix.SizeofSockaddrAny)
if errno != 0 && errno != unix.EAFNOSUPPORT {
disconnectErr = errno
}
})
if controlErr == nil && disconnectErr == nil {
connected.Store(false)
}
return false
}
s.msgx.connectLock.Lock()
defer s.msgx.connectLock.Unlock()
if s.msgx.disabled.Load() {
return false
}
if connected.Load() {
return s.msgx.endpoint.Load().AddrPort == destination
}
var (
storage4 unix.RawSockaddrInet4
storage6 unix.RawSockaddrInet6
connectErr unix.Errno
)
name, nameLen := sockaddrFromAddrPort(destination, &storage4, &storage6)
controlErr := rawConn.Control(func(fd uintptr) {
//nolint:staticcheck
_, _, connectErr = unix.Syscall(unix.SYS_CONNECT, fd, uintptr(name), uintptr(nameLen))
})
if controlErr != nil || connectErr != 0 {
s.msgx.disabled.Store(true)
return false
}
s.msgx.endpoint.Store(&StdNetEndpoint{AddrPort: destination})
connected.Store(true)
return true
}
type sendMsgXState struct {
hdrs []msghdrX
iovs []unix.Iovec
}
var sendMsgXPool = sync.Pool{New: func() any {
return &sendMsgXState{
hdrs: make([]msghdrX, IdealBatchSize),
iovs: make([]unix.Iovec, IdealBatchSize),
}
}}
// sendMsgX sends msgs via sendmsg_x when the socket is connected to their
// endpoint. handled == false means nothing was sent and the caller must use
// the generic path; msgs are never partially consumed in that case.
func (s *StdNetBind) sendMsgX(conn *net.UDPConn, msgs []ipv6.Message) (bool, error) {
var (
rawConn syscall.RawConn
isV6 bool
)
s.mu.Lock()
if conn == s.ipv6 {
rawConn = s.ipv6RC
isV6 = true
} else {
rawConn = s.ipv4RC
}
s.mu.Unlock()
if rawConn == nil {
return false, nil
}
destination := M.AddrPortFromNet(msgs[0].Addr)
if !s.ensureConnected(rawConn, isV6, destination) {
return false, nil
}
state := sendMsgXPool.Get().(*sendMsgXState)
defer sendMsgXPool.Put(state)
for i := range msgs {
buffer := msgs[i].Buffers[0]
state.iovs[i] = unix.Iovec{Base: &buffer[0]}
state.iovs[i].SetLen(len(buffer))
state.hdrs[i] = msghdrX{}
state.hdrs[i].Msg.Iov = &state.iovs[i]
state.hdrs[i].Msg.Iovlen = 1
}
var sent int
for sent < len(msgs) {
var (
n uintptr
errno unix.Errno
)
writeErr := rawConn.Write(func(fd uintptr) bool {
//nolint:staticcheck
n, _, errno = unix.RawSyscall6(unix.SYS_SENDMSG_X, fd,
uintptr(unsafe.Pointer(&state.hdrs[sent])), uintptr(len(msgs)-sent), unix.MSG_DONTWAIT, 0, 0)
return errno != unix.EAGAIN
})
if writeErr != nil {
return true, writeErr
}
if errno != 0 {
if sent == 0 {
// The syscall is refusing this socket entirely (sandbox,
// disconnected by the system, ...): disable and let the
// caller resend everything on the generic path.
s.msgx.disabled.Store(true)
return false, nil
}
return true, errno
}
sent += int(n)
}
return true, nil
}
type receiveMsgXState struct {
hdrs []msghdrX
iovs []unix.Iovec
names []unix.RawSockaddrInet6
fallback bool
}
func (s *StdNetBind) receiveSingle(conn *net.UDPConn, bufs [][]byte, sizes []int, eps []Endpoint) (int, error) {
n, _, _, addr, err := conn.ReadMsgUDPAddrPort(bufs[0], nil)
if err != nil {
return 0, err
}
sizes[0] = n
if n > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
bufs[0][1] = 0
bufs[0][2] = 0
bufs[0][3] = 0
}
eps[0] = &StdNetEndpoint{AddrPort: addr}
return 1, nil
}
func (s *StdNetBind) makeReceiveMsgX(conn *net.UDPConn, isV6 bool) (ReceiveFunc, error) {
rawConn, err := conn.SyscallConn()
if err != nil {
return nil, err
}
state := &receiveMsgXState{
hdrs: make([]msghdrX, msgXBatchSize),
iovs: make([]unix.Iovec, msgXBatchSize),
names: make([]unix.RawSockaddrInet6, msgXBatchSize),
}
return func(bufs [][]byte, sizes []int, eps []Endpoint) (int, error) {
if state.fallback || s.msgx.disabled.Load() {
return s.receiveSingle(conn, bufs, sizes, eps)
}
connectedEndpoint := s.msgx.endpoint.Load()
if !s.msgx.connectedFlag(isV6).Load() {
connectedEndpoint = nil
}
count := len(bufs)
if count > msgXBatchSize {
count = msgXBatchSize
}
for i := 0; i < count; i++ {
state.iovs[i] = unix.Iovec{Base: &bufs[i][0]}
state.iovs[i].SetLen(len(bufs[i]))
state.hdrs[i] = msghdrX{}
if connectedEndpoint == nil {
state.hdrs[i].Msg.Name = (*byte)(unsafe.Pointer(&state.names[i]))
state.hdrs[i].Msg.Namelen = unix.SizeofSockaddrInet6
}
state.hdrs[i].Msg.Iov = &state.iovs[i]
state.hdrs[i].Msg.Iovlen = 1
}
var (
n uintptr
errno unix.Errno
)
readErr := rawConn.Read(func(fd uintptr) bool {
//nolint:staticcheck
n, _, errno = unix.RawSyscall6(unix.SYS_RECVMSG_X, fd,
uintptr(unsafe.Pointer(&state.hdrs[0])), uintptr(count), unix.MSG_DONTWAIT, 0, 0)
return errno != unix.EAGAIN
})
if readErr != nil {
return 0, readErr
}
if errno != 0 {
// recvmsg_x is refusing this socket (sandbox, protocol, ...):
// serve this and all future calls with a plain single read so
// the receive routine keeps running.
state.fallback = true
return s.receiveSingle(conn, bufs, sizes, eps)
}
numMsgs := int(n)
for i := 0; i < numMsgs; i++ {
sizes[i] = int(state.hdrs[i].DataLen)
if sizes[i] > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
bufs[i][1] = 0
bufs[i][2] = 0
bufs[i][3] = 0
}
if connectedEndpoint != nil {
eps[i] = connectedEndpoint
continue
}
var addrPort netip.AddrPort
name := &state.names[i]
if name.Family == unix.AF_INET6 {
port := name.Port<<8 | name.Port>>8
addrPort = netip.AddrPortFrom(netip.AddrFrom16(name.Addr).Unmap(), port)
} else {
name4 := (*unix.RawSockaddrInet4)(unsafe.Pointer(name))
port := name4.Port<<8 | name4.Port>>8
addrPort = netip.AddrPortFrom(netip.AddrFrom4(name4.Addr), port)
}
eps[i] = &StdNetEndpoint{AddrPort: addrPort}
}
return numMsgs, nil
}, nil
}

30
conn/msgx_default.go Normal file
View file

@ -0,0 +1,30 @@
//go:build !darwin || ios
package conn
import (
"net"
"golang.org/x/net/ipv6"
)
const supportsMsgX = false
const msgXBatchSize = 1
type msgXState struct{}
func (m *msgXState) reset() {
}
// SetSinglePeerMode is a no-op on platforms without sendmsg_x/recvmsg_x.
func (s *StdNetBind) SetSinglePeerMode() {
}
func (s *StdNetBind) sendMsgX(conn *net.UDPConn, msgs []ipv6.Message) (bool, error) {
return false, nil
}
func (s *StdNetBind) makeReceiveMsgX(conn *net.UDPConn, isV6 bool) (ReceiveFunc, error) {
panic("makeReceiveMsgX is not supported on this platform")
}

View file

@ -0,0 +1,56 @@
/* SPDX-License-Identifier: MIT
*
* lx: unit coverage for the StdNetBind.hasReserved() gate that guards the
* receive-side reserved-clear. receiveIP zeroes bytes 1-3 (Cloudflare WARP
* "reserved") only when a non-zero reserved value is set for some endpoint;
* otherwise an AmneziaWG magic header landing in bytes 1-3 (small s1/s2/s4
* padding) would be corrupted and the packet dropped. This test pins the gate
* itself; the end-to-end handshake proof lives in the device package.
*/
package conn
import (
"net/netip"
"testing"
)
func stdNetBindForTest(t *testing.T) *StdNetBind {
t.Helper()
b, ok := NewStdNetBind(nil).(*StdNetBind)
if !ok {
t.Fatalf("NewStdNetBind did not return *StdNetBind")
}
return b
}
func TestStdNetBindHasReserved(t *testing.T) {
b := stdNetBindForTest(t)
if b.hasReserved() {
t.Fatal("fresh bind must report no reserved value")
}
ep := netip.MustParseAddrPort("127.0.0.1:51820")
// An all-zero reserved value is indistinguishable from "unset" and must
// not arm the clear.
b.SetReservedForEndpoint(ep, [3]byte{0, 0, 0})
if b.hasReserved() {
t.Fatal("all-zero reserved must not count as reserved")
}
// Any non-zero byte (WARP anycast tag) arms the clear.
b.SetReservedForEndpoint(ep, [3]byte{0, 0, 1})
if !b.hasReserved() {
t.Fatal("non-zero reserved (byte 3) must count as reserved")
}
// A second endpoint's non-zero value must also be seen.
b2 := stdNetBindForTest(t)
ep2 := netip.MustParseAddrPort("192.0.2.1:2408")
b2.SetReservedForEndpoint(ep, [3]byte{0, 0, 0})
b2.SetReservedForEndpoint(ep2, [3]byte{0xAB, 0, 0})
if !b2.hasReserved() {
t.Fatal("non-zero reserved on any endpoint must count as reserved")
}
}

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package conn package conn

View file

@ -1,266 +0,0 @@
//go:build linux && !android
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package conn
import (
"context"
"net"
"net/netip"
"runtime"
"testing"
"unsafe"
"golang.org/x/sys/unix"
)
func setSrc(ep *StdNetEndpoint, addr netip.Addr, ifidx int32) {
var buf []byte
if addr.Is4() {
buf = make([]byte, unix.CmsgSpace(unix.SizeofInet4Pktinfo))
hdr := unix.Cmsghdr{
Level: unix.IPPROTO_IP,
Type: unix.IP_PKTINFO,
}
hdr.SetLen(unix.CmsgLen(unix.SizeofInet4Pktinfo))
copy(buf, unsafe.Slice((*byte)(unsafe.Pointer(&hdr)), int(unsafe.Sizeof(hdr))))
info := unix.Inet4Pktinfo{
Ifindex: ifidx,
Spec_dst: addr.As4(),
}
copy(buf[unix.CmsgLen(0):], unsafe.Slice((*byte)(unsafe.Pointer(&info)), unix.SizeofInet4Pktinfo))
} else {
buf = make([]byte, unix.CmsgSpace(unix.SizeofInet6Pktinfo))
hdr := unix.Cmsghdr{
Level: unix.IPPROTO_IPV6,
Type: unix.IPV6_PKTINFO,
}
hdr.SetLen(unix.CmsgLen(unix.SizeofInet6Pktinfo))
copy(buf, unsafe.Slice((*byte)(unsafe.Pointer(&hdr)), int(unsafe.Sizeof(hdr))))
info := unix.Inet6Pktinfo{
Ifindex: uint32(ifidx),
Addr: addr.As16(),
}
copy(buf[unix.CmsgLen(0):], unsafe.Slice((*byte)(unsafe.Pointer(&info)), unix.SizeofInet6Pktinfo))
}
ep.src = buf
}
func Test_setSrcControl(t *testing.T) {
t.Run("IPv4", func(t *testing.T) {
ep := &StdNetEndpoint{
AddrPort: netip.MustParseAddrPort("127.0.0.1:1234"),
}
setSrc(ep, netip.MustParseAddr("127.0.0.1"), 5)
control := make([]byte, stickyControlSize)
setSrcControl(&control, ep)
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
if hdr.Level != unix.IPPROTO_IP {
t.Errorf("unexpected level: %d", hdr.Level)
}
if hdr.Type != unix.IP_PKTINFO {
t.Errorf("unexpected type: %d", hdr.Type)
}
if uint(hdr.Len) != uint(unix.CmsgLen(int(unsafe.Sizeof(unix.Inet4Pktinfo{})))) {
t.Errorf("unexpected length: %d", hdr.Len)
}
info := (*unix.Inet4Pktinfo)(unsafe.Pointer(&control[unix.CmsgLen(0)]))
if info.Spec_dst[0] != 127 || info.Spec_dst[1] != 0 || info.Spec_dst[2] != 0 || info.Spec_dst[3] != 1 {
t.Errorf("unexpected address: %v", info.Spec_dst)
}
if info.Ifindex != 5 {
t.Errorf("unexpected ifindex: %d", info.Ifindex)
}
})
t.Run("IPv6", func(t *testing.T) {
ep := &StdNetEndpoint{
AddrPort: netip.MustParseAddrPort("[::1]:1234"),
}
setSrc(ep, netip.MustParseAddr("::1"), 5)
control := make([]byte, stickyControlSize)
setSrcControl(&control, ep)
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
if hdr.Level != unix.IPPROTO_IPV6 {
t.Errorf("unexpected level: %d", hdr.Level)
}
if hdr.Type != unix.IPV6_PKTINFO {
t.Errorf("unexpected type: %d", hdr.Type)
}
if uint(hdr.Len) != uint(unix.CmsgLen(int(unsafe.Sizeof(unix.Inet6Pktinfo{})))) {
t.Errorf("unexpected length: %d", hdr.Len)
}
info := (*unix.Inet6Pktinfo)(unsafe.Pointer(&control[unix.CmsgLen(0)]))
if info.Addr != ep.SrcIP().As16() {
t.Errorf("unexpected address: %v", info.Addr)
}
if info.Ifindex != 5 {
t.Errorf("unexpected ifindex: %d", info.Ifindex)
}
})
t.Run("ClearOnNoSrc", func(t *testing.T) {
control := make([]byte, stickyControlSize)
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
hdr.Level = 1
hdr.Type = 2
hdr.Len = 3
setSrcControl(&control, &StdNetEndpoint{})
if len(control) != 0 {
t.Errorf("unexpected control: %v", control)
}
})
}
func Test_getSrcFromControl(t *testing.T) {
t.Run("IPv4", func(t *testing.T) {
control := make([]byte, stickyControlSize)
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
hdr.Level = unix.IPPROTO_IP
hdr.Type = unix.IP_PKTINFO
hdr.SetLen(unix.CmsgLen(int(unsafe.Sizeof(unix.Inet4Pktinfo{}))))
info := (*unix.Inet4Pktinfo)(unsafe.Pointer(&control[unix.CmsgLen(0)]))
info.Spec_dst = [4]byte{127, 0, 0, 1}
info.Ifindex = 5
ep := &StdNetEndpoint{}
getSrcFromControl(control, ep)
if ep.SrcIP() != netip.MustParseAddr("127.0.0.1") {
t.Errorf("unexpected address: %v", ep.SrcIP())
}
if ep.SrcIfidx() != 5 {
t.Errorf("unexpected ifindex: %d", ep.SrcIfidx())
}
})
t.Run("IPv6", func(t *testing.T) {
control := make([]byte, stickyControlSize)
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
hdr.Level = unix.IPPROTO_IPV6
hdr.Type = unix.IPV6_PKTINFO
hdr.SetLen(unix.CmsgLen(int(unsafe.Sizeof(unix.Inet6Pktinfo{}))))
info := (*unix.Inet6Pktinfo)(unsafe.Pointer(&control[unix.CmsgLen(0)]))
info.Addr = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
info.Ifindex = 5
ep := &StdNetEndpoint{}
getSrcFromControl(control, ep)
if ep.SrcIP() != netip.MustParseAddr("::1") {
t.Errorf("unexpected address: %v", ep.SrcIP())
}
if ep.SrcIfidx() != 5 {
t.Errorf("unexpected ifindex: %d", ep.SrcIfidx())
}
})
t.Run("ClearOnEmpty", func(t *testing.T) {
var control []byte
ep := &StdNetEndpoint{}
setSrc(ep, netip.MustParseAddr("::1"), 5)
getSrcFromControl(control, ep)
if ep.SrcIP().IsValid() {
t.Errorf("unexpected address: %v", ep.SrcIP())
}
if ep.SrcIfidx() != 0 {
t.Errorf("unexpected ifindex: %d", ep.SrcIfidx())
}
})
t.Run("Multiple", func(t *testing.T) {
zeroControl := make([]byte, unix.CmsgSpace(0))
zeroHdr := (*unix.Cmsghdr)(unsafe.Pointer(&zeroControl[0]))
zeroHdr.SetLen(unix.CmsgLen(0))
control := make([]byte, unix.CmsgSpace(unix.SizeofInet4Pktinfo))
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
hdr.Level = unix.IPPROTO_IP
hdr.Type = unix.IP_PKTINFO
hdr.SetLen(unix.CmsgLen(int(unsafe.Sizeof(unix.Inet4Pktinfo{}))))
info := (*unix.Inet4Pktinfo)(unsafe.Pointer(&control[unix.CmsgLen(0)]))
info.Spec_dst = [4]byte{127, 0, 0, 1}
info.Ifindex = 5
combined := make([]byte, 0)
combined = append(combined, zeroControl...)
combined = append(combined, control...)
ep := &StdNetEndpoint{}
getSrcFromControl(combined, ep)
if ep.SrcIP() != netip.MustParseAddr("127.0.0.1") {
t.Errorf("unexpected address: %v", ep.SrcIP())
}
if ep.SrcIfidx() != 5 {
t.Errorf("unexpected ifindex: %d", ep.SrcIfidx())
}
})
}
func Test_listenConfig(t *testing.T) {
t.Run("IPv4", func(t *testing.T) {
conn, err := listenConfig().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
sc, err := conn.(*net.UDPConn).SyscallConn()
if err != nil {
t.Fatal(err)
}
if runtime.GOOS == "linux" {
var i int
sc.Control(func(fd uintptr) {
i, err = unix.GetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_PKTINFO)
})
if err != nil {
t.Fatal(err)
}
if i != 1 {
t.Error("IP_PKTINFO not set!")
}
} else {
t.Logf("listenConfig() does not set IPV6_RECVPKTINFO on %s", runtime.GOOS)
}
})
t.Run("IPv6", func(t *testing.T) {
conn, err := listenConfig().ListenPacket(context.Background(), "udp6", ":0")
if err != nil {
t.Fatal(err)
}
sc, err := conn.(*net.UDPConn).SyscallConn()
if err != nil {
t.Fatal(err)
}
if runtime.GOOS == "linux" {
var i int
sc.Control(func(fd uintptr) {
i, err = unix.GetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO)
})
if err != nil {
t.Fatal(err)
}
if i != 1 {
t.Error("IPV6_PKTINFO not set!")
}
} else {
t.Logf("listenConfig() does not set IPV6_RECVPKTINFO on %s", runtime.GOOS)
}
})
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package winrio package winrio

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -55,6 +55,25 @@ func commonBits(ip1, ip2 []byte) uint8 {
} }
} }
func commonBits4(ip1 []byte, ip2 [4]byte) uint8 {
a := binary.BigEndian.Uint32(ip1)
b := binary.BigEndian.Uint32(ip2[:])
return uint8(bits.LeadingZeros32(a ^ b))
}
func commonBits6(ip1 []byte, ip2 [16]byte) uint8 {
a := binary.BigEndian.Uint64(ip1)
b := binary.BigEndian.Uint64(ip2[:])
x := a ^ b
if x != 0 {
return uint8(bits.LeadingZeros64(x))
}
a = binary.BigEndian.Uint64(ip1[8:])
b = binary.BigEndian.Uint64(ip2[8:])
x = a ^ b
return 64 + uint8(bits.LeadingZeros64(x))
}
func (node *trieEntry) addToPeerEntries() { func (node *trieEntry) addToPeerEntries() {
node.perPeerElem = node.peer.trieEntries.PushBack(node) node.perPeerElem = node.peer.trieEntries.PushBack(node)
} }
@ -188,7 +207,37 @@ func (trie parentIndirection) insert(ip []byte, cidr uint8, peer *Peer) {
} }
} }
func (node *trieEntry) lookup(ip []byte) *Peer { func (node *trieEntry) lookup4(ip [4]byte) *Peer {
var found *Peer
for node != nil && commonBits4(node.bits, ip) >= node.cidr {
if node.peer != nil {
found = node.peer
}
if node.bitAtByte == 4 {
break
}
bit := (ip[node.bitAtByte] >> node.bitAtShift) & 1
node = node.child[bit]
}
return found
}
func (node *trieEntry) lookup6(ip [16]byte) *Peer {
var found *Peer
for node != nil && commonBits6(node.bits, ip) >= node.cidr {
if node.peer != nil {
found = node.peer
}
if node.bitAtByte == 16 {
break
}
bit := (ip[node.bitAtByte] >> node.bitAtShift) & 1
node = node.child[bit]
}
return found
}
func (node *trieEntry) lookup(ip net.IP) *Peer {
var found *Peer var found *Peer
size := uint8(len(ip)) size := uint8(len(ip))
for node != nil && commonBits(node.bits, ip) >= node.cidr { for node != nil && commonBits(node.bits, ip) >= node.cidr {
@ -205,14 +254,17 @@ func (node *trieEntry) lookup(ip []byte) *Peer {
} }
type AllowedIPs struct { type AllowedIPs struct {
IPv4 *trieEntry mu sync.RWMutex
IPv6 *trieEntry ipv4 *trieEntry
mutex sync.RWMutex ipv6 *trieEntry
peerByIPPacketFunc PeerByIPPacketFunc // if non-nil, called to look up peers by IP
device *Device // back-reference to parent device; non-nil only if peerByIPPacketFunc is set
} }
func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) bool) { func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) bool) {
table.mutex.RLock() table.mu.RLock()
defer table.mutex.RUnlock() defer table.mu.RUnlock()
for elem := peer.trieEntries.Front(); elem != nil; elem = elem.Next() { for elem := peer.trieEntries.Front(); elem != nil; elem = elem.Next() {
node := elem.Value.(*trieEntry) node := elem.Value.(*trieEntry)
@ -223,72 +275,216 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix)
} }
} }
func (table *AllowedIPs) RemoveByPeer(peer *Peer) { func (node *trieEntry) remove() {
table.mutex.Lock() node.removeFromPeerEntries()
defer table.mutex.Unlock() node.peer = nil
if node.child[0] != nil && node.child[1] != nil {
return
}
bit := 0
if node.child[0] == nil {
bit = 1
}
child := node.child[bit]
if child != nil {
child.parent = node.parent
}
*node.parent.parentBit = child
if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 {
node.zeroizePointers()
return
}
parent := (*trieEntry)(unsafe.Pointer(uintptr(unsafe.Pointer(node.parent.parentBit)) - unsafe.Offsetof(node.child) - unsafe.Sizeof(node.child[0])*uintptr(node.parent.parentBitType)))
if parent.peer != nil {
node.zeroizePointers()
return
}
child = parent.child[node.parent.parentBitType^1]
if child != nil {
child.parent = parent.parent
}
*parent.parent.parentBit = child
node.zeroizePointers()
parent.zeroizePointers()
}
func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) {
table.mu.Lock()
defer table.mu.Unlock()
var node *trieEntry
var exact bool
if prefix.Addr().Is6() {
ip := prefix.Addr().As16()
node, exact = table.ipv6.nodePlacement(ip[:], uint8(prefix.Bits()))
} else if prefix.Addr().Is4() {
ip := prefix.Addr().As4()
node, exact = table.ipv4.nodePlacement(ip[:], uint8(prefix.Bits()))
} else {
panic(errors.New("removing unknown address type"))
}
if !exact || node == nil || peer != node.peer {
return
}
node.remove()
}
// setPeerPrefixes atomically removes all of peer's existing prefixes and adds
// the provided ones.
func (table *AllowedIPs) setPeerPrefixes(peer *Peer, prefixes []netip.Prefix) {
table.mu.Lock()
defer table.mu.Unlock()
table.removeByPeerLocked(peer)
for _, prefix := range prefixes {
table.insertLocked(prefix, peer)
}
}
func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
table.mu.Lock()
defer table.mu.Unlock()
table.removeByPeerLocked(peer)
}
func (table *AllowedIPs) removeByPeerLocked(peer *Peer) {
var next *list.Element var next *list.Element
for elem := peer.trieEntries.Front(); elem != nil; elem = next { for elem := peer.trieEntries.Front(); elem != nil; elem = next {
next = elem.Next() next = elem.Next()
node := elem.Value.(*trieEntry) elem.Value.(*trieEntry).remove()
node.removeFromPeerEntries()
node.peer = nil
if node.child[0] != nil && node.child[1] != nil {
continue
}
bit := 0
if node.child[0] == nil {
bit = 1
}
child := node.child[bit]
if child != nil {
child.parent = node.parent
}
*node.parent.parentBit = child
if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 {
node.zeroizePointers()
continue
}
parent := (*trieEntry)(unsafe.Pointer(uintptr(unsafe.Pointer(node.parent.parentBit)) - unsafe.Offsetof(node.child) - unsafe.Sizeof(node.child[0])*uintptr(node.parent.parentBitType)))
if parent.peer != nil {
node.zeroizePointers()
continue
}
child = parent.child[node.parent.parentBitType^1]
if child != nil {
child.parent = parent.parent
}
*parent.parent.parentBit = child
node.zeroizePointers()
parent.zeroizePointers()
} }
} }
func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) { func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) {
table.mutex.Lock() table.mu.Lock()
defer table.mutex.Unlock() defer table.mu.Unlock()
table.insertLocked(prefix, peer)
}
func (table *AllowedIPs) insertLocked(prefix netip.Prefix, peer *Peer) {
if prefix.Addr().Is6() { if prefix.Addr().Is6() {
ip := prefix.Addr().As16() ip := prefix.Addr().As16()
parentIndirection{&table.IPv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer) parentIndirection{&table.ipv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer)
} else if prefix.Addr().Is4() { } else if prefix.Addr().Is4() {
ip := prefix.Addr().As4() ip := prefix.Addr().As4()
parentIndirection{&table.IPv4, 2}.insert(ip[:], uint8(prefix.Bits()), peer) parentIndirection{&table.ipv4, 2}.insert(ip[:], uint8(prefix.Bits()), peer)
} else { } else {
panic(errors.New("inserting unknown address type")) panic(errors.New("inserting unknown address type"))
} }
} }
func (table *AllowedIPs) Lookup(ip []byte) *Peer { // LookupFromPacket looks up the peer to which an outbound IP packet should be
table.mutex.RLock() // sent. It lives on [AllowedIPs] for legacy/structural reasons: historically
defer table.mutex.RUnlock() // WireGuard's only peer-selection mechanism was the AllowedIPs trie, and the
switch len(ip) { // send path already had a reference to the table. When a [PeerByIPPacketFunc]
case net.IPv6len: // has been registered via [Device.SetPeerByIPPacketFunc], that callback is used
return table.IPv6.lookup(ip) // instead of the trie and the AllowedIPs table is not consulted at all.
case net.IPv4len: //
return table.IPv4.lookup(ip) // When no callback is registered, only dst is used (standard WireGuard
// AllowedIPs trie lookup). When a callback is registered, all three
// parameters are forwarded to it; see [PeerByIPPacketFunc] for details.
func (table *AllowedIPs) LookupFromPacket(src, dst netip.Addr, ipPkt []byte) *Peer {
table.mu.RLock()
if f := table.peerByIPPacketFunc; f != nil {
device := table.device
table.mu.RUnlock()
if pubk, ok := f(src, dst, ipPkt); ok {
return device.LookupPeer(pubk)
}
return nil
}
defer table.mu.RUnlock()
switch {
case dst.Is6():
return table.ipv6.lookup6(dst.As16())
case dst.Is4():
return table.ipv4.lookup4(dst.As4())
default: default:
panic(errors.New("looking up unknown address type")) panic(errors.New("looking up unknown address type"))
} }
} }
// Deprecated: Lookup is only used by legacy tests. It does not call
// [PeerByIPPacketFunc]; use [AllowedIPs.LookupFromPacket] for production lookups.
func (table *AllowedIPs) Lookup(ip []byte) *Peer {
table.mu.RLock()
defer table.mu.RUnlock()
return table.lookupLocked(ip)
}
// lookupLocked looks up the peer associated with the given IP address.
// It assumes the caller holds the read lock (or doesn't hold it, but also
// doesn't concurrently mutate AllowedIP).
//
// It returns nil if no peer is associated with the given IP address.
func (table *AllowedIPs) lookupLocked(ip []byte) *Peer {
switch len(ip) {
case net.IPv6len:
return table.ipv6.lookup(ip)
case net.IPv4len:
return table.ipv4.lookup(ip)
default:
panic(errors.New("looking up unknown address type"))
}
}
// AllowedPeerSourceIP reports whether the given source IP address is allowed
// for the given peer.
func (peer *Peer) AllowedPeerSourceIP(src netip.Addr) bool {
if f := peer.state.testAllowedIP.Load(); f != nil {
return (*f)(src)
}
table := &peer.device.allowedips
table.mu.RLock()
defer table.mu.RUnlock()
switch {
case src.Is6():
return table.ipv6.lookup6(src.As16()) == peer
case src.Is4():
return table.ipv4.lookup4(src.As4()) == peer
}
return false
}
// fakePeer is a zero Peer used only as a placeholder in tries used by mkIPInCIDRsTestFunc.
var fakePeer Peer
// mkIPInCIDRsTestFunc returns a function that tests whether an IP address is
// contained in any of the given CIDRs.
func mkIPInCIDRsTestFunc(cidrs []netip.Prefix) func(netip.Addr) bool {
if len(cidrs) == 0 {
return func(netip.Addr) bool { return false }
}
if len(cidrs) == 1 {
return func(addr netip.Addr) bool { return cidrs[0].Contains(addr) }
}
if len(cidrs) <= 4 {
// For small numbers of CIDRs, just do a linear search. The trie construction
// is more expensive than the linear search, and the test function is faster
// than the trie lookup, so this is a net win.
return func(addr netip.Addr) bool {
for _, c := range cidrs {
if c.Contains(addr) {
return true
}
}
return false
}
}
// Make a trie for faster lookups. We use a dummy Peer.
var a AllowedIPs
for _, c := range cidrs {
a.Insert(c, &fakePeer)
}
return func(addr netip.Addr) bool {
switch {
case addr.Is4():
return a.ipv4.lookup4(addr.As4()) == &fakePeer
default:
return a.ipv6.lookup6(addr.As16()) == &fakePeer
}
}
}

View file

@ -1,141 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"math/rand"
"net"
"net/netip"
"sort"
"testing"
)
const (
NumberOfPeers = 100
NumberOfPeerRemovals = 4
NumberOfAddresses = 250
NumberOfTests = 10000
)
type SlowNode struct {
peer *Peer
cidr uint8
bits []byte
}
type SlowRouter []*SlowNode
func (r SlowRouter) Len() int {
return len(r)
}
func (r SlowRouter) Less(i, j int) bool {
return r[i].cidr > r[j].cidr
}
func (r SlowRouter) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func (r SlowRouter) Insert(addr []byte, cidr uint8, peer *Peer) SlowRouter {
for _, t := range r {
if t.cidr == cidr && commonBits(t.bits, addr) >= cidr {
t.peer = peer
t.bits = addr
return r
}
}
r = append(r, &SlowNode{
cidr: cidr,
bits: addr,
peer: peer,
})
sort.Sort(r)
return r
}
func (r SlowRouter) Lookup(addr []byte) *Peer {
for _, t := range r {
common := commonBits(t.bits, addr)
if common >= t.cidr {
return t.peer
}
}
return nil
}
func (r SlowRouter) RemoveByPeer(peer *Peer) SlowRouter {
n := 0
for _, x := range r {
if x.peer != peer {
r[n] = x
n++
}
}
return r[:n]
}
func TestTrieRandom(t *testing.T) {
var slow4, slow6 SlowRouter
var peers []*Peer
var allowedIPs AllowedIPs
rand.Seed(1)
for n := 0; n < NumberOfPeers; n++ {
peers = append(peers, &Peer{})
}
for n := 0; n < NumberOfAddresses; n++ {
var addr4 [4]byte
rand.Read(addr4[:])
cidr := uint8(rand.Intn(32) + 1)
index := rand.Intn(NumberOfPeers)
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4(addr4), int(cidr)), peers[index])
slow4 = slow4.Insert(addr4[:], cidr, peers[index])
var addr6 [16]byte
rand.Read(addr6[:])
cidr = uint8(rand.Intn(128) + 1)
index = rand.Intn(NumberOfPeers)
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(addr6), int(cidr)), peers[index])
slow6 = slow6.Insert(addr6[:], cidr, peers[index])
}
var p int
for p = 0; ; p++ {
for n := 0; n < NumberOfTests; n++ {
var addr4 [4]byte
rand.Read(addr4[:])
peer1 := slow4.Lookup(addr4[:])
peer2 := allowedIPs.Lookup(addr4[:])
if peer1 != peer2 {
t.Errorf("Trie did not match naive implementation, for %v: want %p, got %p", net.IP(addr4[:]), peer1, peer2)
}
var addr6 [16]byte
rand.Read(addr6[:])
peer1 = slow6.Lookup(addr6[:])
peer2 = allowedIPs.Lookup(addr6[:])
if peer1 != peer2 {
t.Errorf("Trie did not match naive implementation, for %v: want %p, got %p", net.IP(addr6[:]), peer1, peer2)
}
}
if p >= len(peers) || p >= NumberOfPeerRemovals {
break
}
allowedIPs.RemoveByPeer(peers[p])
slow4 = slow4.RemoveByPeer(peers[p])
slow6 = slow6.RemoveByPeer(peers[p])
}
for ; p < len(peers); p++ {
allowedIPs.RemoveByPeer(peers[p])
}
if allowedIPs.IPv4 != nil || allowedIPs.IPv6 != nil {
t.Error("Failed to remove all nodes from trie by peer")
}
}

View file

@ -1,247 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"math/rand"
"net"
"net/netip"
"testing"
)
type testPairCommonBits struct {
s1 []byte
s2 []byte
match uint8
}
func TestCommonBits(t *testing.T) {
tests := []testPairCommonBits{
{s1: []byte{1, 4, 53, 128}, s2: []byte{0, 0, 0, 0}, match: 7},
{s1: []byte{0, 4, 53, 128}, s2: []byte{0, 0, 0, 0}, match: 13},
{s1: []byte{0, 4, 53, 253}, s2: []byte{0, 4, 53, 252}, match: 31},
{s1: []byte{192, 168, 1, 1}, s2: []byte{192, 169, 1, 1}, match: 15},
{s1: []byte{65, 168, 1, 1}, s2: []byte{192, 169, 1, 1}, match: 0},
}
for _, p := range tests {
v := commonBits(p.s1, p.s2)
if v != p.match {
t.Error(
"For slice", p.s1, p.s2,
"expected match", p.match,
",but got", v,
)
}
}
}
func benchmarkTrie(peerNumber, addressNumber, addressLength int, b *testing.B) {
var trie *trieEntry
var peers []*Peer
root := parentIndirection{&trie, 2}
rand.Seed(1)
const AddressLength = 4
for n := 0; n < peerNumber; n++ {
peers = append(peers, &Peer{})
}
for n := 0; n < addressNumber; n++ {
var addr [AddressLength]byte
rand.Read(addr[:])
cidr := uint8(rand.Uint32() % (AddressLength * 8))
index := rand.Int() % peerNumber
root.insert(addr[:], cidr, peers[index])
}
for n := 0; n < b.N; n++ {
var addr [AddressLength]byte
rand.Read(addr[:])
trie.lookup(addr[:])
}
}
func BenchmarkTrieIPv4Peers100Addresses1000(b *testing.B) {
benchmarkTrie(100, 1000, net.IPv4len, b)
}
func BenchmarkTrieIPv4Peers10Addresses10(b *testing.B) {
benchmarkTrie(10, 10, net.IPv4len, b)
}
func BenchmarkTrieIPv6Peers100Addresses1000(b *testing.B) {
benchmarkTrie(100, 1000, net.IPv6len, b)
}
func BenchmarkTrieIPv6Peers10Addresses10(b *testing.B) {
benchmarkTrie(10, 10, net.IPv6len, b)
}
/* Test ported from kernel implementation:
* selftest/allowedips.h
*/
func TestTrieIPv4(t *testing.T) {
a := &Peer{}
b := &Peer{}
c := &Peer{}
d := &Peer{}
e := &Peer{}
g := &Peer{}
h := &Peer{}
var allowedIPs AllowedIPs
insert := func(peer *Peer, a, b, c, d byte, cidr uint8) {
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer)
}
assertEQ := func(peer *Peer, a, b, c, d byte) {
p := allowedIPs.Lookup([]byte{a, b, c, d})
if p != peer {
t.Error("Assert EQ failed")
}
}
assertNEQ := func(peer *Peer, a, b, c, d byte) {
p := allowedIPs.Lookup([]byte{a, b, c, d})
if p == peer {
t.Error("Assert NEQ failed")
}
}
insert(a, 192, 168, 4, 0, 24)
insert(b, 192, 168, 4, 4, 32)
insert(c, 192, 168, 0, 0, 16)
insert(d, 192, 95, 5, 64, 27)
insert(c, 192, 95, 5, 65, 27)
insert(e, 0, 0, 0, 0, 0)
insert(g, 64, 15, 112, 0, 20)
insert(h, 64, 15, 123, 211, 25)
insert(a, 10, 0, 0, 0, 25)
insert(b, 10, 0, 0, 128, 25)
insert(a, 10, 1, 0, 0, 30)
insert(b, 10, 1, 0, 4, 30)
insert(c, 10, 1, 0, 8, 29)
insert(d, 10, 1, 0, 16, 29)
assertEQ(a, 192, 168, 4, 20)
assertEQ(a, 192, 168, 4, 0)
assertEQ(b, 192, 168, 4, 4)
assertEQ(c, 192, 168, 200, 182)
assertEQ(c, 192, 95, 5, 68)
assertEQ(e, 192, 95, 5, 96)
assertEQ(g, 64, 15, 116, 26)
assertEQ(g, 64, 15, 127, 3)
insert(a, 1, 0, 0, 0, 32)
insert(a, 64, 0, 0, 0, 32)
insert(a, 128, 0, 0, 0, 32)
insert(a, 192, 0, 0, 0, 32)
insert(a, 255, 0, 0, 0, 32)
assertEQ(a, 1, 0, 0, 0)
assertEQ(a, 64, 0, 0, 0)
assertEQ(a, 128, 0, 0, 0)
assertEQ(a, 192, 0, 0, 0)
assertEQ(a, 255, 0, 0, 0)
allowedIPs.RemoveByPeer(a)
assertNEQ(a, 1, 0, 0, 0)
assertNEQ(a, 64, 0, 0, 0)
assertNEQ(a, 128, 0, 0, 0)
assertNEQ(a, 192, 0, 0, 0)
assertNEQ(a, 255, 0, 0, 0)
allowedIPs.RemoveByPeer(a)
allowedIPs.RemoveByPeer(b)
allowedIPs.RemoveByPeer(c)
allowedIPs.RemoveByPeer(d)
allowedIPs.RemoveByPeer(e)
allowedIPs.RemoveByPeer(g)
allowedIPs.RemoveByPeer(h)
if allowedIPs.IPv4 != nil || allowedIPs.IPv6 != nil {
t.Error("Expected removing all the peers to empty trie, but it did not")
}
insert(a, 192, 168, 0, 0, 16)
insert(a, 192, 168, 0, 0, 24)
allowedIPs.RemoveByPeer(a)
assertNEQ(a, 192, 168, 0, 1)
}
/* Test ported from kernel implementation:
* selftest/allowedips.h
*/
func TestTrieIPv6(t *testing.T) {
a := &Peer{}
b := &Peer{}
c := &Peer{}
d := &Peer{}
e := &Peer{}
f := &Peer{}
g := &Peer{}
h := &Peer{}
var allowedIPs AllowedIPs
expand := func(a uint32) []byte {
var out [4]byte
out[0] = byte(a >> 24 & 0xff)
out[1] = byte(a >> 16 & 0xff)
out[2] = byte(a >> 8 & 0xff)
out[3] = byte(a & 0xff)
return out[:]
}
insert := func(peer *Peer, a, b, c, d uint32, cidr uint8) {
var addr []byte
addr = append(addr, expand(a)...)
addr = append(addr, expand(b)...)
addr = append(addr, expand(c)...)
addr = append(addr, expand(d)...)
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer)
}
assertEQ := func(peer *Peer, a, b, c, d uint32) {
var addr []byte
addr = append(addr, expand(a)...)
addr = append(addr, expand(b)...)
addr = append(addr, expand(c)...)
addr = append(addr, expand(d)...)
p := allowedIPs.Lookup(addr)
if p != peer {
t.Error("Assert EQ failed")
}
}
insert(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128)
insert(c, 0x26075300, 0x60006b00, 0, 0, 64)
insert(e, 0, 0, 0, 0, 0)
insert(f, 0, 0, 0, 0, 0)
insert(g, 0x24046800, 0, 0, 0, 32)
insert(h, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 64)
insert(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 128)
insert(c, 0x24446800, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)
insert(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98)
assertEQ(d, 0x26075300, 0x60006b00, 0, 0xc05f0543)
assertEQ(c, 0x26075300, 0x60006b00, 0, 0xc02e01ee)
assertEQ(f, 0x26075300, 0x60006b01, 0, 0)
assertEQ(g, 0x24046800, 0x40040806, 0, 0x1006)
assertEQ(g, 0x24046800, 0x40040806, 0x1234, 0x5678)
assertEQ(f, 0x240467ff, 0x40040806, 0x1234, 0x5678)
assertEQ(f, 0x24046801, 0x40040806, 0x1234, 0x5678)
assertEQ(h, 0x24046800, 0x40040800, 0x1234, 0x5678)
assertEQ(h, 0x24046800, 0x40040800, 0, 0)
assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010)
assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef)
}

View file

@ -0,0 +1,190 @@
/* SPDX-License-Identifier: MIT
*
* lx: e2e regression for the reserved-clear vs AWG magic-header collision,
* exercised over the StdNetBind path (no detour) with real loopback UDP.
*
* Bug model. On receive, StdNetBind.receiveIP unconditionally zeroed bytes
* 1-3 of every datagram >3 bytes (the Cloudflare WARP "reserved" field).
* AmneziaWG reads its magic header as LittleEndian.Uint32(packet[padding:]),
* where padding is s1/s2 (handshake) or s4 (transport). With small padding
* (0..3) the 4-byte magic overlaps bytes 1-3, so the unconditional clear
* corrupts it: the value falls outside the ranged h1-h4 window, the packet is
* classified MessageUnknownType and dropped. WARP was never configured on
* these binds (no SetReservedForEndpoint), so the clear was pure collateral.
*
* The fix gates the clear behind StdNetBind.hasReserved(): bytes 1-3 are only
* zeroed when a non-zero reserved value is actually set for some endpoint.
* With no reserved value the magic survives and the handshake completes.
*
* This test provokes the worst case: padding = 0 (no s1/s2/s4 at all), so the
* h1 initiation magic sits in bytes [0..3] and its high bytes (1-3) are the
* ones the clear would destroy. The h1-h4 ranges are chosen entirely above
* 0x10000000, so after zeroing bytes 1-3 the surviving value is <= 255 and can
* never land back inside any range -> guaranteed drop on the buggy tree.
*
* GREEN on the fixed tree. To see RED, temporarily restore the unconditional
* clear in conn/bind_std.go receiveIP:
* if msg.N > 3 {
* common.ClearArray(bufs[i][1:4])
* }
* and the handshake times out (init magic zeroed in bytes 1-3).
*/
package device
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
"testing"
"time"
"github.com/sagernet/wireguard-go/conn"
)
// magic header ranges kept entirely above 0x10000000 (268435456). Any value
// the sender picks therefore has a non-zero byte among positions 1-3; zeroing
// those bytes collapses the value to <= 0xFF, which is below every range start,
// so a corrupted magic can never validate. Distinct windows per message type.
const (
lxH1Lo, lxH1Hi = 268500000, 268600000 // init
lxH2Lo, lxH2Hi = 300000000, 300100000 // response
lxH3Lo, lxH3Hi = 400000000, 400100000 // cookie
lxH4Lo, lxH4Hi = 500000000, 500100000 // transport
)
// lxReadListenPort parses listen_port=<n> out of a device's IpcGet dump.
func lxReadListenPort(t *testing.T, dev *Device) uint16 {
t.Helper()
dump, err := dev.IpcGet()
if err != nil {
t.Fatalf("IpcGet: %v", err)
}
scanner := bufio.NewScanner(strings.NewReader(dump))
for scanner.Scan() {
line := scanner.Text()
if v, ok := strings.CutPrefix(line, "listen_port="); ok {
p, err := strconv.Atoi(v)
if err != nil {
t.Fatalf("parse listen_port %q: %v", v, err)
}
return uint16(p)
}
}
t.Fatalf("listen_port not found in dump:\n%s", dump)
return 0
}
// newStdNetPaddedPair builds two Up()'d Devices peered over real loopback UDP
// (NewStdNetBind), configured with ranged h1-h4 + junk and *no* s1/s2/s4
// (padding = 0). Endpoints are wired after Up, once the ephemeral ports are
// known. No reserved value is ever set, so hasReserved() is false.
func newStdNetPaddedPair(t *testing.T) (devA, devB *Device, tunA, tunB *chanTun) {
t.Helper()
skA, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey A: %v", err)
}
skB, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey B: %v", err)
}
pkA := skA.publicKey()
pkB := skB.publicKey()
tunA = newChanTun()
tunB = newChanTun()
devA = NewDevice(context.Background(), tunA, conn.NewStdNetBind(nil), NewLogger(LogLevelError, "devA: "), 1)
devB = NewDevice(context.Background(), tunB, conn.NewStdNetBind(nil), NewLogger(LogLevelError, "devB: "), 1)
t.Cleanup(devA.Close)
t.Cleanup(devB.Close)
// obfuscation shared by both ends. Ranged magic headers, junk packets,
// and deliberately no s1/s2/s4 so padding stays 0 for every message type.
obf := fmt.Sprintf(
"jc=3\njmin=8\njmax=16\n"+
"h1=%d-%d\nh2=%d-%d\nh3=%d-%d\nh4=%d-%d\n",
lxH1Lo, lxH1Hi, lxH2Lo, lxH2Hi, lxH3Lo, lxH3Hi, lxH4Lo, lxH4Hi)
// Bring both up on an ephemeral port (listen_port=0), no endpoint yet.
cfgA := fmt.Sprintf("private_key=%s\nlisten_port=0\n%sreplace_peers=true\npublic_key=%s\nallowed_ip=%s/32\n",
hex.EncodeToString(skA[:]), obf, hex.EncodeToString(pkB[:]), testIPB)
cfgB := fmt.Sprintf("private_key=%s\nlisten_port=0\n%sreplace_peers=true\npublic_key=%s\nallowed_ip=%s/32\n",
hex.EncodeToString(skB[:]), obf, hex.EncodeToString(pkA[:]), testIPA)
if err := devA.IpcSet(cfgA); err != nil {
t.Fatalf("IpcSet A: %v", err)
}
if err := devB.IpcSet(cfgB); err != nil {
t.Fatalf("IpcSet B: %v", err)
}
if devA.paddings.init != 0 || devA.paddings.response != 0 || devA.paddings.transport != 0 {
t.Fatalf("padding must be 0 for this test: init=%d resp=%d transport=%d",
devA.paddings.init, devA.paddings.response, devA.paddings.transport)
}
if err := devA.Up(); err != nil {
t.Fatalf("Up A: %v", err)
}
if err := devB.Up(); err != nil {
t.Fatalf("Up B: %v", err)
}
portA := lxReadListenPort(t, devA)
portB := lxReadListenPort(t, devB)
if portA == 0 || portB == 0 {
t.Fatalf("ephemeral ports not assigned: A=%d B=%d", portA, portB)
}
// Now that ports are known, point each peer at the other over loopback.
if err := devA.IpcSet(fmt.Sprintf("public_key=%s\nupdate_only=true\nendpoint=127.0.0.1:%d\n",
hex.EncodeToString(pkB[:]), portB)); err != nil {
t.Fatalf("set endpoint A->B: %v", err)
}
if err := devB.IpcSet(fmt.Sprintf("public_key=%s\nupdate_only=true\nendpoint=127.0.0.1:%d\n",
hex.EncodeToString(pkA[:]), portA)); err != nil {
t.Fatalf("set endpoint B->A: %v", err)
}
return devA, devB, tunA, tunB
}
// TestStdNetBindReservedClearVsMagic_ZeroPadding drives a real handshake and a
// data packet A->B over loopback UDP through StdNetBind, with padding=0 so the
// h1 magic overlaps the reserved bytes 1-3. It passes only when receive does
// not blindly clear those bytes (the fix).
func TestStdNetBindReservedClearVsMagic_ZeroPadding(t *testing.T) {
devA, _, tunA, tunB := newStdNetPaddedPair(t)
_ = devA
pkt := buildIPv4Packet(testIPA, testIPB, 8)
// Re-inject periodically: the first packet triggers the handshake and may
// be dropped until keys are established.
send := func() { tunA.toDevice <- pkt }
send()
deadline := time.After(15 * time.Second)
retry := time.NewTicker(500 * time.Millisecond)
defer retry.Stop()
for {
select {
case got := <-tunB.fromDevice:
if bytes.Equal(got, pkt) {
return // delivered end to end: magic survived, handshake ok
}
t.Logf("ignoring unexpected packet len=%d", len(got))
case <-retry.C:
send()
case <-deadline:
t.Fatal("timed out waiting for packet on peer tun " +
"(handshake never completed: reserved-clear likely corrupted the h1 magic)")
}
}
}

View file

@ -1,56 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"errors"
"golang.zx2c4.com/wireguard/conn"
)
type DummyDatagram struct {
msg []byte
endpoint conn.Endpoint
}
type DummyBind struct {
in6 chan DummyDatagram
in4 chan DummyDatagram
closed bool
}
func (b *DummyBind) SetMark(v uint32) error {
return nil
}
func (b *DummyBind) ReceiveIPv6(buf []byte) (int, conn.Endpoint, error) {
datagram, ok := <-b.in6
if !ok {
return 0, nil, errors.New("closed")
}
copy(buf, datagram.msg)
return len(datagram.msg), datagram.endpoint, nil
}
func (b *DummyBind) ReceiveIPv4(buf []byte) (int, conn.Endpoint, error) {
datagram, ok := <-b.in4
if !ok {
return 0, nil, errors.New("closed")
}
copy(buf, datagram.msg)
return len(datagram.msg), datagram.endpoint, nil
}
func (b *DummyBind) Close() error {
close(b.in6)
close(b.in4)
b.closed = true
return nil
}
func (b *DummyBind) Send(buf []byte, end conn.Endpoint) error {
return nil
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -83,15 +83,21 @@ func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue {
q := &autodrainingInboundQueue{ q := &autodrainingInboundQueue{
c: make(chan *QueueInboundElementsContainer, QueueInboundSize), c: make(chan *QueueInboundElementsContainer, QueueInboundSize),
} }
runtime.SetFinalizer(q, device.flushInboundQueue) if device.needsInboundQueueFinalizer() {
runtime.AddCleanup(q, device.flushInboundQueue, q.c)
}
return q return q
} }
func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { func (device *Device) needsInboundQueueFinalizer() bool {
return device.pool.messageBuffers.hasAccounting()
}
func (device *Device) flushInboundQueue(c <-chan *QueueInboundElementsContainer) {
for { for {
select { select {
case elemsContainer := <-q.c: case elemsContainer := <-c:
elemsContainer.Lock() elemsContainer.filling.Wait()
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer) device.PutMessageBuffer(elem.buffer)
device.PutInboundElement(elem) device.PutInboundElement(elem)
@ -116,17 +122,23 @@ func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue {
q := &autodrainingOutboundQueue{ q := &autodrainingOutboundQueue{
c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize), c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize),
} }
runtime.SetFinalizer(q, device.flushOutboundQueue) if device.needsOutboundQueueFinalizer() {
runtime.AddCleanup(q, device.flushOutboundQueue, q.c)
}
return q return q
} }
func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { func (device *Device) needsOutboundQueueFinalizer() bool {
return device.pool.messageBuffers.hasAccounting()
}
func (device *Device) flushOutboundQueue(c <-chan *QueueOutboundElementsContainer) {
for { for {
select { select {
case elemsContainer := <-q.c: case elemsContainer := <-c:
elemsContainer.Lock() elemsContainer.filling.Wait()
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer) device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem) device.PutOutboundElement(elem)
} }
device.PutOutboundElementsContainer(elemsContainer) device.PutOutboundElementsContainer(elemsContainer)

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -27,9 +27,9 @@ const (
) )
const ( const (
MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive) MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive)
MaxMessageSize = MaxSegmentSize // maximum size of transport message MaxMessageSize = MaxSegmentSize // maximum size of transport message
MaxContentSize = MaxSegmentSize - MessageTransportSize // maximum size of transport message content MaxContentSize = MaxSegmentSize - MessageTransportSize - MessageEncapsulatingTransportSize // maximum size of transport message content
) )
/* Implementation constants */ /* Implementation constants */

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -118,6 +118,7 @@ func (st *CookieChecker) CreateReply(
msg []byte, msg []byte,
recv uint32, recv uint32,
src []byte, src []byte,
msgType uint32,
) (*MessageCookieReply, error) { ) (*MessageCookieReply, error) {
st.RLock() st.RLock()
@ -153,7 +154,7 @@ func (st *CookieChecker) CreateReply(
smac1 := smac2 - blake2s.Size128 smac1 := smac2 - blake2s.Size128
reply := new(MessageCookieReply) reply := new(MessageCookieReply)
reply.Type = MessageCookieReplyType reply.Type = msgType
reply.Receiver = recv reply.Receiver = recv
_, err := rand.Read(reply.Nonce[:]) _, err := rand.Read(reply.Nonce[:])

View file

@ -1,190 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"testing"
)
func TestCookieMAC1(t *testing.T) {
// setup generator / checker
var (
generator CookieGenerator
checker CookieChecker
)
sk, err := newPrivateKey()
if err != nil {
t.Fatal(err)
}
pk := sk.publicKey()
generator.Init(pk)
checker.Init(pk)
// check mac1
src := []byte{192, 168, 13, 37, 10, 10, 10}
checkMAC1 := func(msg []byte) {
generator.AddMacs(msg)
if !checker.CheckMAC1(msg) {
t.Fatal("MAC1 generation/verification failed")
}
if checker.CheckMAC2(msg, src) {
t.Fatal("MAC2 generation/verification failed")
}
}
checkMAC1([]byte{
0x99, 0xbb, 0xa5, 0xfc, 0x99, 0xaa, 0x83, 0xbd,
0x7b, 0x00, 0xc5, 0x9a, 0x4c, 0xb9, 0xcf, 0x62,
0x40, 0x23, 0xf3, 0x8e, 0xd8, 0xd0, 0x62, 0x64,
0x5d, 0xb2, 0x80, 0x13, 0xda, 0xce, 0xc6, 0x91,
0x61, 0xd6, 0x30, 0xf1, 0x32, 0xb3, 0xa2, 0xf4,
0x7b, 0x43, 0xb5, 0xa7, 0xe2, 0xb1, 0xf5, 0x6c,
0x74, 0x6b, 0xb0, 0xcd, 0x1f, 0x94, 0x86, 0x7b,
0xc8, 0xfb, 0x92, 0xed, 0x54, 0x9b, 0x44, 0xf5,
0xc8, 0x7d, 0xb7, 0x8e, 0xff, 0x49, 0xc4, 0xe8,
0x39, 0x7c, 0x19, 0xe0, 0x60, 0x19, 0x51, 0xf8,
0xe4, 0x8e, 0x02, 0xf1, 0x7f, 0x1d, 0xcc, 0x8e,
0xb0, 0x07, 0xff, 0xf8, 0xaf, 0x7f, 0x66, 0x82,
0x83, 0xcc, 0x7c, 0xfa, 0x80, 0xdb, 0x81, 0x53,
0xad, 0xf7, 0xd8, 0x0c, 0x10, 0xe0, 0x20, 0xfd,
0xe8, 0x0b, 0x3f, 0x90, 0x15, 0xcd, 0x93, 0xad,
0x0b, 0xd5, 0x0c, 0xcc, 0x88, 0x56, 0xe4, 0x3f,
})
checkMAC1([]byte{
0x33, 0xe7, 0x2a, 0x84, 0x9f, 0xff, 0x57, 0x6c,
0x2d, 0xc3, 0x2d, 0xe1, 0xf5, 0x5c, 0x97, 0x56,
0xb8, 0x93, 0xc2, 0x7d, 0xd4, 0x41, 0xdd, 0x7a,
0x4a, 0x59, 0x3b, 0x50, 0xdd, 0x7a, 0x7a, 0x8c,
0x9b, 0x96, 0xaf, 0x55, 0x3c, 0xeb, 0x6d, 0x0b,
0x13, 0x0b, 0x97, 0x98, 0xb3, 0x40, 0xc3, 0xcc,
0xb8, 0x57, 0x33, 0x45, 0x6e, 0x8b, 0x09, 0x2b,
0x81, 0x2e, 0xd2, 0xb9, 0x66, 0x0b, 0x93, 0x05,
})
checkMAC1([]byte{
0x9b, 0x96, 0xaf, 0x55, 0x3c, 0xeb, 0x6d, 0x0b,
0x13, 0x0b, 0x97, 0x98, 0xb3, 0x40, 0xc3, 0xcc,
0xb8, 0x57, 0x33, 0x45, 0x6e, 0x8b, 0x09, 0x2b,
0x81, 0x2e, 0xd2, 0xb9, 0x66, 0x0b, 0x93, 0x05,
})
// exchange cookie reply
func() {
msg := []byte{
0x6d, 0xd7, 0xc3, 0x2e, 0xb0, 0x76, 0xd8, 0xdf,
0x30, 0x65, 0x7d, 0x62, 0x3e, 0xf8, 0x9a, 0xe8,
0xe7, 0x3c, 0x64, 0xa3, 0x78, 0x48, 0xda, 0xf5,
0x25, 0x61, 0x28, 0x53, 0x79, 0x32, 0x86, 0x9f,
0xa0, 0x27, 0x95, 0x69, 0xb6, 0xba, 0xd0, 0xa2,
0xf8, 0x68, 0xea, 0xa8, 0x62, 0xf2, 0xfd, 0x1b,
0xe0, 0xb4, 0x80, 0xe5, 0x6b, 0x3a, 0x16, 0x9e,
0x35, 0xf6, 0xa8, 0xf2, 0x4f, 0x9a, 0x7b, 0xe9,
0x77, 0x0b, 0xc2, 0xb4, 0xed, 0xba, 0xf9, 0x22,
0xc3, 0x03, 0x97, 0x42, 0x9f, 0x79, 0x74, 0x27,
0xfe, 0xf9, 0x06, 0x6e, 0x97, 0x3a, 0xa6, 0x8f,
0xc9, 0x57, 0x0a, 0x54, 0x4c, 0x64, 0x4a, 0xe2,
0x4f, 0xa1, 0xce, 0x95, 0x9b, 0x23, 0xa9, 0x2b,
0x85, 0x93, 0x42, 0xb0, 0xa5, 0x53, 0xed, 0xeb,
0x63, 0x2a, 0xf1, 0x6d, 0x46, 0xcb, 0x2f, 0x61,
0x8c, 0xe1, 0xe8, 0xfa, 0x67, 0x20, 0x80, 0x6d,
}
generator.AddMacs(msg)
reply, err := checker.CreateReply(msg, 1377, src)
if err != nil {
t.Fatal("Failed to create cookie reply:", err)
}
if !generator.ConsumeReply(reply) {
t.Fatal("Failed to consume cookie reply")
}
}()
// check mac2
checkMAC2 := func(msg []byte) {
generator.AddMacs(msg)
if !checker.CheckMAC1(msg) {
t.Fatal("MAC1 generation/verification failed")
}
if !checker.CheckMAC2(msg, src) {
t.Fatal("MAC2 generation/verification failed")
}
msg[5] ^= 0x20
if checker.CheckMAC1(msg) {
t.Fatal("MAC1 generation/verification failed")
}
if checker.CheckMAC2(msg, src) {
t.Fatal("MAC2 generation/verification failed")
}
msg[5] ^= 0x20
srcBad1 := []byte{192, 168, 13, 37, 40, 1}
if checker.CheckMAC2(msg, srcBad1) {
t.Fatal("MAC2 generation/verification failed")
}
srcBad2 := []byte{192, 168, 13, 38, 40, 1}
if checker.CheckMAC2(msg, srcBad2) {
t.Fatal("MAC2 generation/verification failed")
}
}
checkMAC2([]byte{
0x03, 0x31, 0xb9, 0x9e, 0xb0, 0x2a, 0x54, 0xa3,
0xc1, 0x3f, 0xb4, 0x96, 0x16, 0xb9, 0x25, 0x15,
0x3d, 0x3a, 0x82, 0xf9, 0x58, 0x36, 0x86, 0x3f,
0x13, 0x2f, 0xfe, 0xb2, 0x53, 0x20, 0x8c, 0x3f,
0xba, 0xeb, 0xfb, 0x4b, 0x1b, 0x22, 0x02, 0x69,
0x2c, 0x90, 0xbc, 0xdc, 0xcf, 0xcf, 0x85, 0xeb,
0x62, 0x66, 0x6f, 0xe8, 0xe1, 0xa6, 0xa8, 0x4c,
0xa0, 0x04, 0x23, 0x15, 0x42, 0xac, 0xfa, 0x38,
})
checkMAC2([]byte{
0x0e, 0x2f, 0x0e, 0xa9, 0x29, 0x03, 0xe1, 0xf3,
0x24, 0x01, 0x75, 0xad, 0x16, 0xa5, 0x66, 0x85,
0xca, 0x66, 0xe0, 0xbd, 0xc6, 0x34, 0xd8, 0x84,
0x09, 0x9a, 0x58, 0x14, 0xfb, 0x05, 0xda, 0xf5,
0x90, 0xf5, 0x0c, 0x4e, 0x22, 0x10, 0xc9, 0x85,
0x0f, 0xe3, 0x77, 0x35, 0xe9, 0x6b, 0xc2, 0x55,
0x32, 0x46, 0xae, 0x25, 0xe0, 0xe3, 0x37, 0x7a,
0x4b, 0x71, 0xcc, 0xfc, 0x91, 0xdf, 0xd6, 0xca,
0xfe, 0xee, 0xce, 0x3f, 0x77, 0xa2, 0xfd, 0x59,
0x8e, 0x73, 0x0a, 0x8d, 0x5c, 0x24, 0x14, 0xca,
0x38, 0x91, 0xb8, 0x2c, 0x8c, 0xa2, 0x65, 0x7b,
0xbc, 0x49, 0xbc, 0xb5, 0x58, 0xfc, 0xe3, 0xd7,
0x02, 0xcf, 0xf7, 0x4c, 0x60, 0x91, 0xed, 0x55,
0xe9, 0xf9, 0xfe, 0xd1, 0x44, 0x2c, 0x75, 0xf2,
0xb3, 0x5d, 0x7b, 0x27, 0x56, 0xc0, 0x48, 0x4f,
0xb0, 0xba, 0xe4, 0x7d, 0xd0, 0xaa, 0xcd, 0x3d,
0xe3, 0x50, 0xd2, 0xcf, 0xb9, 0xfa, 0x4b, 0x2d,
0xc6, 0xdf, 0x3b, 0x32, 0x98, 0x45, 0xe6, 0x8f,
0x1c, 0x5c, 0xa2, 0x20, 0x7d, 0x1c, 0x28, 0xc2,
0xd4, 0xa1, 0xe0, 0x21, 0x52, 0x8f, 0x1c, 0xd0,
0x62, 0x97, 0x48, 0xbb, 0xf4, 0xa9, 0xcb, 0x35,
0xf2, 0x07, 0xd3, 0x50, 0xd8, 0xa9, 0xc5, 0x9a,
0x0f, 0xbd, 0x37, 0xaf, 0xe1, 0x45, 0x19, 0xee,
0x41, 0xf3, 0xf7, 0xe5, 0xe0, 0x30, 0x3f, 0xbe,
0x3d, 0x39, 0x64, 0x00, 0x7a, 0x1a, 0x51, 0x5e,
0xe1, 0x70, 0x0b, 0xb9, 0x77, 0x5a, 0xf0, 0xc4,
0x8a, 0xa1, 0x3a, 0x77, 0x1a, 0xe0, 0xc2, 0x06,
0x91, 0xd5, 0xe9, 0x1c, 0xd3, 0xfe, 0xab, 0x93,
0x1a, 0x0a, 0x4c, 0xbb, 0xf0, 0xff, 0xdc, 0xaa,
0x61, 0x73, 0xcb, 0x03, 0x4b, 0x71, 0x68, 0x64,
0x3d, 0x82, 0x31, 0x41, 0xd7, 0x8b, 0x22, 0x7b,
0x7d, 0xa1, 0xd5, 0x85, 0x6d, 0xf0, 0x1b, 0xaa,
})
}

View file

@ -1,20 +1,25 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"context"
"errors"
"net/netip"
"runtime" "runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"golang.zx2c4.com/wireguard/conn" "github.com/sagernet/sing/service"
"golang.zx2c4.com/wireguard/ratelimiter" "github.com/sagernet/sing/service/pause"
"golang.zx2c4.com/wireguard/rwcancel" "github.com/sagernet/wireguard-go/conn"
"golang.zx2c4.com/wireguard/tun" "github.com/sagernet/wireguard-go/ratelimiter"
"github.com/sagernet/wireguard-go/rwcancel"
"github.com/sagernet/wireguard-go/tun"
) )
type Device struct { type Device struct {
@ -56,8 +61,12 @@ type Device struct {
peers struct { peers struct {
sync.RWMutex // protects keyMap sync.RWMutex // protects keyMap
keyMap map[NoisePublicKey]*Peer keyMap map[NoisePublicKey]*Peer
lookupFunc PeerLookupFunc // or nil if unused
} }
peerStateFn atomic.Pointer[PeerSessionStateFunc] // observes peer session state changes, nil if unset
priorityMsgFn atomic.Pointer[PeerPriorityMessageFunc] // returns a priority message to be sent around session establishment, nil if unset
rate struct { rate struct {
underLoadUntil atomic.Int64 underLoadUntil atomic.Int64
limiter ratelimiter.Ratelimiter limiter ratelimiter.Ratelimiter
@ -68,11 +77,11 @@ type Device struct {
cookieChecker CookieChecker cookieChecker CookieChecker
pool struct { pool struct {
inboundElementsContainer *WaitPool inboundElementsContainer *sync.Pool
outboundElementsContainer *WaitPool outboundElementsContainer *sync.Pool
messageBuffers *WaitPool messageBuffers *WaitPool
inboundElements *WaitPool inboundElements *sync.Pool
outboundElements *WaitPool outboundElements *sync.Pool
} }
queue struct { queue struct {
@ -86,9 +95,47 @@ type Device struct {
mtu atomic.Int32 mtu atomic.Int32
} }
ipcMutex sync.RWMutex ipcMutex sync.RWMutex
closed chan struct{} closed chan struct{}
log *Logger log *Logger
pauseManager pause.Manager
// lx: AmneziaWG obfuscation state (grafted from amneziawg-go).
junk struct {
min int
max int
count int
}
headers struct {
init *magicHeader
cookie *magicHeader
response *magicHeader
transport *magicHeader
}
paddings struct {
init int
response int
cookie int
transport int
}
ipackets [5]*obfChain
// lx: SPEC 041 — passive self-heal state: reopen the bind once (fresh
// ephemeral port when freshPort is set) and immediately re-initiate, to
// heal dead per-flow path state (an expired NAT mapping or a poisoned DPI
// flow entry) that otherwise pins every retry to the same dead 5-tuple
// until a manual reconnect. The whole mechanism — three triggers (giveup /
// early / nudge) sharing this state and its debounce — lives in
// lx_giveup_rebind.go. Enabled by default; sing-box decides freshPort
// from whether the user pinned listen_port.
giveUpRebind struct {
enabled atomic.Bool
freshPort atomic.Bool
last atomic.Int64 // unix seconds of the last rebind (debounce)
}
} }
// deviceState represents the state of a Device. // deviceState represents the state of a Device.
@ -162,7 +209,8 @@ func (device *Device) changeState(want deviceState) (err error) {
err = errDown err = errDown
} }
} }
device.log.Verbosef("Interface state was %s, requested %s, now %s", old, want, device.deviceState()) device.log.Verbosef(
"Interface state was %s, requested %s, now %s", old, want, device.deviceState())
return return
} }
@ -179,14 +227,22 @@ func (device *Device) upLocked() error {
device.ipcMutex.Lock() device.ipcMutex.Lock()
defer device.ipcMutex.Unlock() defer device.ipcMutex.Unlock()
// Collect peers under RLock and then release before calling into them,
// because SendKeepalive can reach CreateMessageInitiation which acquires
// staticIdentity.RLock; holding peers.RLock across that path would
// invert the staticIdentity < peers hierarchy (see lock-ordering.md).
device.peers.RLock() device.peers.RLock()
peers := make([]*Peer, 0, len(device.peers.keyMap))
for _, peer := range device.peers.keyMap { for _, peer := range device.peers.keyMap {
peers = append(peers, peer)
}
device.peers.RUnlock()
for _, peer := range peers {
peer.Start() peer.Start()
if peer.persistentKeepaliveInterval.Load() > 0 { if peer.persistentKeepaliveInterval.Load() > 0 {
peer.SendKeepalive() peer.SendKeepalive()
} }
} }
device.peers.RUnlock()
return nil return nil
} }
@ -281,8 +337,10 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error {
return nil return nil
} }
func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device {
device := new(Device) device := new(Device)
device.pauseManager = service.FromContext[pause.Manager](ctx)
device.giveUpRebind.enabled.Store(true) // lx: SPEC 041 — self-heal on by default
device.state.state.Store(uint32(deviceStateDown)) device.state.state.Store(uint32(deviceStateDown))
device.closed = make(chan struct{}) device.closed = make(chan struct{})
device.log = logger device.log = logger
@ -298,6 +356,11 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
device.rate.limiter.Init() device.rate.limiter.Init()
device.indexTable.Init() device.indexTable.Init()
device.headers.init = &magicHeader{start: MessageInitiationType, end: MessageInitiationType}
device.headers.response = &magicHeader{start: MessageResponseType, end: MessageResponseType}
device.headers.cookie = &magicHeader{start: MessageCookieReplyType, end: MessageCookieReplyType}
device.headers.transport = &magicHeader{start: MessageTransportType, end: MessageTransportType}
device.PopulatePools() device.PopulatePools()
// create queues // create queues
@ -308,10 +371,12 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
// start workers // start workers
cpus := runtime.NumCPU() if workers == 0 {
workers = runtime.NumCPU()
}
device.state.stopping.Wait() device.state.stopping.Wait()
device.queue.encryption.wg.Add(cpus) // One for each RoutineHandshake device.queue.encryption.wg.Add(workers) // One for each RoutineHandshake
for i := 0; i < cpus; i++ { for i := 0; i < workers; i++ {
go device.RoutineEncryption(i + 1) go device.RoutineEncryption(i + 1)
go device.RoutineDecryption(i + 1) go device.RoutineDecryption(i + 1)
go device.RoutineHandshake(i + 1) go device.RoutineHandshake(i + 1)
@ -338,13 +403,66 @@ func (device *Device) BatchSize() int {
return size return size
} }
// LookupPeer looks up a peer by its public key.
//
// If the peer does not exist and a [PeerLookupFunc] is set (via
// [Device.SetPeerLookupFunc]), then that function is used to create the peer
// before returning it. Peers created via this mechanism exist only until their
// state machine reaches idle, and then the peers are removed.
//
// If the peer does not exist and no [PeerLookupFunc] is set, nil is returned.
//
// Use [Device.LookupActivePeer] to only return already-existing peers, without
// using a [PeerLookupFunc].
func (device *Device) LookupPeer(pk NoisePublicKey) *Peer { func (device *Device) LookupPeer(pk NoisePublicKey) *Peer {
device.peers.RLock() device.peers.RLock()
defer device.peers.RUnlock() p, ok := device.peers.keyMap[pk]
lookupFunc := device.peers.lookupFunc
device.peers.RUnlock()
if ok || lookupFunc == nil {
return p
}
return device.peers.keyMap[pk] conf, ok := lookupFunc(pk)
if !ok || conf == nil {
return nil
}
p, err := device.NewPeer(pk)
if err != nil {
if errors.Is(err, errAddExistingPeer) {
device.peers.RLock()
defer device.peers.RUnlock()
return device.peers.keyMap[pk]
}
device.log.Errorf("Failed to create peer: %v", err)
return nil
}
p.SetAllowedIPs(conf.AllowedIPs)
p.deleteOnIdle = true
if conf.Endpoint != nil {
p.SetEndpointFromPacket(conf.Endpoint)
}
p.Start()
return p
} }
// LookupActivePeer looks up a peer by its public key.
//
// Unlike [Device.LookupPeer], this function does not use a [PeerLookupFunc] to
// create the peer if it does not already exist.
//
// If the peer does not exist or was created lazily via [PeerLookupFunc]
// and has subsequently idled away, it returns (nil, false).
func (device *Device) LookupActivePeer(pk NoisePublicKey) (_ *Peer, ok bool) {
device.peers.RLock()
defer device.peers.RUnlock()
p, ok := device.peers.keyMap[pk]
return p, ok
}
var errAddExistingPeer = errors.New("adding existing peer")
func (device *Device) RemovePeer(key NoisePublicKey) { func (device *Device) RemovePeer(key NoisePublicKey) {
device.peers.Lock() device.peers.Lock()
defer device.peers.Unlock() defer device.peers.Unlock()
@ -367,6 +485,151 @@ func (device *Device) RemoveAllPeers() {
device.peers.keyMap = make(map[NoisePublicKey]*Peer) device.peers.keyMap = make(map[NoisePublicKey]*Peer)
} }
// RemoveMatchingPeers removes all peers for which shouldRemove returns true.
//
// It returns the number of peers removed.
func (device *Device) RemoveMatchingPeers(shouldRemove func(NoisePublicKey) bool) (numRemoved int) {
device.peers.Lock()
defer device.peers.Unlock()
for key, peer := range device.peers.keyMap {
if shouldRemove(key) {
removePeerLocked(device, peer, key)
numRemoved++
}
}
return numRemoved
}
// NewPeerConfig are the configuration parameters for a new peer created via a
// [PeerLookupFunc] func.
type NewPeerConfig struct {
// AllowedIPs is the initial set of allowed IPs for the new peer.
AllowedIPs []netip.Prefix
// Endpoint, if non-nil, sets the initial endpoint for newly
// created peers.
Endpoint conn.Endpoint
}
// PeerLookupFunc is the type of function used to look up peers by public key
// when receiving packets for unknown peers.
//
// If it returns nil, the peer is not known.
//
// Otherwise, returning non-nil signals that wireguard-go should create the peer
// with the provided allowed IPs.
//
// See [Device.SetPeerLookupFunc] and [Device.LookupPeer].
type PeerLookupFunc func(NoisePublicKey) (_ *NewPeerConfig, ok bool)
// PeerByIPPacketFunc is the type of function used to look up a peer to send to
// for a given src/dst IP pair. The ipPkt parameter is the raw IP packet being
// routed; callers needing transport-layer ports or other header fields may parse
// them from ipPkt, but must handle IP fragmentation (ports may be absent on
// non-first fragments) and protocols that do not use ports (e.g. ICMP).
//
// Except for experimental use cases, dst is the only address
// that should be relied upon when looking up a peer.
//
// If it returns ok=false, the peer is not known.
//
// See [Device.SetPeerByIPPacketFunc] and [Device.SetPeerLookupFunc].
type PeerByIPPacketFunc func(src, dst netip.Addr, ipPkt []byte) (_ NoisePublicKey, ok bool)
// PeerSessionState is the current WireGuard session state for a peer.
type PeerSessionState uint8
const (
// PeerSessionNone means there is no handshake in progress and no session key
// material retained for this peer.
PeerSessionNone PeerSessionState = iota
// PeerSessionHandshake means a handshake is in progress for this peer, but
// there is not currently a usable WireGuard session.
PeerSessionHandshake
// PeerSessionEstablished means the peer has a completed WireGuard session
// with usable session key material.
PeerSessionEstablished
// PeerSessionExpired means the peer's session key material is no longer
// considered usable, but final key cleanup or lazy peer removal may not have
// happened yet.
PeerSessionExpired
)
// PeerSessionStateFunc is called when a peer's WireGuard session state changes.
//
// Calls are serialized per peer and delivered in that peer's transition order. The
// callback must be cheap and must not call back into Device.
type PeerSessionStateFunc func(peer NoisePublicKey, state PeerSessionState)
// SetPeerLookupFunc sets the function used to look up peers by public key
// when receiving packets for unknown peers.
func (device *Device) SetPeerLookupFunc(f PeerLookupFunc) {
device.peers.Lock()
defer device.peers.Unlock()
device.peers.lookupFunc = f
}
// SetPeerByIPPacketFunc sets the function used to look up peers by IP address
// when sending packets to unknown peers.
func (device *Device) SetPeerByIPPacketFunc(f PeerByIPPacketFunc) {
device.allowedips.mu.Lock()
defer device.allowedips.mu.Unlock()
device.allowedips.peerByIPPacketFunc = f
device.allowedips.device = device
}
// SetSessionStateFunc sets the function used to observe peer WireGuard session
// state changes.
//
// It does not replay current state. Callers that need a complete view should set
// it before peers are started or lazily created, and maintain any snapshots,
// sequence numbers, and pubsub state outside wireguard-go.
//
// The callback must be concurrent-safe and must not call back into Device.
func (device *Device) SetSessionStateFunc(f PeerSessionStateFunc) {
if f == nil {
device.peerStateFn.Store(nil)
return
}
device.peerStateFn.Store(&f)
}
// MaxPriorityMessageContentSize is the maximum size of a message returned by a
// [PeerPriorityMessageFunc]. It's a power of 2 that leaves significant space
// when accounting for all WireGuard overhead and encapsulating network protocol
// headers. Future adjustments to this value should consider all these overheads
// and any [conn.Bind] implementation limitations.
const MaxPriorityMessageContentSize = 512
// PeerPriorityMessageFunc is called when a peer's WireGuard session keypair is
// established (or re-keyed) for forward data transmission.
//
// The returned message is transmitted to the peer in priority fashion. Priority
// means it cannot be evicted from the staged packet queue by non-priority
// (read from [tun.Device]) packets. It avoids the staged queue altogether.
//
// The callback must be cheap and must not call back into [Device]. A zero length
// message or a message whose length exceeds [MaxPriorityMessageContentSize] will
// be silently dropped. Message should start with an IPv4 or IPv6 header as it
// is subject to allowed IPs lookup on the receiver, same as any other transport
// message.
type PeerPriorityMessageFunc func(peer NoisePublicKey) (msg []byte)
// SetPriorityMessageOnEstablishmentFunc sets a function to be used for sending
// a priority message around session establishment. See [PeerPriorityMessageFunc]
// docs for more details. A nil value clears any previously set value.
func (device *Device) SetPriorityMessageOnEstablishmentFunc(f PeerPriorityMessageFunc) {
if f == nil {
device.priorityMsgFn.Store(nil)
return
}
device.priorityMsgFn.Store(&f)
}
func (device *Device) Close() { func (device *Device) Close() {
device.state.Lock() device.state.Lock()
defer device.state.Unlock() defer device.state.Unlock()
@ -408,16 +671,25 @@ func (device *Device) SendKeepalivesToPeersWithCurrentKeypair() {
return return
} }
// Collect the set of peers to keepalive under peers.RLock, then release
// before invoking SendKeepalive. SendKeepalive can reach
// CreateMessageInitiation which acquires staticIdentity.RLock; holding
// peers.RLock across that path would invert the
// staticIdentity < peers hierarchy (see lock-ordering.md).
var peers []*Peer
device.peers.RLock() device.peers.RLock()
for _, peer := range device.peers.keyMap { for _, peer := range device.peers.keyMap {
peer.keypairs.RLock() peer.keypairs.RLock()
sendKeepalive := peer.keypairs.current != nil && !peer.keypairs.current.created.Add(RejectAfterTime).Before(time.Now()) sendKeepalive := peer.keypairs.current != nil && !peer.keypairs.current.created.Add(RejectAfterTime).Before(time.Now())
peer.keypairs.RUnlock() peer.keypairs.RUnlock()
if sendKeepalive { if sendKeepalive {
peer.SendKeepalive() peers = append(peers, peer)
} }
} }
device.peers.RUnlock() device.peers.RUnlock()
for _, peer := range peers {
peer.SendKeepalive()
}
} }
// closeBindLocked closes the device's net.bind. // closeBindLocked closes the device's net.bind.

View file

@ -1,476 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"math/rand"
"net/netip"
"os"
"runtime"
"runtime/pprof"
"sync"
"sync/atomic"
"testing"
"time"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/conn/bindtest"
"golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/tun/tuntest"
)
// uapiCfg returns a string that contains cfg formatted use with IpcSet.
// cfg is a series of alternating key/value strings.
// uapiCfg exists because editors and humans like to insert
// whitespace into configs, which can cause failures, some of which are silent.
// For example, a leading blank newline causes the remainder
// of the config to be silently ignored.
func uapiCfg(cfg ...string) string {
if len(cfg)%2 != 0 {
panic("odd number of args to uapiReader")
}
buf := new(bytes.Buffer)
for i, s := range cfg {
buf.WriteString(s)
sep := byte('\n')
if i%2 == 0 {
sep = '='
}
buf.WriteByte(sep)
}
return buf.String()
}
// genConfigs generates a pair of configs that connect to each other.
// The configs use distinct, probably-usable ports.
func genConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) {
var key1, key2 NoisePrivateKey
_, err := rand.Read(key1[:])
if err != nil {
tb.Errorf("unable to generate private key random bytes: %v", err)
}
_, err = rand.Read(key2[:])
if err != nil {
tb.Errorf("unable to generate private key random bytes: %v", err)
}
pub1, pub2 := key1.publicKey(), key2.publicKey()
cfgs[0] = uapiCfg(
"private_key", hex.EncodeToString(key1[:]),
"listen_port", "0",
"replace_peers", "true",
"public_key", hex.EncodeToString(pub2[:]),
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "1.0.0.2/32",
)
endpointCfgs[0] = uapiCfg(
"public_key", hex.EncodeToString(pub2[:]),
"endpoint", "127.0.0.1:%d",
)
cfgs[1] = uapiCfg(
"private_key", hex.EncodeToString(key2[:]),
"listen_port", "0",
"replace_peers", "true",
"public_key", hex.EncodeToString(pub1[:]),
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "1.0.0.1/32",
)
endpointCfgs[1] = uapiCfg(
"public_key", hex.EncodeToString(pub1[:]),
"endpoint", "127.0.0.1:%d",
)
return
}
// A testPair is a pair of testPeers.
type testPair [2]testPeer
// A testPeer is a peer used for testing.
type testPeer struct {
tun *tuntest.ChannelTUN
dev *Device
ip netip.Addr
}
type SendDirection bool
const (
Ping SendDirection = true
Pong SendDirection = false
)
func (d SendDirection) String() string {
if d == Ping {
return "ping"
}
return "pong"
}
func (pair *testPair) Send(tb testing.TB, ping SendDirection, done chan struct{}) {
tb.Helper()
p0, p1 := pair[0], pair[1]
if !ping {
// pong is the new ping
p0, p1 = p1, p0
}
msg := tuntest.Ping(p0.ip, p1.ip)
p1.tun.Outbound <- msg
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
var err error
select {
case msgRecv := <-p0.tun.Inbound:
if !bytes.Equal(msg, msgRecv) {
err = fmt.Errorf("%s did not transit correctly", ping)
}
case <-timer.C:
err = fmt.Errorf("%s did not transit", ping)
case <-done:
}
if err != nil {
// The error may have occurred because the test is done.
select {
case <-done:
return
default:
}
// Real error.
tb.Error(err)
}
}
// genTestPair creates a testPair.
func genTestPair(tb testing.TB, realSocket bool) (pair testPair) {
cfg, endpointCfg := genConfigs(tb)
var binds [2]conn.Bind
if realSocket {
binds[0], binds[1] = conn.NewDefaultBind(), conn.NewDefaultBind()
} else {
binds = bindtest.NewChannelBinds()
}
// Bring up a ChannelTun for each config.
for i := range pair {
p := &pair[i]
p.tun = tuntest.NewChannelTUN()
p.ip = netip.AddrFrom4([4]byte{1, 0, 0, byte(i + 1)})
level := LogLevelVerbose
if _, ok := tb.(*testing.B); ok && !testing.Verbose() {
level = LogLevelError
}
p.dev = NewDevice(p.tun.TUN(), binds[i], NewLogger(level, fmt.Sprintf("dev%d: ", i)))
if err := p.dev.IpcSet(cfg[i]); err != nil {
tb.Errorf("failed to configure device %d: %v", i, err)
p.dev.Close()
continue
}
if err := p.dev.Up(); err != nil {
tb.Errorf("failed to bring up device %d: %v", i, err)
p.dev.Close()
continue
}
endpointCfg[i^1] = fmt.Sprintf(endpointCfg[i^1], p.dev.net.port)
}
for i := range pair {
p := &pair[i]
if err := p.dev.IpcSet(endpointCfg[i]); err != nil {
tb.Errorf("failed to configure device endpoint %d: %v", i, err)
p.dev.Close()
continue
}
// The device is ready. Close it when the test completes.
tb.Cleanup(p.dev.Close)
}
return
}
func TestTwoDevicePing(t *testing.T) {
goroutineLeakCheck(t)
pair := genTestPair(t, true)
t.Run("ping 1.0.0.1", func(t *testing.T) {
pair.Send(t, Ping, nil)
})
t.Run("ping 1.0.0.2", func(t *testing.T) {
pair.Send(t, Pong, nil)
})
}
func TestUpDown(t *testing.T) {
goroutineLeakCheck(t)
const itrials = 50
const otrials = 10
for n := 0; n < otrials; n++ {
pair := genTestPair(t, false)
for i := range pair {
for k := range pair[i].dev.peers.keyMap {
pair[i].dev.IpcSet(fmt.Sprintf("public_key=%s\npersistent_keepalive_interval=1\n", hex.EncodeToString(k[:])))
}
}
var wg sync.WaitGroup
wg.Add(len(pair))
for i := range pair {
go func(d *Device) {
defer wg.Done()
for i := 0; i < itrials; i++ {
if err := d.Up(); err != nil {
t.Errorf("failed up bring up device: %v", err)
}
time.Sleep(time.Duration(rand.Intn(int(time.Nanosecond * (0x10000 - 1)))))
if err := d.Down(); err != nil {
t.Errorf("failed to bring down device: %v", err)
}
time.Sleep(time.Duration(rand.Intn(int(time.Nanosecond * (0x10000 - 1)))))
}
}(pair[i].dev)
}
wg.Wait()
for i := range pair {
pair[i].dev.Up()
pair[i].dev.Close()
}
}
}
// TestConcurrencySafety does other things concurrently with tunnel use.
// It is intended to be used with the race detector to catch data races.
func TestConcurrencySafety(t *testing.T) {
pair := genTestPair(t, true)
done := make(chan struct{})
const warmupIters = 10
var warmup sync.WaitGroup
warmup.Add(warmupIters)
go func() {
// Send data continuously back and forth until we're done.
// Note that we may continue to attempt to send data
// even after done is closed.
i := warmupIters
for ping := Ping; ; ping = !ping {
pair.Send(t, ping, done)
select {
case <-done:
return
default:
}
if i > 0 {
warmup.Done()
i--
}
}
}()
warmup.Wait()
applyCfg := func(cfg string) {
err := pair[0].dev.IpcSet(cfg)
if err != nil {
t.Fatal(err)
}
}
// Change persistent_keepalive_interval concurrently with tunnel use.
t.Run("persistentKeepaliveInterval", func(t *testing.T) {
var pub NoisePublicKey
for key := range pair[0].dev.peers.keyMap {
pub = key
break
}
cfg := uapiCfg(
"public_key", hex.EncodeToString(pub[:]),
"persistent_keepalive_interval", "1",
)
for i := 0; i < 1000; i++ {
applyCfg(cfg)
}
})
// Change private keys concurrently with tunnel use.
t.Run("privateKey", func(t *testing.T) {
bad := uapiCfg("private_key", "7777777777777777777777777777777777777777777777777777777777777777")
good := uapiCfg("private_key", hex.EncodeToString(pair[0].dev.staticIdentity.privateKey[:]))
// Set iters to a large number like 1000 to flush out data races quickly.
// Don't leave it large. That can cause logical races
// in which the handshake is interleaved with key changes
// such that the private key appears to be unchanging but
// other state gets reset, which can cause handshake failures like
// "Received packet with invalid mac1".
const iters = 1
for i := 0; i < iters; i++ {
applyCfg(bad)
applyCfg(good)
}
})
// Perform bind updates and keepalive sends concurrently with tunnel use.
t.Run("bindUpdate and keepalive", func(t *testing.T) {
const iters = 10
for i := 0; i < iters; i++ {
for _, peer := range pair {
peer.dev.BindUpdate()
peer.dev.SendKeepalivesToPeersWithCurrentKeypair()
}
}
})
close(done)
}
func BenchmarkLatency(b *testing.B) {
pair := genTestPair(b, true)
// Establish a connection.
pair.Send(b, Ping, nil)
pair.Send(b, Pong, nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
pair.Send(b, Ping, nil)
pair.Send(b, Pong, nil)
}
}
func BenchmarkThroughput(b *testing.B) {
pair := genTestPair(b, true)
// Establish a connection.
pair.Send(b, Ping, nil)
pair.Send(b, Pong, nil)
// Measure how long it takes to receive b.N packets,
// starting when we receive the first packet.
var recv atomic.Uint64
var elapsed time.Duration
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
var start time.Time
for {
<-pair[0].tun.Inbound
new := recv.Add(1)
if new == 1 {
start = time.Now()
}
// Careful! Don't change this to else if; b.N can be equal to 1.
if new == uint64(b.N) {
elapsed = time.Since(start)
return
}
}
}()
// Send packets as fast as we can until we've received enough.
ping := tuntest.Ping(pair[0].ip, pair[1].ip)
pingc := pair[1].tun.Outbound
var sent uint64
for recv.Load() != uint64(b.N) {
sent++
pingc <- ping
}
wg.Wait()
b.ReportMetric(float64(elapsed)/float64(b.N), "ns/op")
b.ReportMetric(1-float64(b.N)/float64(sent), "packet-loss")
}
func BenchmarkUAPIGet(b *testing.B) {
pair := genTestPair(b, true)
pair.Send(b, Ping, nil)
pair.Send(b, Pong, nil)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
pair[0].dev.IpcGetOperation(io.Discard)
}
}
func goroutineLeakCheck(t *testing.T) {
goroutines := func() (int, []byte) {
p := pprof.Lookup("goroutine")
b := new(bytes.Buffer)
p.WriteTo(b, 1)
return p.Count(), b.Bytes()
}
startGoroutines, startStacks := goroutines()
t.Cleanup(func() {
if t.Failed() {
return
}
// Give goroutines time to exit, if they need it.
for i := 0; i < 10000; i++ {
if runtime.NumGoroutine() <= startGoroutines {
return
}
time.Sleep(1 * time.Millisecond)
}
endGoroutines, endStacks := goroutines()
t.Logf("starting stacks:\n%s\n", startStacks)
t.Logf("ending stacks:\n%s\n", endStacks)
t.Fatalf("expected %d goroutines, got %d, leak?", startGoroutines, endGoroutines)
})
}
type fakeBindSized struct {
size int
}
func (b *fakeBindSized) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
return nil, 0, nil
}
func (b *fakeBindSized) Close() error { return nil }
func (b *fakeBindSized) SetMark(mark uint32) error { return nil }
func (b *fakeBindSized) Send(bufs [][]byte, ep conn.Endpoint) error { return nil }
func (b *fakeBindSized) ParseEndpoint(s string) (conn.Endpoint, error) { return nil, nil }
func (b *fakeBindSized) BatchSize() int { return b.size }
type fakeTUNDeviceSized struct {
size int
}
func (t *fakeTUNDeviceSized) File() *os.File { return nil }
func (t *fakeTUNDeviceSized) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
return 0, nil
}
func (t *fakeTUNDeviceSized) Write(bufs [][]byte, offset int) (int, error) { return 0, nil }
func (t *fakeTUNDeviceSized) MTU() (int, error) { return 0, nil }
func (t *fakeTUNDeviceSized) Name() (string, error) { return "", nil }
func (t *fakeTUNDeviceSized) Events() <-chan tun.Event { return nil }
func (t *fakeTUNDeviceSized) Close() error { return nil }
func (t *fakeTUNDeviceSized) BatchSize() int { return t.size }
func TestBatchSize(t *testing.T) {
d := Device{}
d.net.bind = &fakeBindSized{1}
d.tun.device = &fakeTUNDeviceSized{1}
if want, got := 1, d.BatchSize(); got != want {
t.Errorf("expected batch size %d, got %d", want, got)
}
d.net.bind = &fakeBindSized{1}
d.tun.device = &fakeTUNDeviceSized{128}
if want, got := 128, d.BatchSize(); got != want {
t.Errorf("expected batch size %d, got %d", want, got)
}
d.net.bind = &fakeBindSized{128}
d.tun.device = &fakeTUNDeviceSized{1}
if want, got := 128, d.BatchSize(); got != want {
t.Errorf("expected batch size %d, got %d", want, got)
}
d.net.bind = &fakeBindSized{128}
d.tun.device = &fakeTUNDeviceSized{128}
if want, got := 128, d.BatchSize(); got != want {
t.Errorf("expected batch size %d, got %d", want, got)
}
}

View file

@ -1,49 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"math/rand"
"net/netip"
)
type DummyEndpoint struct {
src, dst netip.Addr
}
func CreateDummyEndpoint() (*DummyEndpoint, error) {
var src, dst [16]byte
if _, err := rand.Read(src[:]); err != nil {
return nil, err
}
_, err := rand.Read(dst[:])
return &DummyEndpoint{netip.AddrFrom16(src), netip.AddrFrom16(dst)}, err
}
func (e *DummyEndpoint) ClearSrc() {}
func (e *DummyEndpoint) SrcToString() string {
return netip.AddrPortFrom(e.SrcIP(), 1000).String()
}
func (e *DummyEndpoint) DstToString() string {
return netip.AddrPortFrom(e.DstIP(), 1000).String()
}
func (e *DummyEndpoint) DstToBytes() []byte {
out := e.DstIP().AsSlice()
out = append(out, byte(1000&0xff))
out = append(out, byte((1000>>8)&0xff))
return out
}
func (e *DummyEndpoint) DstIP() netip.Addr {
return e.dst
}
func (e *DummyEndpoint) SrcIP() netip.Addr {
return e.src
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,85 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"encoding/hex"
"testing"
"golang.org/x/crypto/blake2s"
)
type KDFTest struct {
key string
input string
t0 string
t1 string
t2 string
}
func assertEquals(t *testing.T, a, b string) {
if a != b {
t.Fatal("expected", a, "=", b)
}
}
func TestKDF(t *testing.T) {
tests := []KDFTest{
{
key: "746573742d6b6579",
input: "746573742d696e707574",
t0: "6f0e5ad38daba1bea8a0d213688736f19763239305e0f58aba697f9ffc41c633",
t1: "df1194df20802a4fe594cde27e92991c8cae66c366e8106aaa937a55fa371e8a",
t2: "fac6e2745a325f5dc5d11a5b165aad08b0ada28e7b4e666b7c077934a4d76c24",
},
{
key: "776972656775617264",
input: "776972656775617264",
t0: "491d43bbfdaa8750aaf535e334ecbfe5129967cd64635101c566d4caefda96e8",
t1: "1e71a379baefd8a79aa4662212fcafe19a23e2b609a3db7d6bcba8f560e3d25f",
t2: "31e1ae48bddfbe5de38f295e5452b1909a1b4e38e183926af3780b0c1e1f0160",
},
{
key: "",
input: "",
t0: "8387b46bf43eccfcf349552a095d8315c4055beb90208fb1be23b894bc2ed5d0",
t1: "58a0e5f6faefccf4807bff1f05fa8a9217945762040bcec2f4b4a62bdfe0e86e",
t2: "0ce6ea98ec548f8e281e93e32db65621c45eb18dc6f0a7ad94178610a2f7338e",
},
}
var t0, t1, t2 [blake2s.Size]byte
for _, test := range tests {
key, _ := hex.DecodeString(test.key)
input, _ := hex.DecodeString(test.input)
KDF3(&t0, &t1, &t2, key, input)
t0s := hex.EncodeToString(t0[:])
t1s := hex.EncodeToString(t1[:])
t2s := hex.EncodeToString(t2[:])
assertEquals(t, t0s, test.t0)
assertEquals(t, t1s, test.t1)
assertEquals(t, t2s, test.t2)
}
for _, test := range tests {
key, _ := hex.DecodeString(test.key)
input, _ := hex.DecodeString(test.input)
KDF2(&t0, &t1, key, input)
t0s := hex.EncodeToString(t0[:])
t1s := hex.EncodeToString(t1[:])
assertEquals(t, t0s, test.t0)
assertEquals(t, t1s, test.t1)
}
for _, test := range tests {
key, _ := hex.DecodeString(test.key)
input, _ := hex.DecodeString(test.input)
KDF1(&t0, key, input)
t0s := hex.EncodeToString(t0[:])
assertEquals(t, t0s, test.t0)
}
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -11,7 +11,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"golang.zx2c4.com/wireguard/replay" "github.com/sagernet/wireguard-go/replay"
) )
/* Due to limitations in Go and /x/crypto there is currently /* Due to limitations in Go and /x/crypto there is currently

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -0,0 +1,58 @@
/* SPDX-License-Identifier: MIT
*
* lx: SPEC 041 v2 behavioural red/green test for the EARLY give-up rebind.
* Field failure mode (the v1 field leftover, dump 2026-08-01): the v1 give-up
* rebind heals a dead 5-tuple, but only at ~90s after the first demand while
* the user pings within the first 5-35s after device wake and sees every node
* in ERR. The early trigger fires from the retry branch once >=3 initiations
* went unanswered AND the session is provably dead (no live keypair, or last
* handshake older than RejectAfterTime), shrinking the ERR window to ~15-20s.
*
* The test reuses the v1 harness (gateBind: first socket generation blackholes
* every send) and drives the peer to the 3rd retry expiry the state a real
* ~15s of unanswered retries ends in. Post-fix the retry branch rebinds and
* the tunnel comes up; pre-fix (v1 base) the retry keeps dying in the first
* socket generation and only the ~90s give-up would heal, so the packet never
* arrives within the test window.
*
* This file deliberately uses NO post-fix API, so it compiles and runs RED on
* the v1 base commit.
*/
package device
import (
"testing"
"time"
)
// TestEarlyRebindSelfHeal: dead first socket, cold session (no keypair yet),
// 3rd retry expiry fires — the tunnel must come up and deliver traffic without
// waiting out the full 90s give-up cycle.
func TestEarlyRebindSelfHeal(t *testing.T) {
pair := newGiveUpPair(t, true)
pkt := buildIPv4Packet(testIPA, testIPB, 8)
send := func() { pair.tunA.toDevice <- pkt }
// Traffic demand: stages the packet and sends the first (blackholed)
// initiation.
send()
pair.devA.peers.RLock()
peer := pair.devA.peers.keyMap[pair.pkB]
pair.devA.peers.RUnlock()
if peer == nil {
t.Fatal("peer not found")
}
// Simulate reaching the 3rd retry expiry (~15s in the field): two retries
// already counted, the last initiation older than the retransmit timeout.
peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-2 * RekeyTimeout)
peer.handshake.mutex.Unlock()
peer.timers.handshakeAttempts.Store(2)
expiredRetransmitHandshake(peer)
awaitPacket(t, pair.tunB, pkt, send)
}

143
device/lx_giveup_rebind.go Normal file
View file

@ -0,0 +1,143 @@
/* SPDX-License-Identifier: MIT
*
* lx: SPEC 041 passive self-heal for a dead per-flow path (an expired NAT
* mapping or a poisoned DPI flow entry that pins every retry to the same dead
* 5-tuple until a manual reconnect). One mechanism reopen the bind (fresh
* ephemeral port when allowed) and immediately re-initiate with three
* triggers sharing one debounce window:
*
* giveup the handshake retry cycle exhausted (~90s of unanswered
* initiations under traffic demand); safety net, covers every path;
* early >=3 unanswered initiations against a provably dead session
* (see sessionProvablyDead): no point waiting out the rest of the
* cycle, rebind at ~15s instead of ~90s;
* nudge the consumer reports "device woke up" via
* Device.RebindIfSessionStale (wired through sing-box libbox);
* heals without waiting for traffic demand at all.
*
* Zero cost while healthy: no timers, no goroutines triggers 1-2 live in
* the existing retry cycle, trigger 3 is paid by the caller. The state lives
* in Device.giveUpRebind (device.go); enabled defaults to true in NewDevice,
* sing-box decides freshPort from whether the user pinned listen_port.
*/
package device
import "time"
// earlyGiveUpMinAttempts is the number of unanswered initiations (retry timer
// expiries) after which a provably dead session is rebound early instead of
// waiting out the full RekeyAttemptTime cycle: ~15s at RekeyTimeout=5s.
const earlyGiveUpMinAttempts = 3
// SetGiveUpRebind configures the self-heal (see the giveUpRebind field
// comment in device.go). freshPort must be false when the user pinned an
// explicit listen_port: the pinned port is preserved, at the cost of the
// rebind not changing the 5-tuple.
func (device *Device) SetGiveUpRebind(enabled, freshPort bool) {
device.giveUpRebind.enabled.Store(enabled)
device.giveUpRebind.freshPort.Store(freshPort)
}
// handleHandshakeGiveUp is invoked from the give-up branch of
// expiredRetransmitHandshake: ~90s of initiations went unanswered, so the
// current socket's 5-tuple is proven dead.
func (device *Device) handleHandshakeGiveUp(peer *Peer) {
device.selfHealRebind("giveup", peer)
}
// maybeEarlyGiveUpRebind is invoked from the RETRY branch of
// expiredRetransmitHandshake. Once enough initiations went unanswered AND the
// session is provably dead there is nothing left to protect — rebind now, at
// ~15s instead of ~90s. The retry cycle itself continues untouched: this only
// moves the socket under it. A live session with transient packet loss fails
// sessionProvablyDead and keeps byte-for-byte upstream behaviour; the shared
// debounce means this also suppresses the giveup rebind of the same series.
func (device *Device) maybeEarlyGiveUpRebind(peer *Peer) {
if peer.timers.handshakeAttempts.Load() < earlyGiveUpMinAttempts {
return
}
if !device.sessionProvablyDead(peer) {
return
}
device.selfHealRebind("early", peer)
}
// sessionProvablyDead reports whether the peer's session is beyond saving: no
// live keypair, or the last successful handshake is older than
// RejectAfterTime (the keys are invalid after that, so a rebind loses
// nothing). The stale predicate shared by the early and nudge triggers.
func (device *Device) sessionProvablyDead(peer *Peer) bool {
if peer.keypairs.Current() == nil {
return true
}
return time.Since(time.Unix(0, peer.lastHandshakeNano.Load())) > RejectAfterTime
}
// RebindIfSessionStale is the wake-nudge entry (trigger 3): the consumer
// observed a device wake-up and asks for an immediate heal instead of waiting
// for traffic demand to walk the retry cycle. If any running peer's session
// is provably dead the bind is reopened once (shared debounce) and every such
// peer re-initiates immediately; a healthy device is a no-op. Returns whether
// a rebind was actually scheduled. Never blocks on the rebind itself — the
// heavy part runs in a goroutine (see selfHealRebind). On a down or closed
// device it is a no-op, so callers racing idle-suspend or Close are safe.
func (device *Device) RebindIfSessionStale() bool {
if !device.giveUpRebind.enabled.Load() || !device.isUp() {
return false
}
var stale []*Peer
device.peers.RLock()
for _, peer := range device.peers.keyMap {
if peer.isRunning.Load() && device.sessionProvablyDead(peer) {
stale = append(stale, peer)
}
}
device.peers.RUnlock()
if len(stale) == 0 {
return false
}
return device.selfHealRebind("nudge", stale...)
}
// selfHealRebind is the shared action behind all three triggers. Runs the
// heavy part in a goroutine so a timer callback (or a nudge caller) never
// blocks on BindUpdate's worker drain. Debounced to one rebind per
// RekeyAttemptTime per device across ALL triggers (CAS on `last` settles
// concurrent multi-peer races): an early rebind at ~15s suppresses the giveup
// rebind of the same failed series at ~90s. On a down or closed device
// BindUpdate does not reopen the socket, so a rebind racing idle-suspend
// (SPEC 020) or Close degrades to a no-op.
func (device *Device) selfHealRebind(trigger string, peers ...*Peer) bool {
if !device.giveUpRebind.enabled.Load() {
return false
}
if device.isClosed() {
return false
}
now := time.Now().Unix()
last := device.giveUpRebind.last.Load()
if now-last < int64(RekeyAttemptTime/time.Second) {
return false
}
if !device.giveUpRebind.last.CompareAndSwap(last, now) {
return false
}
fresh := device.giveUpRebind.freshPort.Load()
go func() {
if fresh {
device.net.Lock()
device.net.port = 0
device.net.Unlock()
}
if err := device.BindUpdate(); err != nil {
device.log.Errorf("%v - Failed self-heal rebind (trigger=%s): %v", peers[0], trigger, err)
return
}
device.log.Verbosef("%v - Rebound socket for self-heal (trigger=%s, fresh port=%v)", peers[0], trigger, fresh)
for _, peer := range peers {
peer.SendHandshakeInitiation(false)
}
}()
return true
}

View file

@ -0,0 +1,83 @@
/* SPDX-License-Identifier: MIT
*
* lx: SPEC 041 unit tests for the give-up rebind mechanics on top of the
* self-heal harness (lx_giveup_selfheal_test.go): fresh vs pinned port,
* debounce, and disabled = upstream parity. These use the post-fix API
* (SetGiveUpRebind) and are NOT expected to compile on the pre-fix base.
*/
package device
import (
"testing"
"time"
)
func waitOpens(t *testing.T, bind *gateBind, want int) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for bind.openCount() < want {
if time.Now().After(deadline) {
t.Fatalf("bind reopened %d times, want %d", bind.openCount(), want)
}
time.Sleep(10 * time.Millisecond)
}
}
// Fresh mode (listen_port not pinned): the rebind must ask the OS for a new
// ephemeral port — Open is called with port 0.
func TestGiveUpRebindFreshPort(t *testing.T) {
pair := newGiveUpPair(t, false)
pair.devA.SetGiveUpRebind(true, true)
triggerGiveUp(t, pair.devA, pair.pkB)
waitOpens(t, pair.bindA, 2)
ports := pair.bindA.portsSnapshot()
if ports[1] != 0 {
t.Fatalf("rebind requested port %d, want 0 (fresh ephemeral)", ports[1])
}
}
// Pinned mode (explicit listen_port): the rebind must keep the current port.
// chanBind.Open reports its source id (1) as the actual port, so the device
// stores net.port=1 after the first Open and must reuse it.
func TestGiveUpRebindPinnedPortPreserved(t *testing.T) {
pair := newGiveUpPair(t, false)
pair.devA.SetGiveUpRebind(true, false)
triggerGiveUp(t, pair.devA, pair.pkB)
waitOpens(t, pair.bindA, 2)
ports := pair.bindA.portsSnapshot()
if ports[1] != 1 {
t.Fatalf("rebind requested port %d, want 1 (pinned)", ports[1])
}
}
// A second give-up inside the debounce window must not rebind again.
func TestGiveUpRebindDebounce(t *testing.T) {
pair := newGiveUpPair(t, false)
triggerGiveUp(t, pair.devA, pair.pkB)
waitOpens(t, pair.bindA, 2)
triggerGiveUp(t, pair.devA, pair.pkB)
time.Sleep(300 * time.Millisecond)
if got := pair.bindA.openCount(); got != 2 {
t.Fatalf("debounce failed: bind opened %d times, want 2", got)
}
}
// Disabled: the give-up branch must behave exactly like upstream — flush and
// stop, no rebind.
func TestGiveUpRebindDisabled(t *testing.T) {
pair := newGiveUpPair(t, false)
pair.devA.SetGiveUpRebind(false, false)
triggerGiveUp(t, pair.devA, pair.pkB)
time.Sleep(300 * time.Millisecond)
if got := pair.bindA.openCount(); got != 1 {
t.Fatalf("disabled mechanism still rebound: %d opens, want 1", got)
}
}

View file

@ -0,0 +1,172 @@
/* SPDX-License-Identifier: MIT
*
* lx: SPEC 041 behavioural red/green test for the handshake give-up
* self-heal. Field failure mode (WARP/AWG after device sleep): the per-flow
* path state of the socket's 5-tuple dies (expired NAT mapping / poisoned DPI
* flow entry), every packet sent from the old socket vanishes, and upstream
* wireguard-go retries into that dead socket forever only a manual
* reconnect (new socket, new ephemeral port) heals the peer.
*
* The test models the dead 5-tuple with a bind whose FIRST socket generation
* silently swallows every send; any socket opened after a rebind delivers
* normally. It then drives the peer into the give-up branch of
* expiredRetransmitHandshake and expects traffic to flow end to end without
* any reconnect:
*
* - pre-fix (base): give-up only flushes staged packets, the bind is never
* reopened, every retry keeps dying in the first generation -> timeout;
* - post-fix: give-up rebinds the socket and re-initiates -> tunnel comes
* up and the packet arrives.
*
* This file deliberately uses NO post-fix API, so it compiles and runs RED on
* the pre-fix base commit. Reuses the chanBind/chanTun harness from
* transport_padding_test.go.
*/
package device
import (
"context"
"encoding/hex"
"fmt"
"sync"
"testing"
"time"
"github.com/sagernet/wireguard-go/conn"
)
// gateBind wraps chanBind: it records every Open (the port argument the
// device asked for) and silently swallows sends while the socket generation
// is at most dropOpens — modelling a dead 5-tuple whose packets vanish on the
// path without any local error.
type gateBind struct {
*chanBind
mu sync.Mutex
openPorts []uint16
opens int
dropOpens int // swallow sends while opens <= dropOpens
}
func (b *gateBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
fns, actual, err := b.chanBind.Open(port)
b.mu.Lock()
b.opens++
b.openPorts = append(b.openPorts, port)
b.mu.Unlock()
return fns, actual, err
}
func (b *gateBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error {
b.mu.Lock()
drop := b.opens <= b.dropOpens
b.mu.Unlock()
if drop {
return nil // the dead 5-tuple: no local error, the packet just vanishes
}
return b.chanBind.Send(bufs, ep, offset)
}
func (b *gateBind) openCount() int {
b.mu.Lock()
defer b.mu.Unlock()
return b.opens
}
func (b *gateBind) portsSnapshot() []uint16 {
b.mu.Lock()
defer b.mu.Unlock()
return append([]uint16(nil), b.openPorts...)
}
type giveUpPair struct {
devA, devB *Device
tunA, tunB *chanTun
bindA *gateBind
pkB NoisePublicKey
}
// newGiveUpPair builds two Up()'d peered devices; devA sits on a gateBind
// whose first socket generation optionally blackholes all sends.
func newGiveUpPair(t *testing.T, dropFirstOpen bool) *giveUpPair {
t.Helper()
skA, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey A: %v", err)
}
skB, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey B: %v", err)
}
pkA := skA.publicKey()
pkB := skB.publicKey()
rawA, rawB := newChanBindPair()
bindA := &gateBind{chanBind: rawA}
if dropFirstOpen {
bindA.dropOpens = 1
}
tunA := newChanTun()
tunB := newChanTun()
devA := NewDevice(context.Background(), tunA, bindA, NewLogger(LogLevelError, "devA: "), 1)
devB := NewDevice(context.Background(), tunB, rawB, NewLogger(LogLevelError, "devB: "), 1)
t.Cleanup(devA.Close)
t.Cleanup(devB.Close)
cfgA := fmt.Sprintf(
"private_key=%s\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:2\nallowed_ip=%s/32\n",
hex.EncodeToString(skA[:]), hex.EncodeToString(pkB[:]), testIPB)
cfgB := fmt.Sprintf(
"private_key=%s\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:1\nallowed_ip=%s/32\n",
hex.EncodeToString(skB[:]), hex.EncodeToString(pkA[:]), testIPA)
if err := devA.IpcSet(cfgA); err != nil {
t.Fatalf("IpcSet A: %v", err)
}
if err := devB.IpcSet(cfgB); err != nil {
t.Fatalf("IpcSet B: %v", err)
}
if err := devA.Up(); err != nil {
t.Fatalf("Up A: %v", err)
}
if err := devB.Up(); err != nil {
t.Fatalf("Up B: %v", err)
}
return &giveUpPair{devA: devA, devB: devB, tunA: tunA, tunB: tunB, bindA: bindA, pkB: pkB}
}
// triggerGiveUp drives dev's peer into the give-up branch of
// expiredRetransmitHandshake exactly the way 90s of unanswered retries would:
// attempts past the limit, last initiation older than RekeyTimeout.
func triggerGiveUp(t *testing.T, dev *Device, pk NoisePublicKey) {
t.Helper()
dev.peers.RLock()
peer := dev.peers.keyMap[pk]
dev.peers.RUnlock()
if peer == nil {
t.Fatal("peer not found")
}
peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-2 * RekeyTimeout)
peer.handshake.mutex.Unlock()
peer.timers.handshakeAttempts.Store(MaxTimerHandshakes + 1)
expiredRetransmitHandshake(peer)
}
// TestHandshakeGiveUpSelfHeal: dead first socket, give-up fires — the tunnel
// must come up and deliver traffic without any reconnect.
func TestHandshakeGiveUpSelfHeal(t *testing.T) {
pair := newGiveUpPair(t, true)
pkt := buildIPv4Packet(testIPA, testIPB, 8)
send := func() { pair.tunA.toDevice <- pkt }
// Traffic demand: stages the packet and sends the first (blackholed)
// initiation, exactly the state a real give-up cycle ends in.
send()
triggerGiveUp(t, pair.devA, pair.pkB)
awaitPacket(t, pair.tunB, pkt, send)
}

View file

@ -0,0 +1,63 @@
/* SPDX-License-Identifier: MIT
*
* Pins the AWG get path: IpcGet must report every obfuscation parameter it
* accepted, including i1..i5. The I-slots are emitted by an `i%d=` loop rather
* than literal per-key sendf calls, which makes them easy to miss when auditing
* introspection parity against amneziawg-go by grep alone.
*/
package device
import (
"context"
"encoding/hex"
"strings"
"testing"
)
func TestIpcGetReportsAWGParams(t *testing.T) {
sk, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey: %v", err)
}
bind, _ := newChanBindPair()
dev := NewDevice(context.Background(), newChanTun(), bind, NewLogger(LogLevelError, "dev: "), 1)
t.Cleanup(dev.Close)
set := strings.Join([]string{
"private_key=" + hex.EncodeToString(sk[:]),
"jc=4", "jmin=40", "jmax=70",
"s1=15", "s2=20", "s3=25", "s4=30",
"h1=1", "h2=2", "h3=3", "h4=100-200",
"i1=<b 0xf6a1>", "i3=<r 8>", "i5=<t>",
"",
}, "\n")
if err := dev.IpcSet(set); err != nil {
t.Fatalf("IpcSet: %v", err)
}
got, err := dev.IpcGet()
if err != nil {
t.Fatalf("IpcGet: %v", err)
}
t.Logf("IpcGet:\n%s", got)
for _, want := range []string{
"jc=4", "jmin=40", "jmax=70",
"s1=15", "s2=20", "s3=25", "s4=30",
"h1=1", "h2=2", "h3=3", "h4=100-200",
"i1=<b 0xf6a1>", "i3=<r 8>", "i5=<t>",
} {
if !strings.Contains(got, want) {
t.Errorf("IpcGet missing %q", want)
}
}
// Unset I-slots must stay absent, not surface as empty values.
for _, absent := range []string{"i2=", "i4="} {
if strings.Contains(got, absent) {
t.Errorf("IpcGet reported unset %q", absent)
}
}
}

View file

@ -0,0 +1,228 @@
/* SPDX-License-Identifier: MIT
*
* lx: SPEC 041 v2 unit tests for the stale predicate, the wake nudge
* (RebindIfSessionStale) and the shared debounce across triggers, on top of
* the v1 harness (lx_giveup_selfheal_test.go). These use the post-fix API and
* are NOT expected to compile on the pre-fix base.
*/
package device
import (
"sync"
"testing"
"time"
)
// establishTunnel completes a real handshake over a healthy pair so the peer
// holds a live keypair and a fresh lastHandshakeNano.
func establishTunnel(t *testing.T, pair *giveUpPair) {
t.Helper()
pkt := buildIPv4Packet(testIPA, testIPB, 8)
send := func() { pair.tunA.toDevice <- pkt }
send()
awaitPacket(t, pair.tunB, pkt, send)
}
func peerOf(t *testing.T, dev *Device, pk NoisePublicKey) *Peer {
t.Helper()
dev.peers.RLock()
peer := dev.peers.keyMap[pk]
dev.peers.RUnlock()
if peer == nil {
t.Fatal("peer not found")
}
return peer
}
// A cold peer (no keypair yet, nothing to lose): the nudge must rebind and
// immediately initiate — the tunnel comes up without any traffic demand.
func TestNudgeRebindsStaleSession(t *testing.T) {
pair := newGiveUpPair(t, true)
if !pair.devA.RebindIfSessionStale() {
t.Fatal("nudge on a cold (keypair-less) session must rebind")
}
waitOpens(t, pair.bindA, 2)
// The immediate initiation must bring the tunnel up: traffic sent only
// AFTER the nudge flows end to end.
pkt := buildIPv4Packet(testIPA, testIPB, 8)
send := func() { pair.tunA.toDevice <- pkt }
send()
awaitPacket(t, pair.tunB, pkt, send)
}
// A healthy session (live keypair, fresh handshake) must be a strict no-op.
func TestNudgeHealthySessionNoop(t *testing.T) {
pair := newGiveUpPair(t, false)
establishTunnel(t, pair)
opens := pair.bindA.openCount()
if pair.devA.RebindIfSessionStale() {
t.Fatal("nudge on a healthy session must not rebind")
}
time.Sleep(100 * time.Millisecond)
if got := pair.bindA.openCount(); got != opens {
t.Fatalf("healthy nudge reopened the bind: %d opens, want %d", got, opens)
}
}
// A live keypair whose last handshake is older than RejectAfterTime is
// provably dead (the keys are invalid): the nudge must rebind.
func TestNudgeExpiredHandshakeIsStale(t *testing.T) {
pair := newGiveUpPair(t, false)
establishTunnel(t, pair)
peer := peerOf(t, pair.devA, pair.pkB)
peer.lastHandshakeNano.Store(time.Now().Add(-RejectAfterTime - time.Second).UnixNano())
if !pair.devA.RebindIfSessionStale() {
t.Fatal("nudge on an expired session must rebind")
}
waitOpens(t, pair.bindA, 2)
}
// A down device (how SPEC 020 idle-suspend leaves it) must be a no-op — the
// nudge never wakes sleepers.
func TestNudgeDownDeviceNoop(t *testing.T) {
pair := newGiveUpPair(t, true)
if err := pair.devA.Down(); err != nil {
t.Fatalf("Down: %v", err)
}
if pair.devA.RebindIfSessionStale() {
t.Fatal("nudge on a down device must be a no-op")
}
}
// Pinned listen_port survives the nudge rebind.
func TestNudgePinnedPortPreserved(t *testing.T) {
pair := newGiveUpPair(t, true)
pair.devA.SetGiveUpRebind(true, false)
if !pair.devA.RebindIfSessionStale() {
t.Fatal("nudge must rebind a cold session")
}
waitOpens(t, pair.bindA, 2)
ports := pair.bindA.portsSnapshot()
if ports[1] != 1 {
t.Fatalf("nudge rebind requested port %d, want 1 (pinned)", ports[1])
}
}
// The debounce window is SHARED across triggers: an early/nudge rebind
// suppresses the give-up rebind of the same failed series, and a later series
// (window elapsed) heals again — sliding window, not a latch.
func TestSharedDebounceAcrossTriggers(t *testing.T) {
pair := newGiveUpPair(t, false)
// First trigger of the series: nudge.
if !pair.devA.RebindIfSessionStale() {
t.Fatal("first nudge must rebind")
}
waitOpens(t, pair.bindA, 2)
// The give-up of the same series lands inside the window: suppressed.
triggerGiveUp(t, pair.devA, pair.pkB)
time.Sleep(300 * time.Millisecond)
if got := pair.bindA.openCount(); got != 2 {
t.Fatalf("give-up inside the shared window rebound: %d opens, want 2", got)
}
// Next series: age the window as wall clocks would — heals again.
pair.devA.giveUpRebind.last.Store(time.Now().Add(-RekeyAttemptTime - time.Second).Unix())
triggerGiveUp(t, pair.devA, pair.pkB)
waitOpens(t, pair.bindA, 3)
}
// The early trigger must NOT fire before enough initiations went unanswered,
// even against a provably dead session.
func TestEarlyRebindNeedsMinAttempts(t *testing.T) {
pair := newGiveUpPair(t, true)
peer := peerOf(t, pair.devA, pair.pkB)
peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-2 * RekeyTimeout)
peer.handshake.mutex.Unlock()
peer.timers.handshakeAttempts.Store(0) // this expiry brings it to 1 (< min)
expiredRetransmitHandshake(peer)
time.Sleep(300 * time.Millisecond)
if got := pair.bindA.openCount(); got != 1 {
t.Fatalf("early rebind fired below the attempt floor: %d opens, want 1", got)
}
}
// A fresh session (live keypair, recent handshake) must keep the retry branch
// byte-for-byte upstream even past the attempt floor: no rebind.
func TestEarlyRebindFreshSessionNoop(t *testing.T) {
pair := newGiveUpPair(t, false)
establishTunnel(t, pair)
peer := peerOf(t, pair.devA, pair.pkB)
opens := pair.bindA.openCount()
peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-2 * RekeyTimeout)
peer.handshake.mutex.Unlock()
peer.timers.handshakeAttempts.Store(earlyGiveUpMinAttempts)
expiredRetransmitHandshake(peer)
time.Sleep(300 * time.Millisecond)
if got := pair.bindA.openCount(); got != opens {
t.Fatalf("early rebind fired on a fresh session: %d opens, want %d", got, opens)
}
}
// Nudge racing Close: no panic, no deadlock, no race-detector report.
func TestNudgeRacesClose(t *testing.T) {
for i := 0; i < 25; i++ {
pair := newGiveUpPair(t, false)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
pair.devA.RebindIfSessionStale()
}()
go func() {
defer wg.Done()
pair.devA.Close()
}()
wg.Wait()
}
}
// Nudge racing Down/Up (the SPEC 020 suspend/resume shape): the device must
// end consistent — up, with a live bind.
func TestNudgeRacesSuspend(t *testing.T) {
for i := 0; i < 25; i++ {
pair := newGiveUpPair(t, false)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
pair.devA.RebindIfSessionStale()
}()
go func() {
defer wg.Done()
if err := pair.devA.Down(); err != nil {
t.Errorf("Down: %v", err)
}
if err := pair.devA.Up(); err != nil {
t.Errorf("Up: %v", err)
}
}()
wg.Wait()
pair.devA.net.RLock()
bindAlive := pair.devA.net.bind != nil
pair.devA.net.RUnlock()
if !bindAlive || !pair.devA.isUp() {
t.Fatalf("iteration %d: device inconsistent after nudge/suspend race (bind=%v up=%v)",
i, bindAlive, pair.devA.isUp())
}
pair.devA.Close()
pair.devB.Close()
}
}

65
device/magic-header.go Normal file
View file

@ -0,0 +1,65 @@
package device
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
)
type magicHeader struct {
start uint32
end uint32
}
func newMagicHeader(spec string) (*magicHeader, error) {
parts := strings.Split(spec, "-")
if len(parts) < 1 || len(parts) > 2 {
return nil, errors.New("bad format")
}
start, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", parts[0], err)
}
var end uint64
if len(parts) > 1 {
end, err = strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", parts[1], err)
}
} else {
end = start
}
if end < start {
return nil, errors.New("wrong range specified")
}
return &magicHeader{
start: uint32(start),
end: uint32(end),
}, nil
}
func (h *magicHeader) GenSpec() string {
if h.start == h.end {
return fmt.Sprintf("%d", h.start)
}
return fmt.Sprintf("%d-%d", h.start, h.end)
}
func (h *magicHeader) Validate(val uint32) bool {
return h.start <= val && val <= h.end
}
func (h *magicHeader) Generate() uint32 {
// Widen before arithmetic: end-start+1 in uint32 wraps to 0 for the
// full 0..2^32-1 range, which would panic rand.Int (bound <= 0).
high := int64(h.end) - int64(h.start) + 1
r, _ := rand.Int(rand.Reader, big.NewInt(high))
return h.start + uint32(r.Int64())
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,21 +1,22 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"time" "time"
"github.com/sagernet/wireguard-go/conn"
"github.com/sagernet/wireguard-go/tai64n"
"golang.org/x/crypto/blake2s" "golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305" "golang.org/x/crypto/poly1305"
"golang.zx2c4.com/wireguard/tai64n"
) )
type handshakeState int type handshakeState int
@ -53,20 +54,22 @@ const (
) )
const ( const (
MessageInitiationType = 1 MessageUnknownType uint32 = 0
MessageResponseType = 2 MessageInitiationType uint32 = 1
MessageCookieReplyType = 3 MessageResponseType uint32 = 2
MessageTransportType = 4 MessageCookieReplyType uint32 = 3
MessageTransportType uint32 = 4
) )
const ( const (
MessageInitiationSize = 148 // size of handshake initiation message MessageInitiationSize = 148 // size of handshake initiation message
MessageResponseSize = 92 // size of response message MessageResponseSize = 92 // size of response message
MessageCookieReplySize = 64 // size of cookie reply message MessageCookieReplySize = 64 // size of cookie reply message
MessageTransportHeaderSize = 16 // size of data preceding content in transport message MessageTransportHeaderSize = 16 // size of data preceding content in transport message
MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport MessageEncapsulatingTransportSize = 0 // lx: zeroed so AmneziaWG obfuscation composes without sagernet headroom (AWG path doesn't use the Bind.Send prepend)
MessageKeepaliveSize = MessageTransportSize // size of keepalive MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport
MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message MessageKeepaliveSize = MessageTransportSize // size of keepalive
MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message
) )
const ( const (
@ -115,6 +118,98 @@ type MessageCookieReply struct {
Cookie [blake2s.Size128 + poly1305.TagSize]byte Cookie [blake2s.Size128 + poly1305.TagSize]byte
} }
var errMessageLengthMismatch = errors.New("message length mismatch")
func (msg *MessageInitiation) unmarshal(b []byte) error {
if len(b) != MessageInitiationSize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Sender = binary.LittleEndian.Uint32(b[4:])
copy(msg.Ephemeral[:], b[8:])
copy(msg.Static[:], b[8+len(msg.Ephemeral):])
copy(msg.Timestamp[:], b[8+len(msg.Ephemeral)+len(msg.Static):])
copy(msg.MAC1[:], b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp):])
copy(msg.MAC2[:], b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp)+len(msg.MAC1):])
return nil
}
func (msg *MessageInitiation) marshal(b []byte) error {
if len(b) != MessageInitiationSize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Sender)
copy(b[8:], msg.Ephemeral[:])
copy(b[8+len(msg.Ephemeral):], msg.Static[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static):], msg.Timestamp[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp):], msg.MAC1[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp)+len(msg.MAC1):], msg.MAC2[:])
return nil
}
func (msg *MessageResponse) unmarshal(b []byte) error {
if len(b) != MessageResponseSize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Sender = binary.LittleEndian.Uint32(b[4:])
msg.Receiver = binary.LittleEndian.Uint32(b[8:])
copy(msg.Ephemeral[:], b[12:])
copy(msg.Empty[:], b[12+len(msg.Ephemeral):])
copy(msg.MAC1[:], b[12+len(msg.Ephemeral)+len(msg.Empty):])
copy(msg.MAC2[:], b[12+len(msg.Ephemeral)+len(msg.Empty)+len(msg.MAC1):])
return nil
}
func (msg *MessageResponse) marshal(b []byte) error {
if len(b) != MessageResponseSize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Sender)
binary.LittleEndian.PutUint32(b[8:], msg.Receiver)
copy(b[12:], msg.Ephemeral[:])
copy(b[12+len(msg.Ephemeral):], msg.Empty[:])
copy(b[12+len(msg.Ephemeral)+len(msg.Empty):], msg.MAC1[:])
copy(b[12+len(msg.Ephemeral)+len(msg.Empty)+len(msg.MAC1):], msg.MAC2[:])
return nil
}
func (msg *MessageCookieReply) unmarshal(b []byte) error {
if len(b) != MessageCookieReplySize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Receiver = binary.LittleEndian.Uint32(b[4:])
copy(msg.Nonce[:], b[8:])
copy(msg.Cookie[:], b[8+len(msg.Nonce):])
return nil
}
func (msg *MessageCookieReply) marshal(b []byte) error {
if len(b) != MessageCookieReplySize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Receiver)
copy(b[8:], msg.Nonce[:])
copy(b[8+len(msg.Nonce):], msg.Cookie[:])
return nil
}
type Handshake struct { type Handshake struct {
state handshakeState state handshakeState
mutex sync.RWMutex mutex sync.RWMutex
@ -124,7 +219,7 @@ type Handshake struct {
localEphemeral NoisePrivateKey // ephemeral secret key localEphemeral NoisePrivateKey // ephemeral secret key
localIndex uint32 // used to clear hash-table localIndex uint32 // used to clear hash-table
remoteIndex uint32 // index for sending remoteIndex uint32 // index for sending
remoteStatic NoisePublicKey // long term key remoteStatic NoisePublicKey // long term key, never changes, can be accessed without mutex
remoteEphemeral NoisePublicKey // ephemeral public key remoteEphemeral NoisePublicKey // ephemeral public key
precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret
lastTimestamp tai64n.Timestamp lastTimestamp tai64n.Timestamp
@ -193,8 +288,10 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
handshake.mixHash(handshake.remoteStatic[:]) handshake.mixHash(handshake.remoteStatic[:])
msgType := device.headers.init.Generate()
msg := MessageInitiation{ msg := MessageInitiation{
Type: MessageInitiationType, Type: msgType,
Ephemeral: handshake.localEphemeral.publicKey(), Ephemeral: handshake.localEphemeral.publicKey(),
} }
@ -244,7 +341,7 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
return &msg, nil return &msg, nil
} }
func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation, endpoint conn.Endpoint) *Peer {
var ( var (
hash [blake2s.Size]byte hash [blake2s.Size]byte
chainKey [blake2s.Size]byte chainKey [blake2s.Size]byte
@ -254,17 +351,22 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
return nil return nil
} }
// Snapshot staticIdentity so we don't hold the RLock across LookupPeer,
// which may call NewPeer (reentrant RLock deadlocks against a pending
// SetPrivateKey writer; see lock-ordering.md).
device.staticIdentity.RLock() device.staticIdentity.RLock()
defer device.staticIdentity.RUnlock() publicKey := device.staticIdentity.publicKey
privateKey := device.staticIdentity.privateKey
device.staticIdentity.RUnlock()
mixHash(&hash, &InitialHash, device.staticIdentity.publicKey[:]) mixHash(&hash, &InitialHash, publicKey[:])
mixHash(&hash, &hash, msg.Ephemeral[:]) mixHash(&hash, &hash, msg.Ephemeral[:])
mixKey(&chainKey, &InitialChainKey, msg.Ephemeral[:]) mixKey(&chainKey, &InitialChainKey, msg.Ephemeral[:])
// decrypt static key // decrypt static key
var peerPK NoisePublicKey var peerPK NoisePublicKey
var key [chacha20poly1305.KeySize]byte var key [chacha20poly1305.KeySize]byte
ss, err := device.staticIdentity.privateKey.sharedSecret(msg.Ephemeral) ss, err := privateKey.sharedSecret(msg.Ephemeral)
if err != nil { if err != nil {
return nil return nil
} }
@ -278,6 +380,11 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
// lookup peer // lookup peer
initEP, ok := endpoint.(conn.InitiationAwareEndpoint)
if ok {
initEP.InitiationMessagePublicKey(peerPK)
}
peer := device.LookupPeer(peerPK) peer := device.LookupPeer(peerPK)
if peer == nil || !peer.isRunning.Load() { if peer == nil || !peer.isRunning.Load() {
return nil return nil
@ -367,7 +474,7 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
} }
var msg MessageResponse var msg MessageResponse
msg.Type = MessageResponseType msg.Type = device.headers.response.Generate()
msg.Sender = handshake.localIndex msg.Sender = handshake.localIndex
msg.Receiver = handshake.remoteIndex msg.Receiver = handshake.remoteIndex
@ -434,6 +541,14 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
chainKey [blake2s.Size]byte chainKey [blake2s.Size]byte
) )
// Snapshot the static private key before acquiring handshake.mutex so
// that handshake.mutex is never held while acquiring staticIdentity
// (which would invert the staticIdentity < handshake.mutex hierarchy;
// see lock-ordering.md).
device.staticIdentity.RLock()
privateKey := device.staticIdentity.privateKey
device.staticIdentity.RUnlock()
ok := func() bool { ok := func() bool {
// lock handshake state // lock handshake state
@ -444,11 +559,6 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
return false return false
} }
// lock private key for reading
device.staticIdentity.RLock()
defer device.staticIdentity.RUnlock()
// finish 3-way DH // finish 3-way DH
mixHash(&hash, &handshake.hash, msg.Ephemeral[:]) mixHash(&hash, &handshake.hash, msg.Ephemeral[:])
@ -461,7 +571,7 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
mixKey(&chainKey, &chainKey, ss[:]) mixKey(&chainKey, &chainKey, ss[:])
setZero(ss[:]) setZero(ss[:])
ss, err = device.staticIdentity.privateKey.sharedSecret(msg.Ephemeral) ss, err = privateKey.sharedSecret(msg.Ephemeral)
if err != nil { if err != nil {
return false return false
} }

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,179 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"bytes"
"encoding/binary"
"testing"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/tun/tuntest"
)
func TestCurveWrappers(t *testing.T) {
sk1, err := newPrivateKey()
assertNil(t, err)
sk2, err := newPrivateKey()
assertNil(t, err)
pk1 := sk1.publicKey()
pk2 := sk2.publicKey()
ss1, err1 := sk1.sharedSecret(pk2)
ss2, err2 := sk2.sharedSecret(pk1)
if ss1 != ss2 || err1 != nil || err2 != nil {
t.Fatal("Failed to compute shared secet")
}
}
func randDevice(t *testing.T) *Device {
sk, err := newPrivateKey()
if err != nil {
t.Fatal(err)
}
tun := tuntest.NewChannelTUN()
logger := NewLogger(LogLevelError, "")
device := NewDevice(tun.TUN(), conn.NewDefaultBind(), logger)
device.SetPrivateKey(sk)
return device
}
func assertNil(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}
func assertEqual(t *testing.T, a, b []byte) {
if !bytes.Equal(a, b) {
t.Fatal(a, "!=", b)
}
}
func TestNoiseHandshake(t *testing.T) {
dev1 := randDevice(t)
dev2 := randDevice(t)
defer dev1.Close()
defer dev2.Close()
peer1, err := dev2.NewPeer(dev1.staticIdentity.privateKey.publicKey())
if err != nil {
t.Fatal(err)
}
peer2, err := dev1.NewPeer(dev2.staticIdentity.privateKey.publicKey())
if err != nil {
t.Fatal(err)
}
peer1.Start()
peer2.Start()
assertEqual(
t,
peer1.handshake.precomputedStaticStatic[:],
peer2.handshake.precomputedStaticStatic[:],
)
/* simulate handshake */
// initiation message
t.Log("exchange initiation message")
msg1, err := dev1.CreateMessageInitiation(peer2)
assertNil(t, err)
packet := make([]byte, 0, 256)
writer := bytes.NewBuffer(packet)
err = binary.Write(writer, binary.LittleEndian, msg1)
assertNil(t, err)
peer := dev2.ConsumeMessageInitiation(msg1)
if peer == nil {
t.Fatal("handshake failed at initiation message")
}
assertEqual(
t,
peer1.handshake.chainKey[:],
peer2.handshake.chainKey[:],
)
assertEqual(
t,
peer1.handshake.hash[:],
peer2.handshake.hash[:],
)
// response message
t.Log("exchange response message")
msg2, err := dev2.CreateMessageResponse(peer1)
assertNil(t, err)
peer = dev1.ConsumeMessageResponse(msg2)
if peer == nil {
t.Fatal("handshake failed at response message")
}
assertEqual(
t,
peer1.handshake.chainKey[:],
peer2.handshake.chainKey[:],
)
assertEqual(
t,
peer1.handshake.hash[:],
peer2.handshake.hash[:],
)
// key pairs
t.Log("deriving keys")
err = peer1.BeginSymmetricSession()
if err != nil {
t.Fatal("failed to derive keypair for peer 1", err)
}
err = peer2.BeginSymmetricSession()
if err != nil {
t.Fatal("failed to derive keypair for peer 2", err)
}
key1 := peer1.keypairs.next.Load()
key2 := peer2.keypairs.current
// encrypting / decryption test
t.Log("test key pairs")
func() {
testMsg := []byte("wireguard test message 1")
var err error
var out []byte
var nonce [12]byte
out = key1.send.Seal(out, nonce[:], testMsg, nil)
out, err = key2.receive.Open(out[:0], nonce[:], out, nil)
assertNil(t, err)
assertEqual(t, out, testMsg)
}()
func() {
testMsg := []byte("wireguard test message 2")
var err error
var out []byte
var nonce [12]byte
out = key2.send.Seal(out, nonce[:], testMsg, nil)
out, err = key1.receive.Open(out[:0], nonce[:], out, nil)
assertNil(t, err)
assertEqual(t, out, testMsg)
}()
}

155
device/obf.go Normal file
View file

@ -0,0 +1,155 @@
package device
import (
"errors"
"fmt"
"strconv"
"strings"
)
type obfBuilder func(val string) (obf, error)
// parseObfLen parses and bounds an obfuscator length argument: a negative
// value would panic slice bounds in obfChain.Obfuscate, a huge one would
// OOM in the handshake-time make (SendHandshakeInitiation).
func parseObfLen(val string) (int, error) {
length, err := strconv.Atoi(val)
if err != nil {
return 0, err
}
if length < 0 || length > MaxMessageSize {
return 0, fmt.Errorf("obfuscator length %d out of range [0, %d]", length, MaxMessageSize)
}
return length, nil
}
var obfBuilders = map[string]obfBuilder{
"b": newBytesObf,
"t": newTimestampObf,
"r": newRandObf,
"rc": newRandCharObf,
"rd": newRandDigitsObf,
"d": newDataObf,
"ds": newDataStringObf,
"dz": newDataSizeObf,
}
type obf interface {
Obfuscate(dst, src []byte)
Deobfuscate(dst, src []byte) bool
ObfuscatedLen(srcLen int) int
DeobfuscatedLen(srcLen int) int
}
type obfChain struct {
Spec string
obfs []obf
}
func newObfChain(spec string) (*obfChain, error) {
var (
obfs []obf
errs []error
)
remaining := spec[:]
for {
start := strings.IndexByte(remaining, '<')
if start == -1 {
break
}
end := strings.IndexByte(remaining[start:], '>')
if end == -1 {
return nil, errors.New("missing enclosing >")
}
end += start
tag := remaining[start+1 : end]
parts := strings.Fields(tag)
if len(parts) == 0 {
errs = append(errs, errors.New("empty tag"))
remaining = remaining[end+1:]
continue
}
key := parts[0]
builder, ok := obfBuilders[key]
if !ok {
errs = append(errs, fmt.Errorf("unknown tag <%s>", key))
remaining = remaining[end+1:]
continue
}
val := ""
if len(parts) > 1 {
val = parts[1]
}
o, err := builder(val)
if err != nil {
errs = append(errs, fmt.Errorf("failed to build <%s>: %w", key, err))
remaining = remaining[end+1:]
continue
}
obfs = append(obfs, o)
remaining = remaining[end+1:]
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return &obfChain{
Spec: spec,
obfs: obfs,
}, nil
}
func (c *obfChain) Obfuscate(dst, src []byte) {
written := 0
for _, o := range c.obfs {
obfLen := o.ObfuscatedLen(len(src))
o.Obfuscate(dst[written:written+obfLen], src)
written += obfLen
}
}
func (c *obfChain) Deobfuscate(dst, src []byte) bool {
dynamicLen := len(src) - c.ObfuscatedLen(0)
written, read := 0, 0
for _, o := range c.obfs {
deobfLen := o.DeobfuscatedLen(dynamicLen)
obfLen := o.ObfuscatedLen(deobfLen)
if !o.Deobfuscate(dst[written:written+deobfLen], src[read:read+obfLen]) {
return false
}
written += deobfLen
read += obfLen
}
return true
}
func (c *obfChain) ObfuscatedLen(n int) int {
total := 0
for _, o := range c.obfs {
total += o.ObfuscatedLen(n)
}
return total
}
func (c *obfChain) DeobfuscatedLen(n int) int {
dynamicLen := n - c.ObfuscatedLen(0)
total := 0
for _, o := range c.obfs {
total += o.DeobfuscatedLen(dynamicLen)
}
return total
}

47
device/obf_bytes.go Normal file
View file

@ -0,0 +1,47 @@
package device
import (
"bytes"
"encoding/hex"
"errors"
"strings"
)
func newBytesObf(val string) (obf, error) {
val = strings.TrimPrefix(val, "0x")
if len(val) == 0 {
return nil, errors.New("empty argument")
}
if len(val)%2 != 0 {
return nil, errors.New("odd amount of symbols")
}
bytes, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
return &bytesObf{data: bytes}, nil
}
type bytesObf struct {
data []byte
}
func (o *bytesObf) Obfuscate(dst, src []byte) {
copy(dst, o.data)
}
func (o *bytesObf) Deobfuscate(dst, src []byte) bool {
return bytes.Equal(o.data, src[:o.ObfuscatedLen(0)])
}
func (o *bytesObf) ObfuscatedLen(srcLen int) int {
return len(o.data)
}
func (o *bytesObf) DeobfuscatedLen(srcLen int) int {
return 0
}

25
device/obf_data.go Normal file
View file

@ -0,0 +1,25 @@
package device
func newDataObf(val string) (obf, error) {
return &dataObf{}, nil
}
type dataObf struct {
}
func (obf *dataObf) Obfuscate(dst, src []byte) {
copy(dst, src)
}
func (obf *dataObf) Deobfuscate(dst, src []byte) bool {
copy(dst, src)
return true
}
func (o *dataObf) ObfuscatedLen(n int) int {
return n
}
func (o *dataObf) DeobfuscatedLen(n int) int {
return n
}

36
device/obf_datasize.go Normal file
View file

@ -0,0 +1,36 @@
package device
func newDataSizeObf(val string) (obf, error) {
length, err := parseObfLen(val)
if err != nil {
return nil, err
}
return &dataSizeObf{
length: length,
}, nil
}
type dataSizeObf struct {
length int
}
func (o *dataSizeObf) Obfuscate(dst, src []byte) {
srcLen := len(src)
for i := o.length - 1; i >= 0; i-- {
dst[i] = byte(srcLen & 0xFF)
srcLen >>= 8
}
}
func (o *dataSizeObf) Deobfuscate(dst, src []byte) bool {
return true
}
func (o *dataSizeObf) ObfuscatedLen(srcLen int) int {
return o.length
}
func (o *dataSizeObf) DeobfuscatedLen(srcLen int) int {
return 0
}

29
device/obf_datastring.go Normal file
View file

@ -0,0 +1,29 @@
package device
import (
"encoding/base64"
)
func newDataStringObf(val string) (obf, error) {
return &dataStringObf{}, nil
}
type dataStringObf struct {
}
func (o *dataStringObf) Obfuscate(dst, src []byte) {
base64.RawStdEncoding.Encode(dst, src)
}
func (o *dataStringObf) Deobfuscate(dst, src []byte) bool {
base64.RawStdEncoding.Decode(dst, src)
return true
}
func (o *dataStringObf) ObfuscatedLen(n int) int {
return base64.RawStdEncoding.EncodedLen(n)
}
func (o *dataStringObf) DeobfuscatedLen(n int) int {
return base64.RawStdEncoding.DecodedLen(n)
}

108
device/obf_guards_test.go Normal file
View file

@ -0,0 +1,108 @@
/* SPDX-License-Identifier: MIT
*
* Guards around AWG obfuscation config values: these tests pin the
* crash-on-config-value fixes (swapped jmin/jmax, out-of-range obfuscator
* lengths, full-range magic headers).
*/
package device
import (
"context"
"encoding/hex"
"fmt"
"testing"
)
func TestParseObfLen(t *testing.T) {
cases := []struct {
val string
want int
wantErr bool
}{
{"0", 0, false},
{"100", 100, false},
{fmt.Sprintf("%d", MaxMessageSize), MaxMessageSize, false},
{"-1", 0, true}, // would panic slice bounds in Obfuscate
{fmt.Sprintf("%d", MaxMessageSize+1), 0, true}, // would OOM the handshake make
{"2000000000", 0, true},
{"abc", 0, true},
}
for _, c := range cases {
got, err := parseObfLen(c.val)
if c.wantErr != (err != nil) {
t.Errorf("parseObfLen(%q): err = %v, wantErr = %v", c.val, err, c.wantErr)
}
if err == nil && got != c.want {
t.Errorf("parseObfLen(%q) = %d, want %d", c.val, got, c.want)
}
}
}
func TestMagicHeaderGenerateFullRange(t *testing.T) {
// end-start+1 computed in uint32 wraps to 0 for the full range and
// panics rand.Int; the fix widens to int64 before the arithmetic.
h := &magicHeader{start: 0, end: ^uint32(0)}
for i := 0; i < 8; i++ {
v := h.Generate()
if !h.Validate(v) {
t.Fatalf("generated value %d outside range", v)
}
}
}
// TestJunkSwappedBounds brings up a device pair whose junk config has
// jmin > jmax (passes per-field UAPI validation); without the swap guard
// the first handshake panics rand.Int with a non-positive bound.
func TestJunkSwappedBounds(t *testing.T) {
skA, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey A: %v", err)
}
skB, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey B: %v", err)
}
pkA := skA.publicKey()
pkB := skB.publicKey()
bindA, bindB := newChanBindPair()
tunA := newChanTun()
tunB := newChanTun()
devA := NewDevice(context.Background(), tunA, bindA, NewLogger(LogLevelError, "devA: "), 1)
devB := NewDevice(context.Background(), tunB, bindB, NewLogger(LogLevelError, "devB: "), 1)
t.Cleanup(devA.Close)
t.Cleanup(devB.Close)
// jmin deliberately greater than jmax: each field alone is valid.
junk := "jc=2\njmin=100\njmax=50\n"
cfgA := fmt.Sprintf(
"private_key=%s\n%sreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:2\nallowed_ip=%s/32\n",
hex.EncodeToString(skA[:]), junk, hex.EncodeToString(pkB[:]), testIPB)
cfgB := fmt.Sprintf(
"private_key=%s\n%sreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:1\nallowed_ip=%s/32\n",
hex.EncodeToString(skB[:]), junk, hex.EncodeToString(pkA[:]), testIPA)
if err := devA.IpcSet(cfgA); err != nil {
t.Fatalf("IpcSet A: %v", err)
}
if err := devB.IpcSet(cfgB); err != nil {
t.Fatalf("IpcSet B: %v", err)
}
if err := devA.Up(); err != nil {
t.Fatalf("Up A: %v", err)
}
if err := devB.Up(); err != nil {
t.Fatalf("Up B: %v", err)
}
// Drive a packet end-to-end: the handshake (junk packets included)
// must complete without panicking the process.
pkt := buildIPv4Packet(testIPA, testIPB, 28)
devA.InputPacket(testIPB.AsSlice(), [][]byte{pkt})
awaitPacket(t, tunB, pkt, func() {
devA.InputPacket(testIPB.AsSlice(), [][]byte{pkt})
})
}

38
device/obf_rand.go Normal file
View file

@ -0,0 +1,38 @@
package device
import (
"crypto/rand"
)
func newRandObf(val string) (obf, error) {
length, err := parseObfLen(val)
if err != nil {
return nil, err
}
return &randObf{
length: length,
}, nil
}
type randObf struct {
length int
}
func (o *randObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
}
func (o *randObf) Deobfuscate(dst, src []byte) bool {
// there is no way to validate randomness :)
// assume that it is always true
return true
}
func (o *randObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randObf) DeobfuscatedLen(n int) int {
return 0
}

47
device/obf_randchars.go Normal file
View file

@ -0,0 +1,47 @@
package device
import (
"crypto/rand"
"unicode"
)
const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func newRandCharObf(val string) (obf, error) {
length, err := parseObfLen(val)
if err != nil {
return nil, err
}
return &randCharObf{
length: length,
}, nil
}
type randCharObf struct {
length int
}
func (o *randCharObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
for i := range dst[:o.length] {
dst[i] = chars52[dst[i]%52]
}
}
func (o *randCharObf) Deobfuscate(dst, src []byte) bool {
for _, b := range src[:o.length] {
if !unicode.IsLetter(rune(b)) {
return false
}
}
return true
}
func (o *randCharObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randCharObf) DeobfuscatedLen(n int) int {
return 0
}

47
device/obf_randdigits.go Normal file
View file

@ -0,0 +1,47 @@
package device
import (
"crypto/rand"
"unicode"
)
const digits10 = "0123456789"
func newRandDigitsObf(val string) (obf, error) {
length, err := parseObfLen(val)
if err != nil {
return nil, err
}
return &randDigitObf{
length: length,
}, nil
}
type randDigitObf struct {
length int
}
func (o *randDigitObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
for i := range dst[:o.length] {
dst[i] = digits10[dst[i]%10]
}
}
func (o *randDigitObf) Deobfuscate(dst, src []byte) bool {
for _, b := range src[:o.length] {
if !unicode.IsDigit(rune(b)) {
return false
}
}
return true
}
func (o *randDigitObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randDigitObf) DeobfuscatedLen(n int) int {
return 0
}

31
device/obf_timestamp.go Normal file
View file

@ -0,0 +1,31 @@
package device
import (
"encoding/binary"
"time"
)
func newTimestampObf(_ string) (obf, error) {
return &timestampObf{}, nil
}
type timestampObf struct{}
func (o *timestampObf) Obfuscate(dst, src []byte) {
t := uint32(time.Now().Unix())
binary.BigEndian.PutUint32(dst, t)
}
func (o *timestampObf) Deobfuscate(dst, src []byte) bool {
// replay attack check?
// requires time to be always synchronized
return true
}
func (o *timestampObf) ObfuscatedLen(n int) int {
return 4
}
func (o *timestampObf) DeobfuscatedLen(n int) int {
return 0
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -8,11 +8,13 @@ package device
import ( import (
"container/list" "container/list"
"errors" "errors"
"net/netip"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"golang.zx2c4.com/wireguard/conn" "github.com/sagernet/wireguard-go/conn"
) )
type Peer struct { type Peer struct {
@ -25,6 +27,20 @@ type Peer struct {
rxBytes atomic.Uint64 // bytes received from peer rxBytes atomic.Uint64 // bytes received from peer
lastHandshakeNano atomic.Int64 // nano seconds since epoch lastHandshakeNano atomic.Int64 // nano seconds since epoch
sessionState struct {
sync.Mutex
current PeerSessionState
sessionExpires time.Time
}
queuedOutboundPackets atomic.Int32 // packets in staged+outbound queues, for input backpressure
// deleteOnIdle indicates whether the peer should be deleted when idle
// because it was auto-created via a Device.PeerLookupFunc.
//
// This field should only be set once, before the peer is started.
deleteOnIdle bool
endpoint struct { endpoint struct {
sync.Mutex sync.Mutex
val conn.Endpoint val conn.Endpoint
@ -36,6 +52,7 @@ type Peer struct {
retransmitHandshake *Timer retransmitHandshake *Timer
sendKeepalive *Timer sendKeepalive *Timer
newHandshake *Timer newHandshake *Timer
sessionExpired *Timer
zeroKeyMaterial *Timer zeroKeyMaterial *Timer
persistentKeepalive *Timer persistentKeepalive *Timer
handshakeAttempts atomic.Uint32 handshakeAttempts atomic.Uint32
@ -44,7 +61,14 @@ type Peer struct {
} }
state struct { state struct {
sync.Mutex // protects against concurrent Start/Stop sync.Mutex // protects against concurrent Start/Stop, and fields below
allowedIPs []netip.Prefix
// testAllowedIP, if non-nil, is used to test whether the peer is
// allowed to send a packet from the given IP address. It can be read
// without locking, but must be set with the state mutex locked.
testAllowedIP atomic.Pointer[func(netip.Addr) bool]
} }
queue struct { queue struct {
@ -87,7 +111,7 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
// map public key // map public key
_, ok := device.peers.keyMap[pk] _, ok := device.peers.keyMap[pk]
if ok { if ok {
return nil, errors.New("adding existing peer") return nil, errAddExistingPeer
} }
// pre-compute DH // pre-compute DH
@ -113,6 +137,30 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
return peer, nil return peer, nil
} }
// SetAllowedIPs sets the allowed IP prefixes for this peer.
//
// If the allowedIPs are unchanged since the last call, this method is a no-op.
// It's the caller's responsibility to ensure that no two peers have duplicate
// allowed IPs. If so, the last writer wins.
func (p *Peer) SetAllowedIPs(allowedIPs []netip.Prefix) {
p.state.Lock()
defer p.state.Unlock()
if slices.Equal(p.state.allowedIPs, allowedIPs) {
return
}
p.device.allowedips.setPeerPrefixes(p, allowedIPs)
allowedIPs = slices.Clone(allowedIPs) // avoid retaining caller's slice
p.state.allowedIPs = allowedIPs
f := mkIPInCIDRsTestFunc(allowedIPs)
p.state.testAllowedIP.Store(&f)
}
// SendBuffers sends buffers to peer. WireGuard packet data in each element of
// buffers must be preceded by MessageEncapsulatingTransportSize number of
// bytes.
func (peer *Peer) SendBuffers(buffers [][]byte) error { func (peer *Peer) SendBuffers(buffers [][]byte) error {
peer.device.net.RLock() peer.device.net.RLock()
defer peer.device.net.RUnlock() defer peer.device.net.RUnlock()
@ -133,7 +181,7 @@ func (peer *Peer) SendBuffers(buffers [][]byte) error {
} }
peer.endpoint.Unlock() peer.endpoint.Unlock()
err := peer.device.net.bind.Send(buffers, endpoint) err := peer.device.net.bind.Send(buffers, endpoint, MessageEncapsulatingTransportSize)
if err == nil { if err == nil {
var totalLen uint64 var totalLen uint64
for _, b := range buffers { for _, b := range buffers {
@ -190,6 +238,7 @@ func (peer *Peer) Start() {
// reset routine state // reset routine state
peer.stopping.Wait() peer.stopping.Wait()
peer.stopping.Add(2) peer.stopping.Add(2)
peer.queuedOutboundPackets.Store(0)
peer.handshake.mutex.Lock() peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-(RekeyTimeout + time.Second)) peer.handshake.lastSentHandshake = time.Now().Add(-(RekeyTimeout + time.Second))
@ -199,8 +248,8 @@ func (peer *Peer) Start() {
peer.timersStart() peer.timersStart()
device.flushInboundQueue(peer.queue.inbound) device.flushInboundQueue(peer.queue.inbound.c)
device.flushOutboundQueue(peer.queue.outbound) device.flushOutboundQueue(peer.queue.outbound.c)
// Use the device batch size, not the bind batch size, as the device size is // Use the device batch size, not the bind batch size, as the device size is
// the size of the batch pools. // the size of the batch pools.
@ -209,10 +258,21 @@ func (peer *Peer) Start() {
go peer.RoutineSequentialReceiver(batchSize) go peer.RoutineSequentialReceiver(batchSize)
peer.isRunning.Store(true) peer.isRunning.Store(true)
// A lazily-created peer that never completes a handshake otherwise never
// arms its reaping timer. Arm it here, while running under state.Lock, so
// it's reclaimed after RejectAfterTime*3 of no session and is guaranteed to
// be torn down by a matching Stop. A completed handshake re-Mods it.
if peer.deleteOnIdle {
peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3)
}
} }
func (peer *Peer) ZeroAndFlushAll() { func (peer *Peer) ZeroAndFlushAll() {
device := peer.device device := peer.device
if peer.timers.sessionExpired != nil {
peer.timers.sessionExpired.Del()
}
// clear key pairs // clear key pairs
@ -235,6 +295,11 @@ func (peer *Peer) ZeroAndFlushAll() {
handshake.mutex.Unlock() handshake.mutex.Unlock()
peer.FlushStagedPackets() peer.FlushStagedPackets()
peer.sessionState.Lock()
peer.sessionState.sessionExpires = time.Time{}
peer.noteSessionStateLocked(PeerSessionNone)
peer.sessionState.Unlock()
} }
func (peer *Peer) ExpireCurrentKeypairs() { func (peer *Peer) ExpireCurrentKeypairs() {
@ -254,6 +319,11 @@ func (peer *Peer) ExpireCurrentKeypairs() {
next.sendNonce.Store(RejectAfterMessages) next.sendNonce.Store(RejectAfterMessages)
} }
keypairs.Unlock() keypairs.Unlock()
peer.sessionState.Lock()
peer.sessionState.sessionExpires = time.Time{}
peer.noteSessionStateLocked(PeerSessionExpired)
peer.sessionState.Unlock()
} }
func (peer *Peer) Stop() { func (peer *Peer) Stop() {
@ -276,6 +346,51 @@ func (peer *Peer) Stop() {
peer.ZeroAndFlushAll() peer.ZeroAndFlushAll()
} }
func (peer *Peer) noteSessionState(state PeerSessionState) {
peer.sessionState.Lock()
defer peer.sessionState.Unlock()
peer.noteSessionStateLocked(state)
}
// noteSessionStateLocked records a session state transition and delivers the
// callback. The caller must hold peer.sessionState.Mutex during the
// state determination and transition.
func (peer *Peer) noteSessionStateLocked(state PeerSessionState) {
if peer.sessionState.current == state {
return
}
peer.sessionState.current = state
if f := peer.device.peerStateFn.Load(); f != nil {
(*f)(peer.handshake.remoteStatic, state)
}
}
func (peer *Peer) noteSessionHandshakeStarted() {
peer.sessionState.Lock()
defer peer.sessionState.Unlock()
if peer.sessionState.current == PeerSessionEstablished {
return
}
peer.noteSessionStateLocked(PeerSessionHandshake)
}
func (peer *Peer) noteSessionHandshakeStopped() {
peer.sessionState.Lock()
defer peer.sessionState.Unlock()
state := PeerSessionNone
if peer.hasKeyMaterial() {
state = PeerSessionExpired
}
peer.noteSessionStateLocked(state)
}
func (peer *Peer) hasKeyMaterial() bool {
keypairs := &peer.keypairs
keypairs.RLock()
defer keypairs.RUnlock()
return keypairs.previous != nil || keypairs.current != nil || keypairs.next.Load() != nil
}
func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) { func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) {
peer.endpoint.Lock() peer.endpoint.Lock()
defer peer.endpoint.Unlock() defer peer.endpoint.Unlock()

View file

@ -1,20 +1,21 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"sync" "sync"
"sync/atomic"
"github.com/sagernet/sing/common/buf"
) )
type WaitPool struct { type WaitPool struct {
pool sync.Pool pool sync.Pool
cond sync.Cond cond sync.Cond
lock sync.Mutex lock sync.Mutex
count atomic.Uint32 count uint32 // Get calls not yet Put back
max uint32 max uint32
} }
@ -24,13 +25,17 @@ func NewWaitPool(max uint32, new func() any) *WaitPool {
return p return p
} }
func (p *WaitPool) hasAccounting() bool {
return p != nil && p.max != 0
}
func (p *WaitPool) Get() any { func (p *WaitPool) Get() any {
if p.max != 0 { if p.max != 0 {
p.lock.Lock() p.lock.Lock()
for p.count.Load() >= p.max { for p.count >= p.max {
p.cond.Wait() p.cond.Wait()
} }
p.count.Add(1) p.count++
p.lock.Unlock() p.lock.Unlock()
} }
return p.pool.Get() return p.pool.Get()
@ -41,33 +46,34 @@ func (p *WaitPool) Put(x any) {
if p.max == 0 { if p.max == 0 {
return return
} }
p.count.Add(^uint32(0)) p.lock.Lock()
defer p.lock.Unlock()
p.count--
p.cond.Signal() p.cond.Signal()
} }
func (device *Device) PopulatePools() { func (device *Device) PopulatePools() {
device.pool.inboundElementsContainer = NewWaitPool(PreallocatedBuffersPerPool, func() any { device.pool.inboundElementsContainer = &sync.Pool{New: func() any {
s := make([]*QueueInboundElement, 0, device.BatchSize()) s := make([]*QueueInboundElement, 0, device.BatchSize())
return &QueueInboundElementsContainer{elems: s} return &QueueInboundElementsContainer{elems: s}
}) }}
device.pool.outboundElementsContainer = NewWaitPool(PreallocatedBuffersPerPool, func() any { device.pool.outboundElementsContainer = &sync.Pool{New: func() any {
s := make([]*QueueOutboundElement, 0, device.BatchSize()) s := make([]*QueueOutboundElement, 0, device.BatchSize())
return &QueueOutboundElementsContainer{elems: s} return &QueueOutboundElementsContainer{elems: s}
}) }}
device.pool.messageBuffers = NewWaitPool(PreallocatedBuffersPerPool, func() any { device.pool.messageBuffers = NewWaitPool(PreallocatedBuffersPerPool, func() any {
return new([MaxMessageSize]byte) return new([MaxMessageSize]byte)
}) })
device.pool.inboundElements = NewWaitPool(PreallocatedBuffersPerPool, func() any { device.pool.inboundElements = &sync.Pool{New: func() any {
return new(QueueInboundElement) return new(QueueInboundElement)
}) }}
device.pool.outboundElements = NewWaitPool(PreallocatedBuffersPerPool, func() any { device.pool.outboundElements = &sync.Pool{New: func() any {
return new(QueueOutboundElement) return new(QueueOutboundElement)
}) }}
} }
func (device *Device) GetInboundElementsContainer() *QueueInboundElementsContainer { func (device *Device) GetInboundElementsContainer() *QueueInboundElementsContainer {
c := device.pool.inboundElementsContainer.Get().(*QueueInboundElementsContainer) c := device.pool.inboundElementsContainer.Get().(*QueueInboundElementsContainer)
c.Mutex = sync.Mutex{}
return c return c
} }
@ -81,7 +87,6 @@ func (device *Device) PutInboundElementsContainer(c *QueueInboundElementsContain
func (device *Device) GetOutboundElementsContainer() *QueueOutboundElementsContainer { func (device *Device) GetOutboundElementsContainer() *QueueOutboundElementsContainer {
c := device.pool.outboundElementsContainer.Get().(*QueueOutboundElementsContainer) c := device.pool.outboundElementsContainer.Get().(*QueueOutboundElementsContainer)
c.Mutex = sync.Mutex{}
return c return c
} }
@ -101,6 +106,20 @@ func (device *Device) PutMessageBuffer(msg *[MaxMessageSize]byte) {
device.pool.messageBuffers.Put(msg) device.pool.messageBuffers.Put(msg)
} }
// Outbound buffers come from the sing allocator instead of the bounded
// messageBuffers pool: the injection paths (InputPacket/InputPackets) run on
// the caller's shared read loop, which must never block on pool exhaustion,
// and their packets are far smaller than MaxMessageSize, so they are allocated
// by actual size. This also keeps the bounded pool exclusively for the receive
// path, so outbound backlog can no longer starve it.
func (device *Device) GetOutboundBuffer(size int) []byte {
return buf.Get(size)
}
func (device *Device) PutOutboundBuffer(buffer []byte) {
_ = buf.Put(buffer)
}
func (device *Device) GetInboundElement() *QueueInboundElement { func (device *Device) GetInboundElement() *QueueInboundElement {
return device.pool.inboundElements.Get().(*QueueInboundElement) return device.pool.inboundElements.Get().(*QueueInboundElement)
} }

View file

@ -1,139 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
import (
"math/rand"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestWaitPool(t *testing.T) {
t.Skip("Currently disabled")
var wg sync.WaitGroup
var trials atomic.Int32
startTrials := int32(100000)
if raceEnabled {
// This test can be very slow with -race.
startTrials /= 10
}
trials.Store(startTrials)
workers := runtime.NumCPU() + 2
if workers-4 <= 0 {
t.Skip("Not enough cores")
}
p := NewWaitPool(uint32(workers-4), func() any { return make([]byte, 16) })
wg.Add(workers)
var max atomic.Uint32
updateMax := func() {
count := p.count.Load()
if count > p.max {
t.Errorf("count (%d) > max (%d)", count, p.max)
}
for {
old := max.Load()
if count <= old {
break
}
if max.CompareAndSwap(old, count) {
break
}
}
}
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for trials.Add(-1) > 0 {
updateMax()
x := p.Get()
updateMax()
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
updateMax()
p.Put(x)
updateMax()
}
}()
}
wg.Wait()
if max.Load() != p.max {
t.Errorf("Actual maximum count (%d) != ideal maximum count (%d)", max, p.max)
}
}
func BenchmarkWaitPool(b *testing.B) {
var wg sync.WaitGroup
var trials atomic.Int32
trials.Store(int32(b.N))
workers := runtime.NumCPU() + 2
if workers-4 <= 0 {
b.Skip("Not enough cores")
}
p := NewWaitPool(uint32(workers-4), func() any { return make([]byte, 16) })
wg.Add(workers)
b.ResetTimer()
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for trials.Add(-1) > 0 {
x := p.Get()
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
p.Put(x)
}
}()
}
wg.Wait()
}
func BenchmarkWaitPoolEmpty(b *testing.B) {
var wg sync.WaitGroup
var trials atomic.Int32
trials.Store(int32(b.N))
workers := runtime.NumCPU() + 2
if workers-4 <= 0 {
b.Skip("Not enough cores")
}
p := NewWaitPool(0, func() any { return make([]byte, 16) })
wg.Add(workers)
b.ResetTimer()
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for trials.Add(-1) > 0 {
x := p.Get()
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
p.Put(x)
}
}()
}
wg.Wait()
}
func BenchmarkSyncPool(b *testing.B) {
var wg sync.WaitGroup
var trials atomic.Int32
trials.Store(int32(b.N))
workers := runtime.NumCPU() + 2
if workers-4 <= 0 {
b.Skip("Not enough cores")
}
p := sync.Pool{New: func() any { return make([]byte, 16) }}
wg.Add(workers)
b.ResetTimer()
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for trials.Add(-1) > 0 {
x := p.Get()
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
p.Put(x)
}
}()
}
wg.Wait()
}

View file

@ -1,11 +1,11 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import "golang.zx2c4.com/wireguard/conn" import "github.com/sagernet/wireguard-go/conn"
/* Reduce memory consumption for Android */ /* Reduce memory consumption for Android */

View file

@ -2,12 +2,12 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import "golang.zx2c4.com/wireguard/conn" import "github.com/sagernet/wireguard-go/conn"
const ( const (
QueueStagedSize = conn.IdealBatchSize QueueStagedSize = conn.IdealBatchSize

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device

View file

@ -1,10 +0,0 @@
//go:build !race
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
const raceEnabled = false

View file

@ -1,10 +0,0 @@
//go:build race
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package device
const raceEnabled = true

View file

@ -1,22 +1,23 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"net" "net"
"net/netip"
"sync" "sync"
"time" "time"
"github.com/sagernet/wireguard-go/conn"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn"
) )
type QueueHandshakeElement struct { type QueueHandshakeElement struct {
@ -35,8 +36,13 @@ type QueueInboundElement struct {
} }
type QueueInboundElementsContainer struct { type QueueInboundElementsContainer struct {
sync.Mutex // filling is a one-shot barrier signaling decryption→receive
elems []*QueueInboundElement // handoff. RoutineReceiveIncoming calls Add(1) before sending the
// container down the decryption and inbound queues; RoutineDecryption
// calls Done after decrypting; RoutineSequentialReceiver calls Wait
// before reading the decrypted packets.
filling sync.WaitGroup
elems []*QueueInboundElement
} }
// clearPointers clears elem fields that contain pointers. // clearPointers clears elem fields that contain pointers.
@ -70,7 +76,10 @@ func (peer *Peer) keepKeyFreshReceiving() {
* Every time the bind is updated a new routine is started for * Every time the bind is updated a new routine is started for
* IPv4 and IPv6 (separately) * IPv4 and IPv6 (separately)
*/ */
func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.ReceiveFunc) { func (device *Device) RoutineReceiveIncoming(
maxBatchSize int,
recv conn.ReceiveFunc,
) {
recvName := recv.PrettyName() recvName := recv.PrettyName()
defer func() { defer func() {
device.log.Verbosef("Routine: receive incoming %s - stopped", recvName) device.log.Verbosef("Routine: receive incoming %s - stopped", recvName)
@ -133,9 +142,14 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
} }
// check size of packet // check size of packet
packet := bufsArrs[i][:size] packet := bufsArrs[i][:size]
msgType := binary.LittleEndian.Uint32(packet[:4])
// get message padding and type based on information from S1-S4 and H1-H4
msgType, padding := device.DeterminePacketTypeAndPadding(packet, MessageUnknownType)
if padding > 0 {
copy(packet, packet[padding:])
packet = packet[:len(packet)-padding]
}
switch msgType { switch msgType {
@ -178,7 +192,6 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
elemsForPeer, ok := elemsByPeer[peer] elemsForPeer, ok := elemsByPeer[peer]
if !ok { if !ok {
elemsForPeer = device.GetInboundElementsContainer() elemsForPeer = device.GetInboundElementsContainer()
elemsForPeer.Lock()
elemsByPeer[peer] = elemsForPeer elemsByPeer[peer] = elemsForPeer
} }
elemsForPeer.elems = append(elemsForPeer.elems, elem) elemsForPeer.elems = append(elemsForPeer.elems, elem)
@ -222,6 +235,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
} }
for peer, elemsContainer := range elemsByPeer { for peer, elemsContainer := range elemsByPeer {
if peer.isRunning.Load() { if peer.isRunning.Load() {
elemsContainer.filling.Add(1)
peer.queue.inbound.c <- elemsContainer peer.queue.inbound.c <- elemsContainer
device.queue.decryption.c <- elemsContainer device.queue.decryption.c <- elemsContainer
} else { } else {
@ -263,7 +277,7 @@ func (device *Device) RoutineDecryption(id int) {
elem.packet = nil elem.packet = nil
} }
} }
elemsContainer.Unlock() elemsContainer.filling.Done()
} }
} }
@ -277,7 +291,6 @@ func (device *Device) RoutineHandshake(id int) {
device.log.Verbosef("Routine: handshake worker %d - started", id) device.log.Verbosef("Routine: handshake worker %d - started", id)
for elem := range device.queue.handshake.c { for elem := range device.queue.handshake.c {
// handle cookie fields and ratelimiting // handle cookie fields and ratelimiting
switch elem.msgType { switch elem.msgType {
@ -287,8 +300,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal packet // unmarshal packet
var reply MessageCookieReply var reply MessageCookieReply
reader := bytes.NewReader(elem.packet) err := reply.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &reply)
if err != nil { if err != nil {
device.log.Verbosef("Failed to decode cookie reply") device.log.Verbosef("Failed to decode cookie reply")
goto skip goto skip
@ -305,9 +317,14 @@ func (device *Device) RoutineHandshake(id int) {
// consume reply // consume reply
if peer := entry.peer; peer.isRunning.Load() { if peer := entry.peer; peer.isRunning.Load() {
device.log.Verbosef("Receiving cookie response from %s", elem.endpoint.DstToString()) device.log.Verbosef(
"Receiving cookie response from %s",
elem.endpoint.DstToString(),
)
if !peer.cookieGenerator.ConsumeReply(&reply) { if !peer.cookieGenerator.ConsumeReply(&reply) {
device.log.Verbosef("Could not decrypt invalid cookie response") device.log.Verbosef(
"Could not decrypt invalid cookie response",
)
} }
} }
@ -349,20 +366,18 @@ func (device *Device) RoutineHandshake(id int) {
switch elem.msgType { switch elem.msgType {
case MessageInitiationType: case MessageInitiationType:
// unmarshal // unmarshal
var msg MessageInitiation var msg MessageInitiation
reader := bytes.NewReader(elem.packet) err := msg.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
if err != nil { if err != nil {
device.log.Errorf("Failed to decode initiation message") device.log.Errorf("Failed to decode initiation message")
goto skip goto skip
} }
// consume initiation // have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
peer := device.ConsumeMessageInitiation(&msg) peer := device.ConsumeMessageInitiation(&msg, elem.endpoint)
if peer == nil { if peer == nil {
device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString()) device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString())
goto skip goto skip
@ -386,13 +401,15 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal // unmarshal
var msg MessageResponse var msg MessageResponse
reader := bytes.NewReader(elem.packet) err := msg.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
if err != nil { if err != nil {
device.log.Errorf("Failed to decode response message") device.log.Errorf("Failed to decode response message")
goto skip goto skip
} }
// have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
// consume response // consume response
peer := device.ConsumeMessageResponse(&msg) peer := device.ConsumeMessageResponse(&msg)
@ -415,7 +432,6 @@ func (device *Device) RoutineHandshake(id int) {
// derive keypair // derive keypair
err = peer.BeginSymmetricSession() err = peer.BeginSymmetricSession()
if err != nil { if err != nil {
device.log.Errorf("%v - Failed to derive keypair: %v", peer, err) device.log.Errorf("%v - Failed to derive keypair: %v", peer, err)
goto skip goto skip
@ -423,6 +439,7 @@ func (device *Device) RoutineHandshake(id int) {
peer.timersSessionDerived() peer.timersSessionDerived()
peer.timersHandshakeComplete() peer.timersHandshakeComplete()
peer.SendPriorityMessage()
peer.SendKeepalive() peer.SendKeepalive()
} }
skip: skip:
@ -444,97 +461,183 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
if elemsContainer == nil { if elemsContainer == nil {
return return
} }
elemsContainer.Lock() peer.processInboundContainer(elemsContainer, bufs[:0])
validTailPacket := -1
dataPacketReceived := false
rxBytesLen := uint64(0)
for i, elem := range elemsContainer.elems {
if elem.packet == nil {
// decryption failed
continue
}
if !elem.keypair.replayFilter.ValidateCounter(elem.counter, RejectAfterMessages) {
continue
}
validTailPacket = i
if peer.ReceivedWithKeypair(elem.keypair) {
peer.SetEndpointFromPacket(elem.endpoint)
peer.timersHandshakeComplete()
peer.SendStagedPackets()
}
rxBytesLen += uint64(len(elem.packet) + MinMessageSize)
if len(elem.packet) == 0 {
device.log.Verbosef("%v - Receiving keepalive packet", peer)
continue
}
dataPacketReceived = true
switch elem.packet[0] >> 4 {
case 4:
if len(elem.packet) < ipv4.HeaderLen {
continue
}
field := elem.packet[IPv4offsetTotalLength : IPv4offsetTotalLength+2]
length := binary.BigEndian.Uint16(field)
if int(length) > len(elem.packet) || int(length) < ipv4.HeaderLen {
continue
}
elem.packet = elem.packet[:length]
src := elem.packet[IPv4offsetSrc : IPv4offsetSrc+net.IPv4len]
if device.allowedips.Lookup(src) != peer {
device.log.Verbosef("IPv4 packet with disallowed source address from %v", peer)
continue
}
case 6:
if len(elem.packet) < ipv6.HeaderLen {
continue
}
field := elem.packet[IPv6offsetPayloadLength : IPv6offsetPayloadLength+2]
length := binary.BigEndian.Uint16(field)
length += ipv6.HeaderLen
if int(length) > len(elem.packet) {
continue
}
elem.packet = elem.packet[:length]
src := elem.packet[IPv6offsetSrc : IPv6offsetSrc+net.IPv6len]
if device.allowedips.Lookup(src) != peer {
device.log.Verbosef("IPv6 packet with disallowed source address from %v", peer)
continue
}
default:
device.log.Verbosef("Packet with invalid IP version from %v", peer)
continue
}
bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)])
}
peer.rxBytes.Add(rxBytesLen)
if validTailPacket >= 0 {
peer.SetEndpointFromPacket(elemsContainer.elems[validTailPacket].endpoint)
peer.keepKeyFreshReceiving()
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketReceived()
}
if dataPacketReceived {
peer.timersDataReceived()
}
if len(bufs) > 0 {
_, err := device.tun.device.Write(bufs, MessageTransportOffsetContent)
if err != nil && !device.isClosed() {
device.log.Errorf("Failed to write packets to TUN device: %v", err)
}
}
for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer)
device.PutInboundElement(elem)
}
bufs = bufs[:0]
device.PutInboundElementsContainer(elemsContainer)
} }
} }
// processInboundContainer waits for the decryption routine to finish
// filling elemsContainer, then writes the valid packets to the TUN
// device and returns the container to the pool.
//
// scratch is a length-0 slice used to assemble the per-packet buffers
// passed to tun.device.Write; its backing array is reused across calls.
func (peer *Peer) processInboundContainer(elemsContainer *QueueInboundElementsContainer, scratch [][]byte) {
// Invariants from RoutineSequentialReceiver; all should be unreachable.
if len(scratch) != 0 || cap(scratch) == 0 {
panic(fmt.Sprintf("processInboundContainer: scratch must be empty with non-zero cap; got len=%d cap=%d",
len(scratch), cap(scratch)))
}
if cap(scratch) < len(elemsContainer.elems) {
panic(fmt.Sprintf("processInboundContainer: scratch cap %d < elems %d",
cap(scratch), len(elemsContainer.elems)))
}
device := peer.device
defer device.PutInboundElementsContainer(elemsContainer)
// Wait for RoutineDecryption to finish filling the container. After
// Wait returns we have happens-before with that goroutine and are the
// sole owner of the container until Put hands it back to the pool.
elemsContainer.filling.Wait()
elems := elemsContainer.elems
validTailPacket := -1
dataPacketReceived := false
rxBytesLen := uint64(0)
for i, elem := range elems {
if elem.packet == nil {
// decryption failed
continue
}
if !elem.keypair.replayFilter.ValidateCounter(elem.counter, RejectAfterMessages) {
continue
}
validTailPacket = i
if peer.ReceivedWithKeypair(elem.keypair) {
peer.SetEndpointFromPacket(elem.endpoint)
peer.timersHandshakeComplete()
peer.SendPriorityMessage()
peer.SendStagedPackets()
}
if ep, ok := elem.endpoint.(conn.PeerAwareEndpoint); ok {
ep.FromPeer(peer.handshake.remoteStatic)
}
rxBytesLen += uint64(len(elem.packet) + MinMessageSize)
if len(elem.packet) == 0 {
device.log.Verbosef("%v - Receiving keepalive packet", peer)
continue
}
dataPacketReceived = true
switch elem.packet[0] >> 4 {
case 4:
if len(elem.packet) < ipv4.HeaderLen {
continue
}
field := elem.packet[IPv4offsetTotalLength : IPv4offsetTotalLength+2]
length := binary.BigEndian.Uint16(field)
if int(length) > len(elem.packet) || int(length) < ipv4.HeaderLen {
continue
}
elem.packet = elem.packet[:length]
src := elem.packet[IPv4offsetSrc : IPv4offsetSrc+net.IPv4len]
srcAddr, _ := netip.AddrFromSlice(src)
if !peer.AllowedPeerSourceIP(srcAddr) {
device.log.Verbosef("IPv4 packet with disallowed source address from %v", peer)
continue
}
case 6:
if len(elem.packet) < ipv6.HeaderLen {
continue
}
field := elem.packet[IPv6offsetPayloadLength : IPv6offsetPayloadLength+2]
length := binary.BigEndian.Uint16(field)
length += ipv6.HeaderLen
if int(length) > len(elem.packet) {
continue
}
elem.packet = elem.packet[:length]
src := elem.packet[IPv6offsetSrc : IPv6offsetSrc+net.IPv6len]
srcAddr, _ := netip.AddrFromSlice(src)
if !peer.AllowedPeerSourceIP(srcAddr) {
device.log.Verbosef("IPv6 packet with disallowed source address from %v", peer)
continue
}
default:
device.log.Verbosef("Packet with invalid IP version from %v", peer)
continue
}
scratch = append(scratch, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)])
}
peer.rxBytes.Add(rxBytesLen)
if validTailPacket >= 0 {
peer.SetEndpointFromPacket(elems[validTailPacket].endpoint)
peer.keepKeyFreshReceiving()
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketReceived()
}
if dataPacketReceived {
peer.timersDataReceived()
}
if len(scratch) > 0 {
_, err := device.tun.device.Write(scratch, MessageTransportOffsetContent)
if err != nil && !device.isClosed() {
device.log.Errorf("Failed to write packets to TUN device: %v", err)
}
}
for _, elem := range elems {
device.PutMessageBuffer(elem.buffer)
device.PutInboundElement(elem)
}
}
func (device *Device) DeterminePacketTypeAndPadding(packet []byte, expectedType uint32) (uint32, int) {
size := len(packet)
if expectedType == MessageUnknownType || expectedType == MessageInitiationType {
padding := device.paddings.init
header := device.headers.init
if size == padding+MessageInitiationSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageInitiationType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageResponseType {
padding := device.paddings.response
header := device.headers.response
if size == padding+MessageResponseSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageResponseType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageCookieReplyType {
padding := device.paddings.cookie
header := device.headers.cookie
if size == padding+MessageCookieReplySize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageCookieReplyType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageTransportType {
padding := device.paddings.transport
header := device.headers.transport
if size >= padding+MessageTransportHeaderSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageTransportType, padding
}
}
}
return MessageUnknownType, 0
}

View file

@ -1,24 +1,28 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"math/big"
"net" "net"
"net/netip"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/sagernet/wireguard-go/conn"
"github.com/sagernet/wireguard-go/tun"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/tun"
) )
/* Outbound flow /* Outbound flow
@ -46,21 +50,30 @@ import (
*/ */
type QueueOutboundElement struct { type QueueOutboundElement struct {
buffer *[MaxMessageSize]byte // slice holding the packet data buffer []byte // sing-allocated buffer holding the packet data
packet []byte // slice of "buffer" (always!) // packet is always a slice of "buffer". The starting offset in buffer
nonce uint64 // nonce for encryption // is either:
keypair *Keypair // keypair for encryption // a) MessageEncapsulatingTransportSize+MessageTransportHeaderSize (plaintext)
peer *Peer // related peer // b) 0 (post-encryption)
packet []byte
nonce uint64 // nonce for encryption
keypair *Keypair // keypair for encryption
peer *Peer // related peer
} }
type QueueOutboundElementsContainer struct { type QueueOutboundElementsContainer struct {
sync.Mutex // filling is a one-shot barrier signaling encryption→send handoff.
elems []*QueueOutboundElement // SendStagedPackets calls Add(1) before sending the container down
// the encryption and outbound queues; RoutineEncryption calls Done
// after encrypting; RoutineSequentialSender calls Wait before
// reading the encrypted packets.
filling sync.WaitGroup
elems []*QueueOutboundElement
} }
func (device *Device) NewOutboundElement() *QueueOutboundElement { func (device *Device) NewOutboundElement() *QueueOutboundElement {
elem := device.GetOutboundElement() elem := device.GetOutboundElement()
elem.buffer = device.GetMessageBuffer() elem.buffer = device.GetOutboundBuffer(MaxMessageSize)
elem.nonce = 0 elem.nonce = 0
// keypair and peer were cleared (if necessary) by clearPointers. // keypair and peer were cleared (if necessary) by clearPointers.
return elem return elem
@ -86,9 +99,10 @@ func (peer *Peer) SendKeepalive() {
elemsContainer.elems = append(elemsContainer.elems, elem) elemsContainer.elems = append(elemsContainer.elems, elem)
select { select {
case peer.queue.staged <- elemsContainer: case peer.queue.staged <- elemsContainer:
peer.queuedOutboundPackets.Add(1)
peer.device.log.Verbosef("%v - Sending keepalive packet", peer) peer.device.log.Verbosef("%v - Sending keepalive packet", peer)
default: default:
peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundBuffer(elem.buffer)
peer.device.PutOutboundElement(elem) peer.device.PutOutboundElement(elem)
peer.device.PutOutboundElementsContainer(elemsContainer) peer.device.PutOutboundElementsContainer(elemsContainer)
} }
@ -96,6 +110,70 @@ func (peer *Peer) SendKeepalive() {
peer.SendStagedPackets() peer.SendStagedPackets()
} }
// SendPriorityMessage invokes the [PeerPriorityMessageFunc] callback if one is
// set, and queues the returned message for encryption and transmission if the
// current keypair is valid.
func (peer *Peer) SendPriorityMessage() {
f := peer.device.priorityMsgFn.Load()
if f == nil {
return
}
keypair := peer.keypairs.Current()
if keypair == nil || keypair.sendNonce.Load() >= RejectAfterMessages || time.Since(keypair.created) >= RejectAfterTime {
// SendStagedPackets initializes a handshake when the keypair is invalid,
// but we explicitly avoid that here. A priority message is only intended
// to flow around symmetric session establishment, but it should never
// trigger a new session. Reaching this branch due to nonce exhaustion
// or keypair expiration is highly unlikely considering where
// SendPriorityMessage is called (at current keypair establishment).
return
}
// get plaintext message to send
msg := (*f)(peer.handshake.remoteStatic)
if len(msg) == 0 {
return
}
if len(msg) > MaxPriorityMessageContentSize {
peer.device.log.Verbosef("%v - Failed to queue priority message due to size", peer)
return
}
// get pooled elements
elem := peer.device.NewOutboundElement()
elemsContainer := peer.device.GetOutboundElementsContainer()
elemsContainer.elems = append(elemsContainer.elems, elem)
packetQueued := false
defer func() {
if !packetQueued {
peer.device.PutOutboundBuffer(elem.buffer)
peer.device.PutOutboundElement(elem)
peer.device.PutOutboundElementsContainer(elemsContainer)
}
}()
// initialize outbound element
const offset = MessageEncapsulatingTransportSize + MessageTransportHeaderSize
n := copy(elem.buffer[offset:], msg)
elem.packet = elem.buffer[offset : offset+n]
elem.peer = peer
elem.nonce = keypair.sendNonce.Add(1) - 1
if elem.nonce >= RejectAfterMessages {
keypair.sendNonce.Store(RejectAfterMessages)
return
}
elem.keypair = keypair
// add to parallel and sequential queue
if peer.isRunning.Load() {
elemsContainer.filling.Add(1)
peer.queuedOutboundPackets.Add(1)
peer.queue.outbound.c <- elemsContainer
peer.device.queue.encryption.c <- elemsContainer
packetQueued = true
}
}
func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
if !isRetry { if !isRetry {
peer.timers.handshakeAttempts.Store(0) peer.timers.handshakeAttempts.Store(0)
@ -124,6 +202,34 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err return err
} }
var sendBuffer [][]byte
for _, ipacket := range peer.device.ipackets {
if ipacket != nil {
buf := make([]byte, ipacket.ObfuscatedLen(0))
ipacket.Obfuscate(buf, nil)
sendBuffer = append(sendBuffer, buf)
}
}
jc := peer.device.junk.count
jmin := peer.device.junk.min
jmax := peer.device.junk.max
if jmax < jmin {
// UAPI validates jmin/jmax only individually; a swapped pair
// would panic rand.Int below with a non-positive bound.
jmin, jmax = jmax, jmin
}
for i := 0; i < jc; i++ {
nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(jmax-jmin+1)))
n := int(nBig.Int64()) + jmin
buf := make([]byte, n)
rand.Read(buf)
sendBuffer = append(sendBuffer, buf)
}
var buf [MessageInitiationSize]byte var buf [MessageInitiationSize]byte
writer := bytes.NewBuffer(buf[:0]) writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, msg) binary.Write(writer, binary.LittleEndian, msg)
@ -133,7 +239,16 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent() peer.timersAnyAuthenticatedPacketSent()
err = peer.SendBuffers([][]byte{packet}) if padding := peer.device.paddings.init; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
sendBuffer = append(sendBuffer, packet)
err = peer.SendBuffers(sendBuffer)
if err != nil { if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err)
} }
@ -157,6 +272,7 @@ func (peer *Peer) SendHandshakeResponse() error {
var buf [MessageResponseSize]byte var buf [MessageResponseSize]byte
writer := bytes.NewBuffer(buf[:0]) writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, response) binary.Write(writer, binary.LittleEndian, response)
packet := writer.Bytes() packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet) peer.cookieGenerator.AddMacs(packet)
@ -171,6 +287,13 @@ func (peer *Peer) SendHandshakeResponse() error {
peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent() peer.timersAnyAuthenticatedPacketSent()
if padding := peer.device.paddings.response; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
// TODO: allocation could be avoided // TODO: allocation could be avoided
err = peer.SendBuffers([][]byte{packet}) err = peer.SendBuffers([][]byte{packet})
if err != nil { if err != nil {
@ -183,7 +306,14 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString())
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8]) sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes()) msgType := device.headers.cookie.Generate()
reply, err := device.cookieChecker.CreateReply(
initiatingElem.packet,
sender,
initiatingElem.endpoint.DstToBytes(),
msgType,
)
if err != nil { if err != nil {
device.log.Errorf("Failed to create cookie reply: %v", err) device.log.Errorf("Failed to create cookie reply: %v", err)
return err return err
@ -192,8 +322,17 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
var buf [MessageCookieReplySize]byte var buf [MessageCookieReplySize]byte
writer := bytes.NewBuffer(buf[:0]) writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, reply) binary.Write(writer, binary.LittleEndian, reply)
packet := writer.Bytes()
if padding := device.paddings.cookie; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
// TODO: allocation could be avoided // TODO: allocation could be avoided
device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint) device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint, 0)
return nil return nil
} }
@ -225,7 +364,7 @@ func (device *Device) RoutineReadFromTUN() {
elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize) elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize)
count = 0 count = 0
sizes = make([]int, batchSize) sizes = make([]int, batchSize)
offset = MessageTransportHeaderSize offset = MessageEncapsulatingTransportSize + MessageTransportHeaderSize
) )
for i := range elems { for i := range elems {
@ -236,7 +375,7 @@ func (device *Device) RoutineReadFromTUN() {
defer func() { defer func() {
for _, elem := range elems { for _, elem := range elems {
if elem != nil { if elem != nil {
device.PutMessageBuffer(elem.buffer) device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem) device.PutOutboundElement(elem)
} }
} }
@ -260,15 +399,17 @@ func (device *Device) RoutineReadFromTUN() {
if len(elem.packet) < ipv4.HeaderLen { if len(elem.packet) < ipv4.HeaderLen {
continue continue
} }
dst := elem.packet[IPv4offsetDst : IPv4offsetDst+net.IPv4len] src := netip.AddrFrom4([4]byte(elem.packet[IPv4offsetSrc : IPv4offsetSrc+net.IPv4len]))
peer = device.allowedips.Lookup(dst) dst := netip.AddrFrom4([4]byte(elem.packet[IPv4offsetDst : IPv4offsetDst+net.IPv4len]))
peer = device.allowedips.LookupFromPacket(src, dst, elem.packet)
case 6: case 6:
if len(elem.packet) < ipv6.HeaderLen { if len(elem.packet) < ipv6.HeaderLen {
continue continue
} }
dst := elem.packet[IPv6offsetDst : IPv6offsetDst+net.IPv6len] src := netip.AddrFrom16([16]byte(elem.packet[IPv6offsetSrc : IPv6offsetSrc+net.IPv6len]))
peer = device.allowedips.Lookup(dst) dst := netip.AddrFrom16([16]byte(elem.packet[IPv6offsetDst : IPv6offsetDst+net.IPv6len]))
peer = device.allowedips.LookupFromPacket(src, dst, elem.packet)
default: default:
device.log.Verbosef("Received packet with unknown IP version") device.log.Verbosef("Received packet with unknown IP version")
@ -293,7 +434,7 @@ func (device *Device) RoutineReadFromTUN() {
peer.SendStagedPackets() peer.SendStagedPackets()
} else { } else {
for _, elem := range elemsForPeer.elems { for _, elem := range elemsForPeer.elems {
device.PutMessageBuffer(elem.buffer) device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem) device.PutOutboundElement(elem)
} }
device.PutOutboundElementsContainer(elemsForPeer) device.PutOutboundElementsContainer(elemsForPeer)
@ -320,7 +461,160 @@ func (device *Device) RoutineReadFromTUN() {
} }
} }
// maxQueuedInputPackets bounds the staged+outbound backlog of a peer fed via
// InputPacket/InputPackets. Injected packets beyond it are dropped before they
// are copied into pooled message buffers, like a full qdisc: injection has no
// flow control, and the queues are bounded in containers (up to a full batch
// each), so without this cap a flood is buffered instead of dropped.
const maxQueuedInputPackets = 2048
func (device *Device) inputPacketPeer(destination []byte, packetSlices [][]byte) *Peer {
var src, dst netip.Addr
switch len(destination) {
case net.IPv4len:
dst = netip.AddrFrom4([4]byte(destination))
var srcBytes [net.IPv4len]byte
if !gatherPacketBytes(packetSlices, IPv4offsetSrc, srcBytes[:]) {
return nil
}
src = netip.AddrFrom4(srcBytes)
case net.IPv6len:
dst = netip.AddrFrom16([16]byte(destination))
var srcBytes [net.IPv6len]byte
if !gatherPacketBytes(packetSlices, IPv6offsetSrc, srcBytes[:]) {
return nil
}
src = netip.AddrFrom16(srcBytes)
default:
return nil
}
var ipPkt []byte
if len(packetSlices) == 1 {
ipPkt = packetSlices[0]
}
return device.allowedips.LookupFromPacket(src, dst, ipPkt)
}
func gatherPacketBytes(packetSlices [][]byte, offset int, destination []byte) bool {
for _, packetSlice := range packetSlices {
if offset >= len(packetSlice) {
offset -= len(packetSlice)
continue
}
n := copy(destination, packetSlice[offset:])
destination = destination[n:]
offset = 0
if len(destination) == 0 {
return true
}
}
return false
}
func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) {
peer := device.inputPacketPeer(destination, packetSlices)
if peer == nil {
return
}
if peer.queuedOutboundPackets.Load() >= maxQueuedInputPackets {
return
}
var totalLength int
for _, packetSlice := range packetSlices {
totalLength += len(packetSlice)
}
// paddings.transport (AWG s4) is prepended in-buffer by
// RoutineSequentialSender; reserve headroom for the shift.
allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + device.paddings.transport
if allocLength > MaxMessageSize {
return
}
elem := device.GetOutboundElement()
elem.buffer = device.GetOutboundBuffer(allocLength)
elem.nonce = 0
packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:]
var n int
for _, packetSlice := range packetSlices {
n += copy(packet[n:], packetSlice)
}
elem.packet = packet[:n]
elemsForPeer := device.GetOutboundElementsContainer()
if peer.isRunning.Load() {
elemsForPeer.elems = append(elemsForPeer.elems, elem)
peer.StagePackets(elemsForPeer)
peer.SendStagedPackets()
} else {
device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem)
device.PutOutboundElementsContainer(elemsForPeer)
}
}
type InputPacketRef struct {
Destination []byte
PacketSlices [][]byte
}
func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef {
var unmatched []*InputPacketRef
elemsByPeer := make(map[*Peer][]*QueueOutboundElementsContainer, len(packets))
for _, packetRef := range packets {
peer := device.inputPacketPeer(packetRef.Destination, packetRef.PacketSlices)
if peer == nil {
unmatched = append(unmatched, packetRef)
continue
}
if peer.queuedOutboundPackets.Load() >= maxQueuedInputPackets {
continue
}
var totalLength int
for _, packetSlice := range packetRef.PacketSlices {
totalLength += len(packetSlice)
}
// paddings.transport (AWG s4) is prepended in-buffer by
// RoutineSequentialSender; reserve headroom for the shift.
allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + device.paddings.transport
if allocLength > MaxMessageSize {
continue
}
elem := device.GetOutboundElement()
elem.buffer = device.GetOutboundBuffer(allocLength)
elem.nonce = 0
packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:]
var n int
for _, packetSlice := range packetRef.PacketSlices {
n += copy(packet[n:], packetSlice)
}
elem.packet = packet[:n]
containers := elemsByPeer[peer]
if len(containers) == 0 || len(containers[len(containers)-1].elems) >= conn.IdealBatchSize {
containers = append(containers, device.GetOutboundElementsContainer())
elemsByPeer[peer] = containers
}
elemsForPeer := containers[len(containers)-1]
elemsForPeer.elems = append(elemsForPeer.elems, elem)
}
for peer, containers := range elemsByPeer {
if peer.isRunning.Load() {
for _, elemsForPeer := range containers {
peer.StagePackets(elemsForPeer)
}
peer.SendStagedPackets()
} else {
for _, elemsForPeer := range containers {
for _, elem := range elemsForPeer.elems {
device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem)
}
device.PutOutboundElementsContainer(elemsForPeer)
}
}
}
return unmatched
}
func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) {
peer.queuedOutboundPackets.Add(int32(len(elems.elems)))
for { for {
select { select {
case peer.queue.staged <- elems: case peer.queue.staged <- elems:
@ -329,8 +623,9 @@ func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) {
} }
select { select {
case tooOld := <-peer.queue.staged: case tooOld := <-peer.queue.staged:
peer.queuedOutboundPackets.Add(-int32(len(tooOld.elems)))
for _, elem := range tooOld.elems { for _, elem := range tooOld.elems {
peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundBuffer(elem.buffer)
peer.device.PutOutboundElement(elem) peer.device.PutOutboundElement(elem)
} }
peer.device.PutOutboundElementsContainer(tooOld) peer.device.PutOutboundElementsContainer(tooOld)
@ -373,10 +668,11 @@ top:
elem.keypair = keypair elem.keypair = keypair
} }
elemsContainer.Lock()
elemsContainer.elems = elemsContainer.elems[:i] elemsContainer.elems = elemsContainer.elems[:i]
if elemsContainerOOO != nil { if elemsContainerOOO != nil {
// Already counted at their original staging; StagePackets will count them again.
peer.queuedOutboundPackets.Add(-int32(len(elemsContainerOOO.elems)))
peer.StagePackets(elemsContainerOOO) // XXX: Out of order, but we can't front-load go chans peer.StagePackets(elemsContainerOOO) // XXX: Out of order, but we can't front-load go chans
} }
@ -387,11 +683,13 @@ top:
// add to parallel and sequential queue // add to parallel and sequential queue
if peer.isRunning.Load() { if peer.isRunning.Load() {
elemsContainer.filling.Add(1)
peer.queue.outbound.c <- elemsContainer peer.queue.outbound.c <- elemsContainer
peer.device.queue.encryption.c <- elemsContainer peer.device.queue.encryption.c <- elemsContainer
} else { } else {
peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems)))
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundBuffer(elem.buffer)
peer.device.PutOutboundElement(elem) peer.device.PutOutboundElement(elem)
} }
peer.device.PutOutboundElementsContainer(elemsContainer) peer.device.PutOutboundElementsContainer(elemsContainer)
@ -410,8 +708,9 @@ func (peer *Peer) FlushStagedPackets() {
for { for {
select { select {
case elemsContainer := <-peer.queue.staged: case elemsContainer := <-peer.queue.staged:
peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems)))
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundBuffer(elem.buffer)
peer.device.PutOutboundElement(elem) peer.device.PutOutboundElement(elem)
} }
peer.device.PutOutboundElementsContainer(elemsContainer) peer.device.PutOutboundElementsContainer(elemsContainer)
@ -451,13 +750,15 @@ func (device *Device) RoutineEncryption(id int) {
for elemsContainer := range device.queue.encryption.c { for elemsContainer := range device.queue.encryption.c {
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
// populate header fields // populate header fields
header := elem.buffer[:MessageTransportHeaderSize] header := elem.buffer[MessageEncapsulatingTransportSize : MessageEncapsulatingTransportSize+MessageTransportHeaderSize]
fieldType := header[0:4] fieldType := header[0:4]
fieldReceiver := header[4:8] fieldReceiver := header[4:8]
fieldNonce := header[8:16] fieldNonce := header[8:16]
binary.LittleEndian.PutUint32(fieldType, MessageTransportType) msgType := device.headers.transport.Generate()
binary.LittleEndian.PutUint32(fieldType, msgType)
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
@ -475,7 +776,7 @@ func (device *Device) RoutineEncryption(id int) {
nil, nil,
) )
} }
elemsContainer.Unlock() elemsContainer.filling.Done()
} }
} }
@ -487,60 +788,100 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
}() }()
device.log.Verbosef("%v - Routine: sequential sender - started", peer) device.log.Verbosef("%v - Routine: sequential sender - started", peer)
bufs := make([][]byte, 0, maxBatchSize) bufs := make([][]byte, 0, max(maxBatchSize, conn.IdealBatchSize))
for elemsContainer := range peer.queue.outbound.c { for elemsContainer := range peer.queue.outbound.c {
bufs = bufs[:0]
if elemsContainer == nil { if elemsContainer == nil {
return return
} }
if !peer.isRunning.Load() { peer.processOutboundContainer(elemsContainer, bufs[:0])
// peer has been stopped; return re-usable elems to the shared pool.
// This is an optimization only. It is possible for the peer to be stopped
// immediately after this check, in which case, elem will get processed.
// The timers and SendBuffers code are resilient to a few stragglers.
// TODO: rework peer shutdown order to ensure
// that we never accidentally keep timers alive longer than necessary.
elemsContainer.Lock()
for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer)
device.PutOutboundElement(elem)
}
continue
}
dataSent := false
elemsContainer.Lock()
for _, elem := range elemsContainer.elems {
if len(elem.packet) != MessageKeepaliveSize {
dataSent = true
}
bufs = append(bufs, elem.packet)
}
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
err := peer.SendBuffers(bufs)
if dataSent {
peer.timersDataSent()
}
for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer)
device.PutOutboundElement(elem)
}
device.PutOutboundElementsContainer(elemsContainer)
if err != nil {
var errGSO conn.ErrUDPGSODisabled
if errors.As(err, &errGSO) {
device.log.Verbosef(err.Error())
err = errGSO.RetryErr
}
}
if err != nil {
device.log.Errorf("%v - Failed to send data packets: %v", peer, err)
continue
}
peer.keepKeyFreshSending()
} }
} }
// processOutboundContainer waits for the encryption routine to finish
// filling elemsContainer, then sends the batch (or drops it, if the peer
// has been stopped) and returns the container to the pool.
//
// scratch is a length-0 slice used to assemble the per-packet buffers
// passed to SendBuffers; its backing array is reused across calls.
func (peer *Peer) processOutboundContainer(elemsContainer *QueueOutboundElementsContainer, scratch [][]byte) {
// Invariants from RoutineSequentialSender; all should be unreachable.
if len(scratch) != 0 || cap(scratch) == 0 {
panic(fmt.Sprintf("processOutboundContainer: scratch must be empty with non-zero cap; got len=%d cap=%d",
len(scratch), cap(scratch)))
}
if cap(scratch) < len(elemsContainer.elems) {
panic(fmt.Sprintf("processOutboundContainer: scratch cap %d < elems %d",
cap(scratch), len(elemsContainer.elems)))
}
device := peer.device
defer device.PutOutboundElementsContainer(elemsContainer)
// Wait for RoutineEncryption to finish filling the container. After
// Wait returns we have happens-before with that goroutine and are the
// sole owner of the container until Put hands it back to the pool.
elemsContainer.filling.Wait()
if !peer.isRunning.Load() {
// peer has been stopped; return re-usable elems to the shared pool.
// This is an optimization only. It is possible for the peer to be stopped
// immediately after this check, in which case, elem will get processed.
// The timers and SendBuffers code are resilient to a few stragglers.
// TODO: rework peer shutdown order to ensure
// that we never accidentally keep timers alive longer than necessary.
peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems)))
for _, elem := range elemsContainer.elems {
device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem)
}
return
}
dataSent := false
for _, elem := range elemsContainer.elems {
if len(elem.packet[MessageEncapsulatingTransportSize:]) != MessageKeepaliveSize {
dataSent = true
}
// lx:begin awg (SPEC 025 — AmneziaWG transport padding, S4)
// Prepend `transport` random bytes ahead of the transport header. The AWG
// path zeroes MessageEncapsulatingTransportSize (see noise-protocol.go), so
// elem.packet starts at buffer offset 0 and this shift is what creates the
// prefix; the buffer is allocated with PaddingMultiple headroom.
if padding := device.paddings.transport; padding > 0 {
for i := len(elem.packet) - 1; i >= 0; i-- {
elem.buffer[i+padding] = elem.buffer[i]
}
rand.Read(elem.buffer[:padding])
elem.packet = elem.buffer[:padding+len(elem.packet)]
}
// lx:end awg
scratch = append(scratch, elem.packet)
}
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
err := peer.SendBuffers(scratch)
if dataSent {
peer.timersDataSent()
}
peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems)))
for _, elem := range elemsContainer.elems {
device.PutOutboundBuffer(elem.buffer)
device.PutOutboundElement(elem)
}
if err != nil {
var errGSO conn.ErrUDPGSODisabled
if errors.As(err, &errGSO) {
device.log.Verbosef(err.Error())
err = errGSO.RetryErr
}
}
if err != nil {
device.log.Errorf("%v - Failed to send data packets: %v", peer, err)
return
}
peer.keepKeyFreshSending()
}

View file

@ -3,10 +3,10 @@
package device package device
import ( import (
"golang.zx2c4.com/wireguard/conn" "github.com/sagernet/wireguard-go/conn"
"golang.zx2c4.com/wireguard/rwcancel" "github.com/sagernet/wireguard-go/rwcancel"
) )
func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { func (device *Device) startRouteListener(_ conn.Bind) (*rwcancel.RWCancel, error) {
return nil, nil return nil, nil
} }

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
* *
* This implements userspace semantics of "sticky sockets", modeled after * This implements userspace semantics of "sticky sockets", modeled after
* WireGuard's kernelspace implementation. This is more or less a straight port * WireGuard's kernelspace implementation. This is more or less a straight port
@ -9,7 +9,7 @@
* *
* Currently there is no way to achieve this within the net package: * Currently there is no way to achieve this within the net package:
* See e.g. https://github.com/golang/go/issues/17930 * See e.g. https://github.com/golang/go/issues/17930
* So this code is remains platform dependent. * So this code remains platform dependent.
*/ */
package device package device
@ -18,10 +18,9 @@ import (
"sync" "sync"
"unsafe" "unsafe"
"github.com/sagernet/wireguard-go/conn"
"github.com/sagernet/wireguard-go/rwcancel"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/rwcancel"
) )
func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) {
@ -47,7 +46,7 @@ func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, er
return netlinkCancel, nil return netlinkCancel, nil
} }
func (device *Device) routineRouteListener(bind conn.Bind, netlinkSock int, netlinkCancel *rwcancel.RWCancel) { func (device *Device) routineRouteListener(_ conn.Bind, netlinkSock int, netlinkCancel *rwcancel.RWCancel) {
type peerEndpointPtr struct { type peerEndpointPtr struct {
peer *Peer peer *Peer
endpoint *conn.Endpoint endpoint *conn.Endpoint

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
* *
* This is based heavily on timers.c from the kernel implementation. * This is based heavily on timers.c from the kernel implementation.
*/ */
@ -39,6 +39,9 @@ func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer {
timer.isPending = false timer.isPending = false
timer.modifyingLock.Unlock() timer.modifyingLock.Unlock()
if pauseManager := peer.device.pauseManager; pauseManager != nil {
pauseManager.WaitActive()
}
expirationFunction(peer) expirationFunction(peer)
}) })
timer.Stop() timer.Stop()
@ -95,10 +98,26 @@ func expiredRetransmitHandshake(peer *Peer) {
if peer.timersActive() && !peer.timers.zeroKeyMaterial.IsPending() { if peer.timersActive() && !peer.timers.zeroKeyMaterial.IsPending() {
peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3) peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3)
} }
peer.noteSessionHandshakeStopped()
/* lx: SPEC 041 the exhausted cycle just proved the current socket's
* 5-tuple dead (90s of initiations, zero replies). Rebind once and
* re-initiate, so a stale NAT mapping / poisoned DPI flow entry cannot
* pin this peer to a dead socket until a manual reconnect. Runs after
* the session-state notification so a consumer sees "handshake stopped"
* before the socket is recreated. */
peer.device.handleHandshakeGiveUp(peer)
} else { } else {
peer.timers.handshakeAttempts.Add(1) peer.timers.handshakeAttempts.Add(1)
peer.device.log.Verbosef("%s - Handshake did not complete after %d seconds, retrying (try %d)", peer, int(RekeyTimeout.Seconds()), peer.timers.handshakeAttempts.Load()+1) peer.device.log.Verbosef("%s - Handshake did not complete after %d seconds, retrying (try %d)", peer, int(RekeyTimeout.Seconds()), peer.timers.handshakeAttempts.Load()+1)
/* lx: SPEC 041 v2 early self-heal: >=3 unanswered initiations against
* a provably dead session (no live keypair, or last handshake older
* than RejectAfterTime) prove the 5-tuple dead without waiting out the
* full cycle. Rebind now (debounced with the give-up trigger below);
* the retry cycle itself continues untouched. */
peer.device.maybeEarlyGiveUpRebind(peer)
/* We clear the endpoint address src address, in case this is the cause of trouble. */ /* We clear the endpoint address src address, in case this is the cause of trouble. */
peer.markEndpointSrcForClearing() peer.markEndpointSrcForClearing()
@ -126,6 +145,24 @@ func expiredNewHandshake(peer *Peer) {
func expiredZeroKeyMaterial(peer *Peer) { func expiredZeroKeyMaterial(peer *Peer) {
peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int((RejectAfterTime * 3).Seconds())) peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int((RejectAfterTime * 3).Seconds()))
peer.ZeroAndFlushAll() peer.ZeroAndFlushAll()
if peer.deleteOnIdle {
peer.device.log.Verbosef("%s - Removing idle lazy peer", peer)
// Remove the peer from the device in a new goroutine as we're currently
// holding timer locks which RemovePeer also needs. This is TOCTOU, but
// acceptable since the worst case is we remove the peer and the lazy
// peerfunc created it again after. We might lose some packets.
go peer.device.RemovePeer(peer.handshake.remoteStatic)
}
}
func expiredSession(peer *Peer) {
peer.sessionState.Lock()
defer peer.sessionState.Unlock()
if peer.sessionState.sessionExpires.IsZero() || time.Now().Before(peer.sessionState.sessionExpires) {
return
}
peer.device.log.Verbosef("%s - Session expired after %d seconds", peer, int(RejectAfterTime.Seconds()))
peer.noteSessionStateLocked(PeerSessionExpired)
} }
func expiredPersistentKeepalive(peer *Peer) { func expiredPersistentKeepalive(peer *Peer) {
@ -171,6 +208,7 @@ func (peer *Peer) timersHandshakeInitiated() {
if peer.timersActive() { if peer.timersActive() {
peer.timers.retransmitHandshake.Mod(RekeyTimeout + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs))) peer.timers.retransmitHandshake.Mod(RekeyTimeout + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs)))
} }
peer.noteSessionHandshakeStarted()
} }
/* Should be called after a handshake response message is received and processed or when getting key confirmation via the first data message. */ /* Should be called after a handshake response message is received and processed or when getting key confirmation via the first data message. */
@ -186,7 +224,14 @@ func (peer *Peer) timersHandshakeComplete() {
/* Should be called after an ephemeral key is created, which is before sending a handshake response or after receiving a handshake response. */ /* Should be called after an ephemeral key is created, which is before sending a handshake response or after receiving a handshake response. */
func (peer *Peer) timersSessionDerived() { func (peer *Peer) timersSessionDerived() {
if peer.timersActive() { if peer.timersActive() {
peer.sessionState.Lock()
peer.sessionState.sessionExpires = time.Now().Add(RejectAfterTime)
peer.noteSessionStateLocked(PeerSessionEstablished)
peer.sessionState.Unlock()
peer.timers.sessionExpired.Mod(RejectAfterTime)
peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3) peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3)
} else {
peer.noteSessionState(PeerSessionEstablished)
} }
} }
@ -202,6 +247,7 @@ func (peer *Peer) timersInit() {
peer.timers.retransmitHandshake = peer.NewTimer(expiredRetransmitHandshake) peer.timers.retransmitHandshake = peer.NewTimer(expiredRetransmitHandshake)
peer.timers.sendKeepalive = peer.NewTimer(expiredSendKeepalive) peer.timers.sendKeepalive = peer.NewTimer(expiredSendKeepalive)
peer.timers.newHandshake = peer.NewTimer(expiredNewHandshake) peer.timers.newHandshake = peer.NewTimer(expiredNewHandshake)
peer.timers.sessionExpired = peer.NewTimer(expiredSession)
peer.timers.zeroKeyMaterial = peer.NewTimer(expiredZeroKeyMaterial) peer.timers.zeroKeyMaterial = peer.NewTimer(expiredZeroKeyMaterial)
peer.timers.persistentKeepalive = peer.NewTimer(expiredPersistentKeepalive) peer.timers.persistentKeepalive = peer.NewTimer(expiredPersistentKeepalive)
} }
@ -216,6 +262,7 @@ func (peer *Peer) timersStop() {
peer.timers.retransmitHandshake.DelSync() peer.timers.retransmitHandshake.DelSync()
peer.timers.sendKeepalive.DelSync() peer.timers.sendKeepalive.DelSync()
peer.timers.newHandshake.DelSync() peer.timers.newHandshake.DelSync()
peer.timers.sessionExpired.DelSync()
peer.timers.zeroKeyMaterial.DelSync() peer.timers.zeroKeyMaterial.DelSync()
peer.timers.persistentKeepalive.DelSync() peer.timers.persistentKeepalive.DelSync()
} }

View file

@ -0,0 +1,361 @@
/* SPDX-License-Identifier: MIT
*
* Regression test for the AWG transport-padding (uapi "s4") out-of-bounds
* crash: RoutineSequentialSender shifts elem.packet right by
* device.paddings.transport bytes inside elem.buffer to prepend a random
* padding prefix, but the injection paths (InputPacket/InputPackets)
* allocated elem.buffer tightly, without headroom for that shift:
*
* panic: runtime error: index out of range [123] with length 76
* device.(*Peer).RoutineSequentialSender
*
* (payload 28 bytes -> allocLength 76, sealed packet 64, s4=60 -> 63+60=123).
*
* The tests spin up two real Devices wired together through an in-memory
* conn.Bind (Go channels) and an in-memory tun.Device, configure s4=60 on
* both sides, and pass a small IPv4 packet end to end via both outbound
* paths: Device.InputPacket (the crashing path) and the regular tun read
* loop (in-place shift path).
*/
package device
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"net/netip"
"os"
"sync"
"testing"
"time"
"github.com/sagernet/wireguard-go/conn"
"github.com/sagernet/wireguard-go/tun"
"golang.org/x/net/ipv4"
)
const testTransportPadding = 60 // uapi s4, matches the on-device crash
// ---------------------------------------------------------------------------
// In-memory conn.Bind over Go channels (minimal bindtest.ChannelBind clone).
// ---------------------------------------------------------------------------
type chanEndpoint uint16
func (e chanEndpoint) ClearSrc() {}
func (e chanEndpoint) SrcToString() string { return "" }
func (e chanEndpoint) DstToString() string { return fmt.Sprintf("127.0.0.1:%d", uint16(e)) }
func (e chanEndpoint) DstToBytes() []byte { return []byte{byte(e), byte(e >> 8)} }
func (e chanEndpoint) DstIP() netip.Addr { return netip.AddrFrom4([4]byte{127, 0, 0, 1}) }
func (e chanEndpoint) SrcIP() netip.Addr { return netip.Addr{} }
type chanBind struct {
rx, tx chan []byte
source chanEndpoint // "port" this bind listens on
target chanEndpoint // endpoint of the opposite bind
mu sync.Mutex
closeSignal chan struct{} // recreated on every Open (BindUpdate closes+reopens)
}
// newChanBindPair returns two Binds whose Send/Receive are cross-wired.
func newChanBindPair() (*chanBind, *chanBind) {
aToB := make(chan []byte, 1024)
bToA := make(chan []byte, 1024)
a := &chanBind{rx: bToA, tx: aToB, source: 1, target: 2}
b := &chanBind{rx: aToB, tx: bToA, source: 2, target: 1}
return a, b
}
func (b *chanBind) currentCloseSignal() chan struct{} {
b.mu.Lock()
defer b.mu.Unlock()
return b.closeSignal
}
func (b *chanBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
b.mu.Lock()
b.closeSignal = make(chan struct{})
closeSignal := b.closeSignal
b.mu.Unlock()
fn := func(packets [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
select {
case <-closeSignal:
// Must be net.ErrClosed: RoutineReceiveIncoming treats anything
// else as a transient error and death-spirals before exiting.
return 0, net.ErrClosed
case pkt, ok := <-b.rx:
if !ok {
return 0, net.ErrClosed
}
sizes[0] = copy(packets[0], pkt)
eps[0] = b.target
return 1, nil
}
}
return []conn.ReceiveFunc{fn}, uint16(b.source), nil
}
func (b *chanBind) Close() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.closeSignal != nil {
select {
case <-b.closeSignal:
default:
close(b.closeSignal)
}
}
return nil
}
func (b *chanBind) SetMark(mark uint32) error { return nil }
func (b *chanBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error {
closeSignal := b.currentCloseSignal()
if closeSignal == nil {
return net.ErrClosed
}
for _, buf := range bufs {
pkt := make([]byte, len(buf)-offset)
copy(pkt, buf[offset:])
select {
case <-closeSignal:
return net.ErrClosed
case b.tx <- pkt:
}
}
return nil
}
func (b *chanBind) ParseEndpoint(s string) (conn.Endpoint, error) { return b.target, nil }
func (b *chanBind) BatchSize() int { return 1 }
func (b *chanBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) {}
// ---------------------------------------------------------------------------
// In-memory tun.Device over Go channels (minimal tuntest.ChannelTUN clone).
// ---------------------------------------------------------------------------
type chanTun struct {
toDevice chan []byte // packets the device Reads (outbound plaintext)
fromDevice chan []byte // packets the device Writes (inbound plaintext)
events chan tun.Event
closed chan struct{}
closeOnce sync.Once
}
func newChanTun() *chanTun {
return &chanTun{
toDevice: make(chan []byte, 1024),
fromDevice: make(chan []byte, 1024),
events: make(chan tun.Event, 4),
closed: make(chan struct{}),
}
}
func (t *chanTun) File() *os.File { return nil }
func (t *chanTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
select {
case <-t.closed:
return 0, os.ErrClosed
case pkt, ok := <-t.toDevice:
if !ok {
return 0, os.ErrClosed
}
sizes[0] = copy(bufs[0][offset:], pkt)
return 1, nil
}
}
func (t *chanTun) Write(bufs [][]byte, offset int) (int, error) {
for _, buf := range bufs {
pkt := make([]byte, len(buf)-offset)
copy(pkt, buf[offset:])
select {
case <-t.closed:
return 0, os.ErrClosed
case t.fromDevice <- pkt:
}
}
return len(bufs), nil
}
func (t *chanTun) MTU() (int, error) { return DefaultMTU, nil }
func (t *chanTun) Name() (string, error) { return "chantun", nil }
func (t *chanTun) Events() <-chan tun.Event { return t.events }
func (t *chanTun) BatchSize() int { return 1 }
func (t *chanTun) Close() error {
t.closeOnce.Do(func() {
close(t.closed)
close(t.events)
})
return nil
}
// ---------------------------------------------------------------------------
// Test scaffolding.
// ---------------------------------------------------------------------------
var (
testIPA = netip.AddrFrom4([4]byte{10, 0, 0, 1})
testIPB = netip.AddrFrom4([4]byte{10, 0, 0, 2})
)
// buildIPv4Packet builds a minimal, routable IPv4/UDP packet whose header
// fields satisfy the receive-side validation in RoutineSequentialReceiver
// (version, total-length field, allowed source address).
func buildIPv4Packet(src, dst netip.Addr, payloadLen int) []byte {
total := ipv4.HeaderLen + payloadLen
pkt := make([]byte, total)
pkt[0] = 0x45 // version 4, IHL 5
binary.BigEndian.PutUint16(pkt[IPv4offsetTotalLength:IPv4offsetTotalLength+2], uint16(total))
pkt[8] = 64 // TTL
pkt[9] = 17 // protocol: UDP
copy(pkt[IPv4offsetSrc:], src.AsSlice())
copy(pkt[IPv4offsetDst:], dst.AsSlice())
for i := ipv4.HeaderLen; i < total; i++ {
pkt[i] = byte(i) // deterministic payload
}
return pkt
}
type paddedPair struct {
devA, devB *Device
tunA, tunB *chanTun
}
// newPaddedDevicePair builds two Up()'d devices peered with each other over
// the channel bind, both configured with s4 (transport padding) enabled.
func newPaddedDevicePair(t *testing.T) *paddedPair {
t.Helper()
skA, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey A: %v", err)
}
skB, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey B: %v", err)
}
pkA := skA.publicKey()
pkB := skB.publicKey()
bindA, bindB := newChanBindPair()
tunA := newChanTun()
tunB := newChanTun()
devA := NewDevice(context.Background(), tunA, bindA, NewLogger(LogLevelError, "devA: "), 1)
devB := NewDevice(context.Background(), tunB, bindB, NewLogger(LogLevelError, "devB: "), 1)
t.Cleanup(devA.Close)
t.Cleanup(devB.Close)
cfgA := fmt.Sprintf(
"private_key=%s\ns4=%d\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:2\nallowed_ip=%s/32\n",
hex.EncodeToString(skA[:]), testTransportPadding, hex.EncodeToString(pkB[:]), testIPB)
cfgB := fmt.Sprintf(
"private_key=%s\ns4=%d\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:1\nallowed_ip=%s/32\n",
hex.EncodeToString(skB[:]), testTransportPadding, hex.EncodeToString(pkA[:]), testIPA)
if err := devA.IpcSet(cfgA); err != nil {
t.Fatalf("IpcSet A: %v", err)
}
if err := devB.IpcSet(cfgB); err != nil {
t.Fatalf("IpcSet B: %v", err)
}
if devA.paddings.transport != testTransportPadding {
t.Fatalf("s4 not applied: paddings.transport = %d", devA.paddings.transport)
}
if err := devA.Up(); err != nil {
t.Fatalf("Up A: %v", err)
}
if err := devB.Up(); err != nil {
t.Fatalf("Up B: %v", err)
}
return &paddedPair{devA: devA, devB: devB, tunA: tunA, tunB: tunB}
}
// awaitPacket waits for want to arrive on the receiving tun, periodically
// re-sending via resend (injection has no delivery guarantee before the
// handshake completes).
func awaitPacket(t *testing.T, from *chanTun, want []byte, resend func()) {
t.Helper()
deadline := time.After(20 * time.Second)
retry := time.NewTicker(1 * time.Second)
defer retry.Stop()
for {
select {
case got := <-from.fromDevice:
if bytes.Equal(got, want) {
return
}
t.Logf("ignoring unexpected packet, len=%d", len(got))
case <-retry.C:
resend()
case <-deadline:
t.Fatal("timed out waiting for packet on peer tun")
}
}
}
// ---------------------------------------------------------------------------
// Tests.
// ---------------------------------------------------------------------------
// TestTransportPaddingInputPacket exercises the exact crash path: an injected
// packet (Device.InputPacket) whose buffer was allocated by payload size.
// With s4=60 and a 28-byte IPv4 packet the pre-fix buffer was 76 bytes and
// the padding shift indexed [123] -> index out of range.
func TestTransportPaddingInputPacket(t *testing.T) {
pair := newPaddedDevicePair(t)
// 20-byte header + 8-byte payload = 28 bytes, the on-device crash size.
pkt := buildIPv4Packet(testIPA, testIPB, 8)
dst := testIPB.AsSlice()
send := func() { pair.devA.InputPacket(dst, [][]byte{pkt}) }
send()
awaitPacket(t, pair.tunB, pkt, send)
}
// TestTransportPaddingInputPackets covers the batched injection path
// (Device.InputPackets), which had the same tight allocation.
func TestTransportPaddingInputPackets(t *testing.T) {
pair := newPaddedDevicePair(t)
pkt := buildIPv4Packet(testIPA, testIPB, 8)
refs := []*InputPacketRef{{
Destination: testIPB.AsSlice(),
PacketSlices: [][]byte{pkt[:12], pkt[12:]}, // multi-slice on purpose
}}
send := func() {
if unmatched := pair.devA.InputPackets(refs); len(unmatched) != 0 {
t.Fatalf("InputPackets returned %d unmatched refs", len(unmatched))
}
}
send()
awaitPacket(t, pair.tunB, pkt, send)
}
// TestTransportPaddingTunPath covers the regular outbound path (tun read
// loop), whose MaxMessageSize buffers take the in-place shift branch.
func TestTransportPaddingTunPath(t *testing.T) {
pair := newPaddedDevicePair(t)
pkt := buildIPv4Packet(testIPA, testIPB, 8)
send := func() { pair.tunA.toDevice <- pkt }
send()
awaitPacket(t, pair.tunB, pkt, send)
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -8,7 +8,7 @@ package device
import ( import (
"fmt" "fmt"
"golang.zx2c4.com/wireguard/tun" "github.com/sagernet/wireguard-go/tun"
) )
const DefaultMTU = 1420 const DefaultMTU = 1420

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -18,7 +18,7 @@ import (
"sync" "sync"
"time" "time"
"golang.zx2c4.com/wireguard/ipc" "github.com/sagernet/wireguard-go/ipc"
) )
type IPCError struct { type IPCError struct {
@ -97,6 +97,56 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
sendf("fwmark=%d", device.net.fwmark) sendf("fwmark=%d", device.net.fwmark)
} }
if device.junk.count != 0 {
sendf("jc=%d", device.junk.count)
}
if device.junk.min != 0 {
sendf("jmin=%d", device.junk.min)
}
if device.junk.max != 0 {
sendf("jmax=%d", device.junk.max)
}
if device.paddings.init != 0 {
sendf("s1=%d", device.paddings.init)
}
if device.paddings.response != 0 {
sendf("s2=%d", device.paddings.response)
}
if device.paddings.cookie != 0 {
sendf("s3=%d", device.paddings.cookie)
}
if device.paddings.transport != 0 {
sendf("s4=%d", device.paddings.transport)
}
if device.headers.init != nil {
sendf("h1=%s", device.headers.init.GenSpec())
}
if device.headers.response != nil {
sendf("h2=%s", device.headers.response.GenSpec())
}
if device.headers.cookie != nil {
sendf("h3=%s", device.headers.cookie.GenSpec())
}
if device.headers.transport != nil {
sendf("h4=%s", device.headers.transport.GenSpec())
}
for i, ipacket := range device.ipackets {
if ipacket != nil {
sendf("i%d=%s", i+1, ipacket.Spec)
}
}
for _, peer := range device.peers.keyMap { for _, peer := range device.peers.keyMap {
// Serialize peer state. // Serialize peer state.
peer.handshake.mutex.RLock() peer.handshake.mutex.RLock()
@ -147,6 +197,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
} }
}() }()
ipcDev := new(ipcSetDevice)
peer := new(ipcSetPeer) peer := new(ipcSetPeer)
deviceConfig := true deviceConfig := true
@ -155,12 +206,20 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
line := scanner.Text() line := scanner.Text()
if line == "" { if line == "" {
// Blank line means terminate operation. // Blank line means terminate operation.
err := ipcDev.mergeWithDevice(device)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err)
}
peer.handlePostConfig() peer.handlePostConfig()
return nil return nil
} }
key, value, ok := strings.Cut(line, "=") key, value, ok := strings.Cut(line, "=")
if !ok { if !ok {
return ipcErrorf(ipc.IpcErrorProtocol, "failed to parse line %q", line) return ipcErrorf(
ipc.IpcErrorProtocol,
"failed to parse line %q",
line,
)
} }
if key == "public_key" { if key == "public_key" {
@ -186,6 +245,10 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
return err return err
} }
} }
err = ipcDev.mergeWithDevice(device)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err)
}
peer.handlePostConfig() peer.handlePostConfig()
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
@ -235,11 +298,155 @@ func (device *Device) handleDeviceLine(key, value string) error {
case "replace_peers": case "replace_peers":
if value != "true" { if value != "true" {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set replace_peers, invalid value: %v", value) return ipcErrorf(
ipc.IpcErrorInvalid,
"failed to set replace_peers, invalid value: %v",
value,
)
} }
device.log.Verbosef("UAPI: Removing all peers") device.log.Verbosef("UAPI: Removing all peers")
device.RemoveAllPeers() device.RemoveAllPeers()
case "jc":
jc, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jc: %w", err)
}
if jc <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jc must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk count")
device.junk.count = jc
case "jmin":
jmin, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmin: %w", err)
}
if jmin <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmin must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk min")
device.junk.min = jmin
case "jmax":
jmax, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmax: %w", err)
}
if jmax <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmax must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk max")
device.junk.max = jmax
case "s1":
padding, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s1: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s1 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s1 padding")
device.paddings.init = padding
case "s2":
padding, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s2: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s2 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s2 padding")
device.paddings.response = padding
case "s3":
padding, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s3: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s3 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s3 padding")
device.paddings.cookie = padding
case "s4":
padding, err := strconv.Atoi(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s4: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s4 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s4 padding")
device.paddings.transport = padding
case "h1":
header, err := newMagicHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H1: %w", err)
}
device.headers.init = header
case "h2":
header, err := newMagicHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H2: %w", err)
}
device.headers.response = header
case "h3":
header, err := newMagicHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H3: %w", err)
}
device.headers.cookie = header
case "h4":
header, err := newMagicHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H4: %w", err)
}
device.headers.transport = header
case "i1":
chain, err := newObfChain(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I1: %w", err)
}
device.ipackets[0] = chain
case "i2":
chain, err := newObfChain(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I2: %w", err)
}
device.ipackets[1] = chain
case "i3":
chain, err := newObfChain(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I3: %w", err)
}
device.ipackets[2] = chain
case "i4":
chain, err := newObfChain(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I4: %w", err)
}
device.ipackets[3] = chain
case "i5":
chain, err := newObfChain(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I5: %w", err)
}
device.ipackets[4] = chain
default: default:
return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key)
} }
@ -271,7 +478,10 @@ func (peer *ipcSetPeer) handlePostConfig() {
} }
} }
func (device *Device) handlePublicKeyLine(peer *ipcSetPeer, value string) error { func (device *Device) handlePublicKeyLine(
peer *ipcSetPeer,
value string,
) error {
// Load/create the peer we are configuring. // Load/create the peer we are configuring.
var publicKey NoisePublicKey var publicKey NoisePublicKey
err := publicKey.FromHex(value) err := publicKey.FromHex(value)
@ -301,12 +511,19 @@ func (device *Device) handlePublicKeyLine(peer *ipcSetPeer, value string) error
return nil return nil
} }
func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error { func (device *Device) handlePeerLine(
peer *ipcSetPeer,
key, value string,
) error {
switch key { switch key {
case "update_only": case "update_only":
// allow disabling of creation // allow disabling of creation
if value != "true" { if value != "true" {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set update only, invalid value: %v", value) return ipcErrorf(
ipc.IpcErrorInvalid,
"failed to set update only, invalid value: %v",
value,
)
} }
if peer.created && !peer.dummy { if peer.created && !peer.dummy {
device.RemovePeer(peer.handshake.remoteStatic) device.RemovePeer(peer.handshake.remoteStatic)
@ -352,7 +569,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
secs, err := strconv.ParseUint(value, 10, 16) secs, err := strconv.ParseUint(value, 10, 16)
if err != nil { if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set persistent keepalive interval: %w", err) return ipcErrorf(
ipc.IpcErrorInvalid,
"failed to set persistent keepalive interval: %w",
err,
)
} }
old := peer.persistentKeepaliveInterval.Swap(uint32(secs)) old := peer.persistentKeepaliveInterval.Swap(uint32(secs))
@ -363,7 +584,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
case "replace_allowed_ips": case "replace_allowed_ips":
device.log.Verbosef("%v - UAPI: Removing all allowedips", peer.Peer) device.log.Verbosef("%v - UAPI: Removing all allowedips", peer.Peer)
if value != "true" { if value != "true" {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to replace allowedips, invalid value: %v", value) return ipcErrorf(
ipc.IpcErrorInvalid,
"failed to replace allowedips, invalid value: %v",
value,
)
} }
if peer.dummy { if peer.dummy {
return nil return nil
@ -371,7 +596,14 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
device.allowedips.RemoveByPeer(peer.Peer) device.allowedips.RemoveByPeer(peer.Peer)
case "allowed_ip": case "allowed_ip":
device.log.Verbosef("%v - UAPI: Adding allowedip", peer.Peer) add := true
verb := "Adding"
if len(value) > 0 && value[0] == '-' {
add = false
verb = "Removing"
value = value[1:]
}
device.log.Verbosef("%v - UAPI: %s allowedip", peer.Peer, verb)
prefix, err := netip.ParsePrefix(value) prefix, err := netip.ParsePrefix(value)
if err != nil { if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err) return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err)
@ -379,7 +611,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
if peer.dummy { if peer.dummy {
return nil return nil
} }
device.allowedips.Insert(prefix, peer.Peer) if add {
device.allowedips.Insert(prefix, peer.Peer)
} else {
device.allowedips.Remove(prefix, peer.Peer)
}
case "protocol_version": case "protocol_version":
if value != "1" { if value != "1" {
@ -431,7 +667,11 @@ func (device *Device) IpcHandle(socket net.Conn) {
return return
} }
if nextByte != '\n' { if nextByte != '\n' {
err = ipcErrorf(ipc.IpcErrorInvalid, "trailing character in UAPI get: %q", nextByte) err = ipcErrorf(
ipc.IpcErrorInvalid,
"trailing character in UAPI get: %q",
nextByte,
)
break break
} }
err = device.IpcGetOperation(buffered.Writer) err = device.IpcGetOperation(buffered.Writer)
@ -455,3 +695,49 @@ func (device *Device) IpcHandle(socket net.Conn) {
buffered.Flush() buffered.Flush()
} }
} }
type ipcSetDevice struct {
headers struct {
init *magicHeader
response *magicHeader
cookie *magicHeader
transport *magicHeader
}
}
func (d *ipcSetDevice) mergeWithDevice(device *Device) error {
if d.headers.init == nil {
d.headers.init = device.headers.init
}
if d.headers.response == nil {
d.headers.response = device.headers.response
}
if d.headers.cookie == nil {
d.headers.cookie = device.headers.cookie
}
if d.headers.transport == nil {
d.headers.transport = device.headers.transport
}
headers := []*magicHeader{d.headers.init, d.headers.response, d.headers.cookie, d.headers.transport}
for i := 0; i < len(headers); i++ {
for j := i + 1; j < len(headers); j++ {
left := headers[i]
right := headers[j]
if left.start <= right.end && right.start <= left.end {
return errors.New("headers must not overlap")
}
}
}
device.headers.init = d.headers.init
device.headers.response = d.headers.response
device.headers.cookie = d.headers.cookie
device.headers.transport = d.headers.transport
return nil
}

View file

@ -1,51 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package main
import (
"bytes"
"go/format"
"io/fs"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
)
func TestFormatting(t *testing.T) {
var wg sync.WaitGroup
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Errorf("unable to walk %s: %v", path, err)
return nil
}
if d.IsDir() || filepath.Ext(path) != ".go" {
return nil
}
wg.Add(1)
go func(path string) {
defer wg.Done()
src, err := os.ReadFile(path)
if err != nil {
t.Errorf("unable to read %s: %v", path, err)
return
}
if runtime.GOOS == "windows" {
src = bytes.ReplaceAll(src, []byte{'\r', '\n'}, []byte{'\n'})
}
formatted, err := format.Source(src)
if err != nil {
t.Errorf("unable to format %s: %v", path, err)
return
}
if !bytes.Equal(src, formatted) {
t.Errorf("unformatted code: %s", path)
}
}(path)
return nil
})
wg.Wait()
}

13
go.mod
View file

@ -1,16 +1,11 @@
module golang.zx2c4.com/wireguard module github.com/sagernet/wireguard-go
go 1.20 go 1.25
require ( require (
github.com/sagernet/sing v0.7.10
golang.org/x/crypto v0.13.0 golang.org/x/crypto v0.13.0
golang.org/x/net v0.15.0 golang.org/x/net v0.15.0
golang.org/x/sys v0.12.0 golang.org/x/sys v0.21.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
)
require (
github.com/google/btree v1.0.1 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
) )

12
go.sum
View file

@ -1,14 +1,10 @@
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=

View file

@ -1,674 +0,0 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Copyright 2015 Microsoft
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package namedpipe_test
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"net"
"os"
"sync"
"syscall"
"testing"
"time"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/ipc/namedpipe"
)
func randomPipePath() string {
guid, err := windows.GenerateGUID()
if err != nil {
panic(err)
}
return `\\.\PIPE\go-namedpipe-test-` + guid.String()
}
func TestPingPong(t *testing.T) {
const (
ping = 42
pong = 24
)
pipePath := randomPipePath()
listener, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatalf("unable to listen on pipe: %v", err)
}
defer listener.Close()
go func() {
incoming, err := listener.Accept()
if err != nil {
t.Fatalf("unable to accept pipe connection: %v", err)
}
defer incoming.Close()
var data [1]byte
_, err = incoming.Read(data[:])
if err != nil {
t.Fatalf("unable to read ping from pipe: %v", err)
}
if data[0] != ping {
t.Fatalf("expected ping, got %d", data[0])
}
data[0] = pong
_, err = incoming.Write(data[:])
if err != nil {
t.Fatalf("unable to write pong to pipe: %v", err)
}
}()
client, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatalf("unable to dial pipe: %v", err)
}
defer client.Close()
client.SetDeadline(time.Now().Add(time.Second * 5))
var data [1]byte
data[0] = ping
_, err = client.Write(data[:])
if err != nil {
t.Fatalf("unable to write ping to pipe: %v", err)
}
_, err = client.Read(data[:])
if err != nil {
t.Fatalf("unable to read pong from pipe: %v", err)
}
if data[0] != pong {
t.Fatalf("expected pong, got %d", data[0])
}
}
func TestDialUnknownFailsImmediately(t *testing.T) {
_, err := namedpipe.DialTimeout(randomPipePath(), time.Duration(0))
if !errors.Is(err, syscall.ENOENT) {
t.Fatalf("expected ENOENT got %v", err)
}
}
func TestDialListenerTimesOut(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
pipe, err := namedpipe.DialTimeout(pipePath, 10*time.Millisecond)
if err == nil {
pipe.Close()
}
if err != os.ErrDeadlineExceeded {
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err)
}
}
func TestDialContextListenerTimesOut(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
d := 10 * time.Millisecond
ctx, _ := context.WithTimeout(context.Background(), d)
pipe, err := namedpipe.DialContext(ctx, pipePath)
if err == nil {
pipe.Close()
}
if err != context.DeadlineExceeded {
t.Fatalf("expected context.DeadlineExceeded, got %v", err)
}
}
func TestDialListenerGetsCancelled(t *testing.T) {
pipePath := randomPipePath()
ctx, cancel := context.WithCancel(context.Background())
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
ch := make(chan error)
go func(ctx context.Context, ch chan error) {
_, err := namedpipe.DialContext(ctx, pipePath)
ch <- err
}(ctx, ch)
time.Sleep(time.Millisecond * 30)
cancel()
err = <-ch
if err != context.Canceled {
t.Fatalf("expected context.Canceled, got %v", err)
}
}
func TestDialAccessDeniedWithRestrictedSD(t *testing.T) {
if windows.NewLazySystemDLL("ntdll.dll").NewProc("wine_get_version").Find() == nil {
t.Skip("dacls on named pipes are broken on wine")
}
pipePath := randomPipePath()
sd, _ := windows.SecurityDescriptorFromString("D:")
l, err := (&namedpipe.ListenConfig{
SecurityDescriptor: sd,
}).Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
pipe, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err == nil {
pipe.Close()
}
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
t.Fatalf("expected ERROR_ACCESS_DENIED, got %v", err)
}
}
func getConnection(cfg *namedpipe.ListenConfig) (client, server net.Conn, err error) {
pipePath := randomPipePath()
if cfg == nil {
cfg = &namedpipe.ListenConfig{}
}
l, err := cfg.Listen(pipePath)
if err != nil {
return
}
defer l.Close()
type response struct {
c net.Conn
err error
}
ch := make(chan response)
go func() {
c, err := l.Accept()
ch <- response{c, err}
}()
c, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
return
}
r := <-ch
if err = r.err; err != nil {
c.Close()
return
}
client = c
server = r.c
return
}
func TestReadTimeout(t *testing.T) {
c, s, err := getConnection(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
defer s.Close()
c.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
buf := make([]byte, 10)
_, err = c.Read(buf)
if err != os.ErrDeadlineExceeded {
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err)
}
}
func server(l net.Listener, ch chan int) {
c, err := l.Accept()
if err != nil {
panic(err)
}
rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
s, err := rw.ReadString('\n')
if err != nil {
panic(err)
}
_, err = rw.WriteString("got " + s)
if err != nil {
panic(err)
}
err = rw.Flush()
if err != nil {
panic(err)
}
c.Close()
ch <- 1
}
func TestFullListenDialReadWrite(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
ch := make(chan int)
go server(l, ch)
c, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatal(err)
}
defer c.Close()
rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
_, err = rw.WriteString("hello world\n")
if err != nil {
t.Fatal(err)
}
err = rw.Flush()
if err != nil {
t.Fatal(err)
}
s, err := rw.ReadString('\n')
if err != nil {
t.Fatal(err)
}
ms := "got hello world\n"
if s != ms {
t.Errorf("expected '%s', got '%s'", ms, s)
}
<-ch
}
func TestCloseAbortsListen(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
ch := make(chan error)
go func() {
_, err := l.Accept()
ch <- err
}()
time.Sleep(30 * time.Millisecond)
l.Close()
err = <-ch
if err != net.ErrClosed {
t.Fatalf("expected net.ErrClosed, got %v", err)
}
}
func ensureEOFOnClose(t *testing.T, r io.Reader, w io.Closer) {
b := make([]byte, 10)
w.Close()
n, err := r.Read(b)
if n > 0 {
t.Errorf("unexpected byte count %d", n)
}
if err != io.EOF {
t.Errorf("expected EOF: %v", err)
}
}
func TestCloseClientEOFServer(t *testing.T) {
c, s, err := getConnection(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
defer s.Close()
ensureEOFOnClose(t, c, s)
}
func TestCloseServerEOFClient(t *testing.T) {
c, s, err := getConnection(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
defer s.Close()
ensureEOFOnClose(t, s, c)
}
func TestCloseWriteEOF(t *testing.T) {
cfg := &namedpipe.ListenConfig{
MessageMode: true,
}
c, s, err := getConnection(cfg)
if err != nil {
t.Fatal(err)
}
defer c.Close()
defer s.Close()
type closeWriter interface {
CloseWrite() error
}
err = c.(closeWriter).CloseWrite()
if err != nil {
t.Fatal(err)
}
b := make([]byte, 10)
_, err = s.Read(b)
if err != io.EOF {
t.Fatal(err)
}
}
func TestAcceptAfterCloseFails(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
l.Close()
_, err = l.Accept()
if err != net.ErrClosed {
t.Fatalf("expected net.ErrClosed, got %v", err)
}
}
func TestDialTimesOutByDefault(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
pipe, err := namedpipe.DialTimeout(pipePath, time.Duration(0)) // Should timeout after 2 seconds.
if err == nil {
pipe.Close()
}
if err != os.ErrDeadlineExceeded {
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err)
}
}
func TestTimeoutPendingRead(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
serverDone := make(chan struct{})
go func() {
s, err := l.Accept()
if err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Second)
s.Close()
close(serverDone)
}()
client, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatal(err)
}
defer client.Close()
clientErr := make(chan error)
go func() {
buf := make([]byte, 10)
_, err = client.Read(buf)
clientErr <- err
}()
time.Sleep(100 * time.Millisecond) // make *sure* the pipe is reading before we set the deadline
client.SetReadDeadline(time.Unix(1, 0))
select {
case err = <-clientErr:
if err != os.ErrDeadlineExceeded {
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err)
}
case <-time.After(100 * time.Millisecond):
t.Fatalf("timed out while waiting for read to cancel")
<-clientErr
}
<-serverDone
}
func TestTimeoutPendingWrite(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
serverDone := make(chan struct{})
go func() {
s, err := l.Accept()
if err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Second)
s.Close()
close(serverDone)
}()
client, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatal(err)
}
defer client.Close()
clientErr := make(chan error)
go func() {
_, err = client.Write([]byte("this should timeout"))
clientErr <- err
}()
time.Sleep(100 * time.Millisecond) // make *sure* the pipe is writing before we set the deadline
client.SetWriteDeadline(time.Unix(1, 0))
select {
case err = <-clientErr:
if err != os.ErrDeadlineExceeded {
t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err)
}
case <-time.After(100 * time.Millisecond):
t.Fatalf("timed out while waiting for write to cancel")
<-clientErr
}
<-serverDone
}
type CloseWriter interface {
CloseWrite() error
}
func TestEchoWithMessaging(t *testing.T) {
pipePath := randomPipePath()
l, err := (&namedpipe.ListenConfig{
MessageMode: true, // Use message mode so that CloseWrite() is supported
InputBufferSize: 65536, // Use 64KB buffers to improve performance
OutputBufferSize: 65536,
}).Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
listenerDone := make(chan bool)
clientDone := make(chan bool)
go func() {
// server echo
conn, err := l.Accept()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
time.Sleep(500 * time.Millisecond) // make *sure* we don't begin to read before eof signal is sent
_, err = io.Copy(conn, conn)
if err != nil {
t.Fatal(err)
}
conn.(CloseWriter).CloseWrite()
close(listenerDone)
}()
client, err := namedpipe.DialTimeout(pipePath, time.Second)
if err != nil {
t.Fatal(err)
}
defer client.Close()
go func() {
// client read back
bytes := make([]byte, 2)
n, e := client.Read(bytes)
if e != nil {
t.Fatal(e)
}
if n != 2 || bytes[0] != 0 || bytes[1] != 1 {
t.Fatalf("expected 2 bytes, got %v", n)
}
close(clientDone)
}()
payload := make([]byte, 2)
payload[0] = 0
payload[1] = 1
n, err := client.Write(payload)
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("expected 2 bytes, got %v", n)
}
client.(CloseWriter).CloseWrite()
<-listenerDone
<-clientDone
}
func TestConnectRace(t *testing.T) {
pipePath := randomPipePath()
l, err := namedpipe.Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
go func() {
for {
s, err := l.Accept()
if err == net.ErrClosed {
return
}
if err != nil {
t.Fatal(err)
}
s.Close()
}
}()
for i := 0; i < 1000; i++ {
c, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatal(err)
}
c.Close()
}
}
func TestMessageReadMode(t *testing.T) {
if maj, _, _ := windows.RtlGetNtVersionNumbers(); maj <= 8 {
t.Skipf("Skipping on Windows %d", maj)
}
var wg sync.WaitGroup
defer wg.Wait()
pipePath := randomPipePath()
l, err := (&namedpipe.ListenConfig{MessageMode: true}).Listen(pipePath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
msg := ([]byte)("hello world")
wg.Add(1)
go func() {
defer wg.Done()
s, err := l.Accept()
if err != nil {
t.Fatal(err)
}
_, err = s.Write(msg)
if err != nil {
t.Fatal(err)
}
s.Close()
}()
c, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err != nil {
t.Fatal(err)
}
defer c.Close()
mode := uint32(windows.PIPE_READMODE_MESSAGE)
err = windows.SetNamedPipeHandleState(c.(interface{ Handle() windows.Handle }).Handle(), &mode, nil, nil)
if err != nil {
t.Fatal(err)
}
ch := make([]byte, 1)
var vmsg []byte
for {
n, err := c.Read(ch)
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("expected 1, got %d", n)
}
vmsg = append(vmsg, ch[0])
}
if !bytes.Equal(msg, vmsg) {
t.Fatalf("expected %s, got %s", msg, vmsg)
}
}
func TestListenConnectRace(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long race test")
}
pipePath := randomPipePath()
for i := 0; i < 50 && !t.Failed(); i++ {
var wg sync.WaitGroup
wg.Add(1)
go func() {
c, err := namedpipe.DialTimeout(pipePath, time.Duration(0))
if err == nil {
c.Close()
}
wg.Done()
}()
s, err := namedpipe.Listen(pipePath)
if err != nil {
t.Error(i, err)
} else {
s.Close()
}
wg.Wait()
}
}

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc
@ -9,8 +9,8 @@ import (
"net" "net"
"os" "os"
"github.com/sagernet/wireguard-go/rwcancel"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/rwcancel"
) )
type UAPIListener struct { type UAPIListener struct {
@ -84,7 +84,6 @@ func UAPIListen(name string, file *os.File) (net.Listener, error) {
unix.IN_DELETE| unix.IN_DELETE|
unix.IN_DELETE_SELF, unix.IN_DELETE_SELF,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc
@ -8,8 +8,8 @@ package ipc
import ( import (
"net" "net"
"github.com/sagernet/wireguard-go/ipc/namedpipe"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/ipc/namedpipe"
) )
// TODO: replace these with actual standard windows error numbers from the win package // TODO: replace these with actual standard windows error numbers from the win package

268
main.go
View file

@ -1,268 +0,0 @@
//go:build !windows
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package main
import (
"fmt"
"os"
"os/signal"
"runtime"
"strconv"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
)
const (
ExitSetupSuccess = 0
ExitSetupFailed = 1
)
const (
ENV_WG_TUN_FD = "WG_TUN_FD"
ENV_WG_UAPI_FD = "WG_UAPI_FD"
ENV_WG_PROCESS_FOREGROUND = "WG_PROCESS_FOREGROUND"
)
func printUsage() {
fmt.Printf("Usage: %s [-f/--foreground] INTERFACE-NAME\n", os.Args[0])
}
func warning() {
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
if os.Getenv(ENV_WG_PROCESS_FOREGROUND) == "1" {
return
}
default:
return
}
fmt.Fprintln(os.Stderr, "┌──────────────────────────────────────────────────────┐")
fmt.Fprintln(os.Stderr, "│ │")
fmt.Fprintln(os.Stderr, "│ Running wireguard-go is not required because this │")
fmt.Fprintln(os.Stderr, "│ kernel has first class support for WireGuard. For │")
fmt.Fprintln(os.Stderr, "│ information on installing the kernel module, │")
fmt.Fprintln(os.Stderr, "│ please visit: │")
fmt.Fprintln(os.Stderr, "│ https://www.wireguard.com/install/ │")
fmt.Fprintln(os.Stderr, "│ │")
fmt.Fprintln(os.Stderr, "└──────────────────────────────────────────────────────┘")
}
func main() {
if len(os.Args) == 2 && os.Args[1] == "--version" {
fmt.Printf("wireguard-go v%s\n\nUserspace WireGuard daemon for %s-%s.\nInformation available at https://www.wireguard.com.\nCopyright (C) Jason A. Donenfeld <Jason@zx2c4.com>.\n", Version, runtime.GOOS, runtime.GOARCH)
return
}
warning()
var foreground bool
var interfaceName string
if len(os.Args) < 2 || len(os.Args) > 3 {
printUsage()
return
}
switch os.Args[1] {
case "-f", "--foreground":
foreground = true
if len(os.Args) != 3 {
printUsage()
return
}
interfaceName = os.Args[2]
default:
foreground = false
if len(os.Args) != 2 {
printUsage()
return
}
interfaceName = os.Args[1]
}
if !foreground {
foreground = os.Getenv(ENV_WG_PROCESS_FOREGROUND) == "1"
}
// get log level (default: info)
logLevel := func() int {
switch os.Getenv("LOG_LEVEL") {
case "verbose", "debug":
return device.LogLevelVerbose
case "error":
return device.LogLevelError
case "silent":
return device.LogLevelSilent
}
return device.LogLevelError
}()
// open TUN device (or use supplied fd)
tdev, err := func() (tun.Device, error) {
tunFdStr := os.Getenv(ENV_WG_TUN_FD)
if tunFdStr == "" {
return tun.CreateTUN(interfaceName, device.DefaultMTU)
}
// construct tun device from supplied fd
fd, err := strconv.ParseUint(tunFdStr, 10, 32)
if err != nil {
return nil, err
}
err = unix.SetNonblock(int(fd), true)
if err != nil {
return nil, err
}
file := os.NewFile(uintptr(fd), "")
return tun.CreateTUNFromFile(file, device.DefaultMTU)
}()
if err == nil {
realInterfaceName, err2 := tdev.Name()
if err2 == nil {
interfaceName = realInterfaceName
}
}
logger := device.NewLogger(
logLevel,
fmt.Sprintf("(%s) ", interfaceName),
)
logger.Verbosef("Starting wireguard-go version %s", Version)
if err != nil {
logger.Errorf("Failed to create TUN device: %v", err)
os.Exit(ExitSetupFailed)
}
// open UAPI file (or use supplied fd)
fileUAPI, err := func() (*os.File, error) {
uapiFdStr := os.Getenv(ENV_WG_UAPI_FD)
if uapiFdStr == "" {
return ipc.UAPIOpen(interfaceName)
}
// use supplied fd
fd, err := strconv.ParseUint(uapiFdStr, 10, 32)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), ""), nil
}()
if err != nil {
logger.Errorf("UAPI listen error: %v", err)
os.Exit(ExitSetupFailed)
return
}
// daemonize the process
if !foreground {
env := os.Environ()
env = append(env, fmt.Sprintf("%s=3", ENV_WG_TUN_FD))
env = append(env, fmt.Sprintf("%s=4", ENV_WG_UAPI_FD))
env = append(env, fmt.Sprintf("%s=1", ENV_WG_PROCESS_FOREGROUND))
files := [3]*os.File{}
if os.Getenv("LOG_LEVEL") != "" && logLevel != device.LogLevelSilent {
files[0], _ = os.Open(os.DevNull)
files[1] = os.Stdout
files[2] = os.Stderr
} else {
files[0], _ = os.Open(os.DevNull)
files[1], _ = os.Open(os.DevNull)
files[2], _ = os.Open(os.DevNull)
}
attr := &os.ProcAttr{
Files: []*os.File{
files[0], // stdin
files[1], // stdout
files[2], // stderr
tdev.File(),
fileUAPI,
},
Dir: ".",
Env: env,
}
path, err := os.Executable()
if err != nil {
logger.Errorf("Failed to determine executable: %v", err)
os.Exit(ExitSetupFailed)
}
process, err := os.StartProcess(
path,
os.Args,
attr,
)
if err != nil {
logger.Errorf("Failed to daemonize: %v", err)
os.Exit(ExitSetupFailed)
}
process.Release()
return
}
device := device.NewDevice(tdev, conn.NewDefaultBind(), logger)
logger.Verbosef("Device started")
errs := make(chan error)
term := make(chan os.Signal, 1)
uapi, err := ipc.UAPIListen(interfaceName, fileUAPI)
if err != nil {
logger.Errorf("Failed to listen on uapi socket: %v", err)
os.Exit(ExitSetupFailed)
}
go func() {
for {
conn, err := uapi.Accept()
if err != nil {
errs <- err
return
}
go device.IpcHandle(conn)
}
}()
logger.Verbosef("UAPI listener started")
// wait for program to terminate
signal.Notify(term, unix.SIGTERM)
signal.Notify(term, os.Interrupt)
select {
case <-term:
case <-errs:
case <-device.Wait():
}
// clean up
uapi.Close()
device.Close()
logger.Verbosef("Shutting down")
}

View file

@ -1,99 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package main
import (
"fmt"
"os"
"os/signal"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
)
const (
ExitSetupSuccess = 0
ExitSetupFailed = 1
)
func main() {
if len(os.Args) != 2 {
os.Exit(ExitSetupFailed)
}
interfaceName := os.Args[1]
fmt.Fprintln(os.Stderr, "Warning: this is a test program for Windows, mainly used for debugging this Go package. For a real WireGuard for Windows client, the repo you want is <https://git.zx2c4.com/wireguard-windows/>, which includes this code as a module.")
logger := device.NewLogger(
device.LogLevelVerbose,
fmt.Sprintf("(%s) ", interfaceName),
)
logger.Verbosef("Starting wireguard-go version %s", Version)
tun, err := tun.CreateTUN(interfaceName, 0)
if err == nil {
realInterfaceName, err2 := tun.Name()
if err2 == nil {
interfaceName = realInterfaceName
}
} else {
logger.Errorf("Failed to create TUN device: %v", err)
os.Exit(ExitSetupFailed)
}
device := device.NewDevice(tun, conn.NewDefaultBind(), logger)
err = device.Up()
if err != nil {
logger.Errorf("Failed to bring up device: %v", err)
os.Exit(ExitSetupFailed)
}
logger.Verbosef("Device started")
uapi, err := ipc.UAPIListen(interfaceName)
if err != nil {
logger.Errorf("Failed to listen on uapi socket: %v", err)
os.Exit(ExitSetupFailed)
}
errs := make(chan error)
term := make(chan os.Signal, 1)
go func() {
for {
conn, err := uapi.Accept()
if err != nil {
errs <- err
return
}
go device.IpcHandle(conn)
}
}()
logger.Verbosef("UAPI listener started")
// wait for program to terminate
signal.Notify(term, os.Interrupt)
signal.Notify(term, os.Kill)
signal.Notify(term, windows.SIGTERM)
select {
case <-term:
case <-errs:
case <-device.Wait():
}
// clean up
uapi.Close()
device.Close()
logger.Verbosef("Shutting down")
}

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ratelimiter package ratelimiter

View file

@ -1,119 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
*/
package ratelimiter
import (
"net/netip"
"testing"
"time"
)
type result struct {
allowed bool
text string
wait time.Duration
}
func TestRatelimiter(t *testing.T) {
var rate Ratelimiter
var expectedResults []result
nano := func(nano int64) time.Duration {
return time.Nanosecond * time.Duration(nano)
}
add := func(res result) {
expectedResults = append(
expectedResults,
res,
)
}
for i := 0; i < packetsBurstable; i++ {
add(result{
allowed: true,
text: "initial burst",
})
}
add(result{
allowed: false,
text: "after burst",
})
add(result{
allowed: true,
wait: nano(time.Second.Nanoseconds() / packetsPerSecond),
text: "filling tokens for single packet",
})
add(result{
allowed: false,
text: "not having refilled enough",
})
add(result{
allowed: true,
wait: 2 * (nano(time.Second.Nanoseconds() / packetsPerSecond)),
text: "filling tokens for two packet burst",
})
add(result{
allowed: true,
text: "second packet in 2 packet burst",
})
add(result{
allowed: false,
text: "packet following 2 packet burst",
})
ips := []netip.Addr{
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("192.168.1.1"),
netip.MustParseAddr("172.167.2.3"),
netip.MustParseAddr("97.231.252.215"),
netip.MustParseAddr("248.97.91.167"),
netip.MustParseAddr("188.208.233.47"),
netip.MustParseAddr("104.2.183.179"),
netip.MustParseAddr("72.129.46.120"),
netip.MustParseAddr("2001:0db8:0a0b:12f0:0000:0000:0000:0001"),
netip.MustParseAddr("f5c2:818f:c052:655a:9860:b136:6894:25f0"),
netip.MustParseAddr("b2d7:15ab:48a7:b07c:a541:f144:a9fe:54fc"),
netip.MustParseAddr("a47b:786e:1671:a22b:d6f9:4ab0:abc7:c918"),
netip.MustParseAddr("ea1e:d155:7f7a:98fb:2bf5:9483:80f6:5445"),
netip.MustParseAddr("3f0e:54a2:f5b4:cd19:a21d:58e1:3746:84c4"),
}
now := time.Now()
rate.timeNow = func() time.Time {
return now
}
defer func() {
// Lock to avoid data race with cleanup goroutine from Init.
rate.mu.Lock()
defer rate.mu.Unlock()
rate.timeNow = time.Now
}()
timeSleep := func(d time.Duration) {
now = now.Add(d + 1)
rate.cleanup()
}
rate.Init()
defer rate.Close()
for i, res := range expectedResults {
timeSleep(res.wait)
for _, ip := range ips {
allowed := rate.Allow(ip)
if allowed != res.allowed {
t.Fatalf("%d: %s: rate.Allow(%q)=%v, want %v", i, res.text, ip, allowed, res.allowed)
}
}
}
}

9
reformat.sh Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e -o pipefail
GO_FILES=$(find . -name "*.go" | grep -v .git)
gofumpt -l -w $GO_FILES
gofmt -l -w $GO_FILES
gci write $GO_FILES

23
remove-unused.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -e -o pipefail
function remove_unused() {
git rm -rf --ignore-unmatch \
.github \
tests \
*_test.go \
**/*_test.go \
conn/bindtest \
tun/netstack \
tun/tuntest \
tun/testdata \
main*.go \
*.md
}
remove_unused
remove_unused
go mod tidy
git commit -a -m "Remove unused"

Some files were not shown because too many files have changed in this diff Show more