From bd7e9d35d114da345d0aa845e61686df8cfc5e1e Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 28 Mar 2023 13:40:31 -0700 Subject: [PATCH 001/173] all: rename module (#7) Signed-off-by: Jordan Whited --- conn/bind_windows.go | 2 +- conn/bindtest/bindtest.go | 2 +- device/bind_test.go | 2 +- device/device.go | 8 ++++---- device/device_test.go | 8 ++++---- device/keypair.go | 2 +- device/noise-protocol.go | 2 +- device/noise_test.go | 4 ++-- device/peer.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/receive.go | 2 +- device/send.go | 2 +- device/sticky_default.go | 4 ++-- device/sticky_linux.go | 4 ++-- device/tun.go | 2 +- device/uapi.go | 2 +- go.mod | 2 +- ipc/namedpipe/namedpipe_test.go | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_windows.go | 2 +- main.go | 8 ++++---- main_windows.go | 8 ++++---- tun/netstack/examples/http_client.go | 6 +++--- tun/netstack/examples/http_server.go | 6 +++--- tun/netstack/examples/ping_client.go | 6 +++--- tun/netstack/tun.go | 2 +- tun/tcp_offload_linux.go | 2 +- tun/tcp_offload_linux_test.go | 2 +- tun/tun_linux.go | 4 ++-- tun/tuntest/tuntest.go | 2 +- 31 files changed, 53 insertions(+), 53 deletions(-) diff --git a/conn/bind_windows.go b/conn/bind_windows.go index d5095e0..9638b30 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -17,7 +17,7 @@ import ( "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/conn/winrio" + "github.com/tailscale/wireguard-go/conn/winrio" ) const ( diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 74e7add..836d983 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -12,7 +12,7 @@ import ( "net/netip" "os" - "golang.zx2c4.com/wireguard/conn" + "github.com/tailscale/wireguard-go/conn" ) type ChannelBind struct { diff --git a/device/bind_test.go b/device/bind_test.go index 302a521..d64ca09 100644 --- a/device/bind_test.go +++ b/device/bind_test.go @@ -8,7 +8,7 @@ package device import ( "errors" - "golang.zx2c4.com/wireguard/conn" + "github.com/tailscale/wireguard-go/conn" ) type DummyDatagram struct { diff --git a/device/device.go b/device/device.go index 1af9fe0..7482d9b 100644 --- a/device/device.go +++ b/device/device.go @@ -11,10 +11,10 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/ratelimiter" - "golang.zx2c4.com/wireguard/rwcancel" - "golang.zx2c4.com/wireguard/tun" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/ratelimiter" + "github.com/tailscale/wireguard-go/rwcancel" + "github.com/tailscale/wireguard-go/tun" ) type Device struct { diff --git a/device/device_test.go b/device/device_test.go index fff172b..4088b9f 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -20,10 +20,10 @@ import ( "testing" "time" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/conn/bindtest" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/tuntest" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/conn/bindtest" + "github.com/tailscale/wireguard-go/tun" + "github.com/tailscale/wireguard-go/tun/tuntest" ) // uapiCfg returns a string that contains cfg formatted use with IpcSet. diff --git a/device/keypair.go b/device/keypair.go index e3540d7..2689ee2 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -11,7 +11,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/replay" + "github.com/tailscale/wireguard-go/replay" ) /* Due to limitations in Go and /x/crypto there is currently diff --git a/device/noise-protocol.go b/device/noise-protocol.go index e8f6145..9f2ba50 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - "golang.zx2c4.com/wireguard/tai64n" + "github.com/tailscale/wireguard-go/tai64n" ) type handshakeState int diff --git a/device/noise_test.go b/device/noise_test.go index 2dd5324..7d6af1d 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -10,8 +10,8 @@ import ( "encoding/binary" "testing" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/tun/tuntest" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/tun/tuntest" ) func TestCurveWrappers(t *testing.T) { diff --git a/device/peer.go b/device/peer.go index 0ac4896..c7163ac 100644 --- a/device/peer.go +++ b/device/peer.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" + "github.com/tailscale/wireguard-go/conn" ) type Peer struct { diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index 3d80ead..bab9625 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -5,7 +5,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/tailscale/wireguard-go/conn" /* Reduce memory consumption for Android */ diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index ea763d0..9749cb7 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -7,7 +7,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/tailscale/wireguard-go/conn" const ( QueueStagedSize = conn.IdealBatchSize diff --git a/device/receive.go b/device/receive.go index e24d29f..c0cf747 100644 --- a/device/receive.go +++ b/device/receive.go @@ -13,10 +13,10 @@ import ( "sync" "time" + "github.com/tailscale/wireguard-go/conn" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/conn" ) type QueueHandshakeElement struct { diff --git a/device/send.go b/device/send.go index d22bf26..a95a46f 100644 --- a/device/send.go +++ b/device/send.go @@ -14,10 +14,10 @@ import ( "sync" "time" + "github.com/tailscale/wireguard-go/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/tun" ) /* Outbound flow diff --git a/device/sticky_default.go b/device/sticky_default.go index 1038256..732f84c 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -3,8 +3,8 @@ package device import ( - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/sticky_linux.go b/device/sticky_linux.go index f9230f8..7a519c1 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -20,8 +20,8 @@ import ( "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/tun.go b/device/tun.go index 2a2ace9..960ecca 100644 --- a/device/tun.go +++ b/device/tun.go @@ -8,7 +8,7 @@ package device import ( "fmt" - "golang.zx2c4.com/wireguard/tun" + "github.com/tailscale/wireguard-go/tun" ) const DefaultMTU = 1420 diff --git a/device/uapi.go b/device/uapi.go index 617dcd3..2a91a93 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "golang.zx2c4.com/wireguard/ipc" + "github.com/tailscale/wireguard-go/ipc" ) type IPCError struct { diff --git a/go.mod b/go.mod index c04e1bb..0d60c9a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module golang.zx2c4.com/wireguard +module github.com/tailscale/wireguard-go go 1.20 diff --git a/ipc/namedpipe/namedpipe_test.go b/ipc/namedpipe/namedpipe_test.go index 998453b..de7d0f6 100644 --- a/ipc/namedpipe/namedpipe_test.go +++ b/ipc/namedpipe/namedpipe_test.go @@ -20,8 +20,8 @@ import ( "testing" "time" + "github.com/tailscale/wireguard-go/ipc/namedpipe" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/ipc/namedpipe" ) func randomPipePath() string { diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 1562a18..bfdf1bf 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -9,8 +9,8 @@ import ( "net" "os" + "github.com/tailscale/wireguard-go/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/rwcancel" ) type UAPIListener struct { diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index aa023c9..bc30ae0 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -8,8 +8,8 @@ package ipc import ( "net" + "github.com/tailscale/wireguard-go/ipc/namedpipe" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/ipc/namedpipe" ) // TODO: replace these with actual standard windows error numbers from the win package diff --git a/main.go b/main.go index e016116..55000e9 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "runtime" "strconv" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/ipc" + "github.com/tailscale/wireguard-go/tun" "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 ( diff --git a/main_windows.go b/main_windows.go index a4dc46f..689a9a7 100644 --- a/main_windows.go +++ b/main_windows.go @@ -12,11 +12,11 @@ import ( "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/ipc" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/ipc" - "golang.zx2c4.com/wireguard/tun" + "github.com/tailscale/wireguard-go/tun" ) const ( diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go index ccd32ed..81f4d31 100644 --- a/tun/netstack/examples/http_client.go +++ b/tun/netstack/examples/http_client.go @@ -13,9 +13,9 @@ import ( "net/http" "net/netip" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go index f5b7a8f..30f4544 100644 --- a/tun/netstack/examples/http_server.go +++ b/tun/netstack/examples/http_server.go @@ -14,9 +14,9 @@ import ( "net/http" "net/netip" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go index 2eef0fb..fc991b1 100644 --- a/tun/netstack/examples/ping_client.go +++ b/tun/netstack/examples/ping_client.go @@ -17,9 +17,9 @@ import ( "golang.org/x/net/icmp" "golang.org/x/net/ipv4" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun/netstack" ) func main() { diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 596cfcd..2402842 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -22,7 +22,7 @@ import ( "syscall" "time" - "golang.zx2c4.com/wireguard/tun" + "github.com/tailscale/wireguard-go/tun" "golang.org/x/net/dns/dnsmessage" "gvisor.dev/gvisor/pkg/bufferv2" diff --git a/tun/tcp_offload_linux.go b/tun/tcp_offload_linux.go index 39a7180..d64010d 100644 --- a/tun/tcp_offload_linux.go +++ b/tun/tcp_offload_linux.go @@ -12,8 +12,8 @@ import ( "io" "unsafe" + "github.com/tailscale/wireguard-go/conn" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" ) const tcpFlagsOffset = 13 diff --git a/tun/tcp_offload_linux_test.go b/tun/tcp_offload_linux_test.go index 9160e18..e828642 100644 --- a/tun/tcp_offload_linux_test.go +++ b/tun/tcp_offload_linux_test.go @@ -9,8 +9,8 @@ import ( "net/netip" "testing" + "github.com/tailscale/wireguard-go/conn" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" ) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 12cd49f..eb5051e 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -17,9 +17,9 @@ import ( "time" "unsafe" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" ) const ( diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go index d07e860..e7507c2 100644 --- a/tun/tuntest/tuntest.go +++ b/tun/tuntest/tuntest.go @@ -11,7 +11,7 @@ import ( "net/netip" "os" - "golang.zx2c4.com/wireguard/tun" + "github.com/tailscale/wireguard-go/tun" ) func Ping(dst, src netip.Addr) []byte { From e26adb828d950319d0d0f17178035597e70904a7 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Wed, 27 Sep 2023 16:15:09 -0700 Subject: [PATCH 002/173] go.mod,tun/netstack: bump gvisor Signed-off-by: James Tucker --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- tun/netstack/tun.go | 14 +++++++------- tun/tcp_offload_linux_test.go | 8 ++++---- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 0d60c9a..9c9b02a 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/tailscale/wireguard-go go 1.20 require ( - golang.org/x/crypto v0.6.0 - golang.org/x/net v0.7.0 - golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89 + golang.org/x/crypto v0.13.0 + golang.org/x/net v0.15.0 + golang.org/x/sys v0.12.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 ) require ( github.com/google/btree v1.0.1 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect ) diff --git a/go.sum b/go.sum index cfeaee6..6bcecea 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89 h1:260HNjMTPDya+jq5AM1zZLgG9pv9GASPAGiEEJUbRg4= -golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= -gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 2402842..d8e70bb 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -25,7 +25,7 @@ import ( "github.com/tailscale/wireguard-go/tun" "golang.org/x/net/dns/dnsmessage" - "gvisor.dev/gvisor/pkg/bufferv2" + "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/header" @@ -43,7 +43,7 @@ type netTun struct { ep *channel.Endpoint stack *stack.Stack events chan tun.Event - incomingPacket chan *bufferv2.View + incomingPacket chan *buffer.View mtu int dnsServers []netip.Addr hasV4, hasV6 bool @@ -61,7 +61,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, ep: channel.New(1024, uint32(mtu), ""), stack: stack.New(opts), events: make(chan tun.Event, 10), - incomingPacket: make(chan *bufferv2.View), + incomingPacket: make(chan *buffer.View), dnsServers: dnsServers, mtu: mtu, } @@ -84,7 +84,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, } protoAddr := tcpip.ProtocolAddress{ Protocol: protoNumber, - AddressWithPrefix: tcpip.Address(ip.AsSlice()).WithPrefix(), + AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), } tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) if tcpipErr != nil { @@ -140,7 +140,7 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { continue } - pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: bufferv2.MakeWithData(packet)}) + pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) switch packet[0] >> 4 { case 4: tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) @@ -198,7 +198,7 @@ func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.Networ } return tcpip.FullAddress{ NIC: 1, - Addr: tcpip.Address(endpoint.Addr().AsSlice()), + Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), Port: endpoint.Port(), }, protoNumber } @@ -453,7 +453,7 @@ func (pc *PingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { return 0, nil, fmt.Errorf("ping read: %s", tcpipErr) } - remoteAddr, _ := netip.AddrFromSlice([]byte(res.RemoteAddr.Addr)) + remoteAddr, _ := netip.AddrFromSlice(res.RemoteAddr.Addr.AsSlice()) return res.Count, &PingAddr{remoteAddr}, nil } diff --git a/tun/tcp_offload_linux_test.go b/tun/tcp_offload_linux_test.go index e828642..41fba70 100644 --- a/tun/tcp_offload_linux_test.go +++ b/tun/tcp_offload_linux_test.go @@ -35,8 +35,8 @@ func tcp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header. srcAs4 := srcIPPort.Addr().As4() dstAs4 := dstIPPort.Addr().As4() ipFields := &header.IPv4Fields{ - SrcAddr: tcpip.Address(srcAs4[:]), - DstAddr: tcpip.Address(dstAs4[:]), + SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), + DstAddr: tcpip.AddrFromSlice(dstAs4[:]), Protocol: unix.IPPROTO_TCP, TTL: 64, TotalLength: uint16(totalLen), @@ -72,8 +72,8 @@ func tcp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header. srcAs16 := srcIPPort.Addr().As16() dstAs16 := dstIPPort.Addr().As16() ipFields := &header.IPv6Fields{ - SrcAddr: tcpip.Address(srcAs16[:]), - DstAddr: tcpip.Address(dstAs16[:]), + SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), + DstAddr: tcpip.AddrFromSlice(dstAs16[:]), TransportProtocol: unix.IPPROTO_TCP, HopLimit: 64, PayloadLength: uint16(segmentSize + 20), From e06231b8611133d249b8a1d5eaf1588c27800f05 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 4 Apr 2023 13:04:30 -0700 Subject: [PATCH 003/173] conn, device: use UDP GSO and GRO on Linux StdNetBind probes for UDP GSO and GRO support at runtime. UDP GSO is dependent on checksum offload support on the egress netdev. UDP GSO will be disabled in the event sendmmsg() returns EIO, which is a strong signal that the egress netdev does not support checksum offload. The iperf3 results below demonstrate the effect of this commit between two Linux computers with i5-12400 CPUs. There is roughly ~13us of round trip latency between them. The first result is from commit 052af4a without UDP GSO or GRO. Starting Test: protocol: TCP, 1 streams, 131072 byte blocks [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-10.00 sec 9.85 GBytes 8.46 Gbits/sec 1139 3.01 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - Test Complete. Summary Results: [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 9.85 GBytes 8.46 Gbits/sec 1139 sender [ 5] 0.00-10.04 sec 9.85 GBytes 8.42 Gbits/sec receiver The second result is with UDP GSO and GRO. Starting Test: protocol: TCP, 1 streams, 131072 byte blocks [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-10.00 sec 12.3 GBytes 10.6 Gbits/sec 232 3.15 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - Test Complete. Summary Results: [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 12.3 GBytes 10.6 Gbits/sec 232 sender [ 5] 0.00-10.04 sec 12.3 GBytes 10.6 Gbits/sec receiver Reviewed-by: Adrian Dewhurst Signed-off-by: Jordan Whited --- conn/bind_std.go | 396 ++++++++++++------ conn/bind_std_test.go | 230 +++++++++- .../{sticky_default.go => control_default.go} | 22 +- conn/{sticky_linux.go => control_linux.go} | 51 ++- ...ky_linux_test.go => control_linux_test.go} | 4 +- conn/controlfns_linux.go | 8 + conn/errors_default.go | 12 + conn/errors_linux.go | 26 ++ conn/features_default.go | 15 + conn/features_linux.go | 42 ++ device/send.go | 8 + 11 files changed, 673 insertions(+), 141 deletions(-) rename conn/{sticky_default.go => control_default.go} (54%) rename conn/{sticky_linux.go => control_linux.go} (65%) rename conn/{sticky_linux_test.go => control_linux_test.go} (98%) create mode 100644 conn/errors_default.go create mode 100644 conn/errors_linux.go create mode 100644 conn/features_default.go create mode 100644 conn/features_linux.go diff --git a/conn/bind_std.go b/conn/bind_std.go index c701ef8..cc5cf23 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -8,6 +8,7 @@ package conn import ( "context" "errors" + "fmt" "net" "net/netip" "runtime" @@ -20,7 +21,8 @@ import ( ) var ( - _ Bind = (*StdNetBind)(nil) + _ Bind = (*StdNetBind)(nil) + _ Endpoint = (*StdNetEndpoint)(nil) ) // StdNetBind implements Bind for all platforms. While Windows has its own Bind @@ -29,16 +31,19 @@ var ( // methods for sending and receiving multiple datagrams per-syscall. See the // proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564. type StdNetBind struct { - mu sync.Mutex // protects all fields except as specified - ipv4 *net.UDPConn - ipv6 *net.UDPConn - ipv4PC *ipv4.PacketConn // will be nil on non-Linux - ipv6PC *ipv6.PacketConn // will be nil on non-Linux + mu sync.Mutex // protects all fields except as specified + ipv4 *net.UDPConn + ipv6 *net.UDPConn + ipv4PC *ipv4.PacketConn // will be nil on non-Linux + ipv6PC *ipv6.PacketConn // will be nil on non-Linux + ipv4TxOffload bool + ipv4RxOffload bool + ipv6TxOffload bool + ipv6RxOffload bool - // these three fields are not guarded by mu - udpAddrPool sync.Pool - ipv4MsgsPool sync.Pool - ipv6MsgsPool sync.Pool + // these two fields are not guarded by mu + udpAddrPool sync.Pool + msgsPool sync.Pool blackhole4 bool blackhole6 bool @@ -54,23 +59,12 @@ func NewStdNetBind() Bind { }, }, - ipv4MsgsPool: sync.Pool{ - New: func() any { - msgs := make([]ipv4.Message, IdealBatchSize) - for i := range msgs { - msgs[i].Buffers = make(net.Buffers, 1) - msgs[i].OOB = make([]byte, srcControlSize) - } - return &msgs - }, - }, - - ipv6MsgsPool: sync.Pool{ + msgsPool: sync.Pool{ New: func() any { msgs := make([]ipv6.Message, IdealBatchSize) for i := range msgs { msgs[i].Buffers = make(net.Buffers, 1) - msgs[i].OOB = make([]byte, srcControlSize) + msgs[i].OOB = make([]byte, controlSize) } return &msgs }, @@ -179,19 +173,21 @@ again: } var fns []ReceiveFunc if v4conn != nil { + s.ipv4TxOffload, s.ipv4RxOffload = supportsUDPOffload(v4conn) if runtime.GOOS == "linux" { v4pc = ipv4.NewPacketConn(v4conn) s.ipv4PC = v4pc } - fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn)) + fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload)) s.ipv4 = v4conn } if v6conn != nil { + s.ipv6TxOffload, s.ipv6RxOffload = supportsUDPOffload(v6conn) if runtime.GOOS == "linux" { v6pc = ipv6.NewPacketConn(v6conn) s.ipv6PC = v6pc } - fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn)) + fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload)) s.ipv6 = v6conn } if len(fns) == 0 { @@ -201,69 +197,93 @@ again: return fns, uint16(port), nil } -func (s *StdNetBind) makeReceiveIPv4(pc *ipv4.PacketConn, conn *net.UDPConn) ReceiveFunc { - return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) { - msgs := s.ipv4MsgsPool.Get().(*[]ipv4.Message) - defer s.ipv4MsgsPool.Put(msgs) - for i := range bufs { - (*msgs)[i].Buffers[0] = bufs[i] - } - var numMsgs int - if runtime.GOOS == "linux" { - numMsgs, err = pc.ReadBatch(*msgs, 0) +func (s *StdNetBind) putMessages(msgs *[]ipv6.Message) { + for i := range *msgs { + (*msgs)[i] = ipv6.Message{Buffers: (*msgs)[i].Buffers, OOB: (*msgs)[i].OOB} + } + s.msgsPool.Put(msgs) +} + +func (s *StdNetBind) getMessages() *[]ipv6.Message { + return s.msgsPool.Get().(*[]ipv6.Message) +} + +var ( + // If compilation fails here these are no longer the same underlying type. + _ ipv6.Message = ipv4.Message{} +) + +type batchReader interface { + ReadBatch([]ipv6.Message, int) (int, error) +} + +type batchWriter interface { + WriteBatch([]ipv6.Message, int) (int, error) +} + +func (s *StdNetBind) receiveIP( + br batchReader, + conn *net.UDPConn, + rxOffload bool, + bufs [][]byte, + sizes []int, + eps []Endpoint, +) (n int, err error) { + msgs := s.getMessages() + for i := range bufs { + (*msgs)[i].Buffers[0] = bufs[i] + (*msgs)[i].OOB = (*msgs)[i].OOB[:cap((*msgs)[i].OOB)] + } + defer s.putMessages(msgs) + var numMsgs int + if runtime.GOOS == "linux" { + if rxOffload { + readAt := len(*msgs) - 2 + numMsgs, err = br.ReadBatch((*msgs)[readAt:], 0) + if err != nil { + return 0, err + } + numMsgs, err = splitCoalescedMessages(*msgs, readAt, getGSOSize) if err != nil { return 0, err } } else { - msg := &(*msgs)[0] - msg.N, msg.NN, _, msg.Addr, err = conn.ReadMsgUDP(msg.Buffers[0], msg.OOB) + numMsgs, err = br.ReadBatch(*msgs, 0) if err != nil { return 0, err } - numMsgs = 1 } - for i := 0; i < numMsgs; i++ { - msg := &(*msgs)[i] - sizes[i] = msg.N - addrPort := msg.Addr.(*net.UDPAddr).AddrPort() - ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation - getSrcFromControl(msg.OOB[:msg.NN], ep) - eps[i] = ep + } else { + msg := &(*msgs)[0] + msg.N, msg.NN, _, msg.Addr, err = conn.ReadMsgUDP(msg.Buffers[0], msg.OOB) + if err != nil { + return 0, err } - return numMsgs, nil + numMsgs = 1 + } + for i := 0; i < numMsgs; i++ { + msg := &(*msgs)[i] + sizes[i] = msg.N + if sizes[i] == 0 { + continue + } + addrPort := msg.Addr.(*net.UDPAddr).AddrPort() + ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation + getSrcFromControl(msg.OOB[:msg.NN], ep) + eps[i] = ep + } + return numMsgs, nil +} + +func (s *StdNetBind) makeReceiveIPv4(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc { + return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) { + return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps) } } -func (s *StdNetBind) makeReceiveIPv6(pc *ipv6.PacketConn, conn *net.UDPConn) ReceiveFunc { +func (s *StdNetBind) makeReceiveIPv6(pc *ipv6.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc { return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) { - msgs := s.ipv6MsgsPool.Get().(*[]ipv6.Message) - defer s.ipv6MsgsPool.Put(msgs) - for i := range bufs { - (*msgs)[i].Buffers[0] = bufs[i] - } - var numMsgs int - if runtime.GOOS == "linux" { - numMsgs, err = pc.ReadBatch(*msgs, 0) - if err != nil { - return 0, err - } - } else { - msg := &(*msgs)[0] - msg.N, msg.NN, _, msg.Addr, err = conn.ReadMsgUDP(msg.Buffers[0], msg.OOB) - if err != nil { - return 0, err - } - numMsgs = 1 - } - for i := 0; i < numMsgs; i++ { - msg := &(*msgs)[i] - sizes[i] = msg.N - addrPort := msg.Addr.(*net.UDPAddr).AddrPort() - ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation - getSrcFromControl(msg.OOB[:msg.NN], ep) - eps[i] = ep - } - return numMsgs, nil + return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps) } } @@ -293,28 +313,42 @@ func (s *StdNetBind) Close() error { } s.blackhole4 = false s.blackhole6 = false + s.ipv4TxOffload = false + s.ipv4RxOffload = false + s.ipv6TxOffload = false + s.ipv6RxOffload = false if err1 != nil { return err1 } return err2 } +type ErrUDPGSODisabled struct { + onLaddr string + RetryErr error +} + +func (e ErrUDPGSODisabled) Error() string { + return fmt.Sprintf("disabled UDP GSO on %s, NIC(s) may not support checksum offload", e.onLaddr) +} + +func (e ErrUDPGSODisabled) Unwrap() error { + return e.RetryErr +} + func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { s.mu.Lock() blackhole := s.blackhole4 conn := s.ipv4 - var ( - pc4 *ipv4.PacketConn - pc6 *ipv6.PacketConn - ) + offload := s.ipv4TxOffload + br := batchWriter(s.ipv4PC) is6 := false if endpoint.DstIP().Is6() { blackhole = s.blackhole6 conn = s.ipv6 - pc6 = s.ipv6PC + br = s.ipv6PC is6 = true - } else { - pc4 = s.ipv4PC + offload = s.ipv6TxOffload } s.mu.Unlock() @@ -324,25 +358,56 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { if conn == nil { return syscall.EAFNOSUPPORT } + + msgs := s.getMessages() + defer s.putMessages(msgs) + ua := s.udpAddrPool.Get().(*net.UDPAddr) + defer s.udpAddrPool.Put(ua) if is6 { - return s.send6(conn, pc6, endpoint, bufs) + as16 := endpoint.DstIP().As16() + copy(ua.IP, as16[:]) + ua.IP = ua.IP[:16] } else { - return s.send4(conn, pc4, endpoint, bufs) + as4 := endpoint.DstIP().As4() + copy(ua.IP, as4[:]) + ua.IP = ua.IP[:4] } + ua.Port = int(endpoint.(*StdNetEndpoint).Port()) + var ( + retried bool + err error + ) +retry: + if offload { + n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, *msgs, setGSOSize) + err = s.send(conn, br, (*msgs)[:n]) + if err != nil && offload && errShouldDisableUDPGSO(err) { + offload = false + s.mu.Lock() + if is6 { + s.ipv6TxOffload = false + } else { + s.ipv4TxOffload = false + } + s.mu.Unlock() + retried = true + goto retry + } + } else { + for i := range bufs { + (*msgs)[i].Addr = ua + (*msgs)[i].Buffers[0] = bufs[i] + setSrcControl(&(*msgs)[i].OOB, endpoint.(*StdNetEndpoint)) + } + err = s.send(conn, br, (*msgs)[:len(bufs)]) + } + if retried { + return ErrUDPGSODisabled{onLaddr: conn.LocalAddr().String(), RetryErr: err} + } + return err } -func (s *StdNetBind) send4(conn *net.UDPConn, pc *ipv4.PacketConn, ep Endpoint, bufs [][]byte) error { - ua := s.udpAddrPool.Get().(*net.UDPAddr) - as4 := ep.DstIP().As4() - copy(ua.IP, as4[:]) - ua.IP = ua.IP[:4] - ua.Port = int(ep.(*StdNetEndpoint).Port()) - msgs := s.ipv4MsgsPool.Get().(*[]ipv4.Message) - for i, buf := range bufs { - (*msgs)[i].Buffers[0] = buf - (*msgs)[i].Addr = ua - setSrcControl(&(*msgs)[i].OOB, ep.(*StdNetEndpoint)) - } +func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error { var ( n int err error @@ -350,59 +415,128 @@ func (s *StdNetBind) send4(conn *net.UDPConn, pc *ipv4.PacketConn, ep Endpoint, ) if runtime.GOOS == "linux" { for { - n, err = pc.WriteBatch((*msgs)[start:len(bufs)], 0) - if err != nil || n == len((*msgs)[start:len(bufs)]) { + n, err = pc.WriteBatch(msgs[start:], 0) + if err != nil || n == len(msgs[start:]) { break } start += n } } else { - for i, buf := range bufs { - _, _, err = conn.WriteMsgUDP(buf, (*msgs)[i].OOB, ua) + for _, msg := range msgs { + _, _, err = conn.WriteMsgUDP(msg.Buffers[0], msg.OOB, msg.Addr.(*net.UDPAddr)) if err != nil { break } } } - s.udpAddrPool.Put(ua) - s.ipv4MsgsPool.Put(msgs) return err } -func (s *StdNetBind) send6(conn *net.UDPConn, pc *ipv6.PacketConn, ep Endpoint, bufs [][]byte) error { - ua := s.udpAddrPool.Get().(*net.UDPAddr) - as16 := ep.DstIP().As16() - copy(ua.IP, as16[:]) - ua.IP = ua.IP[:16] - ua.Port = int(ep.(*StdNetEndpoint).Port()) - msgs := s.ipv6MsgsPool.Get().(*[]ipv6.Message) - for i, buf := range bufs { - (*msgs)[i].Buffers[0] = buf - (*msgs)[i].Addr = ua - setSrcControl(&(*msgs)[i].OOB, ep.(*StdNetEndpoint)) - } +const ( + // Exceeding these values results in EMSGSIZE. They account for layer3 and + // layer4 headers. IPv6 does not need to account for itself as the payload + // length field is self excluding. + maxIPv4PayloadLen = 1<<16 - 1 - 20 - 8 + maxIPv6PayloadLen = 1<<16 - 1 - 8 + + // This is a hard limit imposed by the kernel. + udpSegmentMaxDatagrams = 64 +) + +type setGSOFunc func(control *[]byte, gsoSize uint16) + +func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs []ipv6.Message, setGSO setGSOFunc) int { var ( - n int - err error - start int + base = -1 // index of msg we are currently coalescing into + gsoSize int // segmentation size of 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 ) - if runtime.GOOS == "linux" { - for { - n, err = pc.WriteBatch((*msgs)[start:len(bufs)], 0) - if err != nil || n == len((*msgs)[start:len(bufs)]) { - break + maxPayloadLen := maxIPv4PayloadLen + if ep.DstIP().Is6() { + maxPayloadLen = maxIPv6PayloadLen + } + for i, buf := range bufs { + if i > 0 { + msgLen := len(buf) + baseLenBefore := len(msgs[base].Buffers[0]) + freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore + if msgLen+baseLenBefore <= maxPayloadLen && + msgLen <= gsoSize && + msgLen <= freeBaseCap && + dgramCnt < udpSegmentMaxDatagrams && + !endBatch { + msgs[base].Buffers[0] = append(msgs[base].Buffers[0], buf...) + if i == len(bufs)-1 { + setGSO(&msgs[base].OOB, uint16(gsoSize)) + } + dgramCnt++ + if msgLen < gsoSize { + // A smaller than gsoSize packet on the tail is legal, but + // it must end the batch. + endBatch = true + } + continue } - start += n } - } else { - for i, buf := range bufs { - _, _, err = conn.WriteMsgUDP(buf, (*msgs)[i].OOB, ua) - if err != nil { - break + if dgramCnt > 1 { + setGSO(&msgs[base].OOB, uint16(gsoSize)) + } + // Reset prior to incrementing base since we are preparing to start a + // new potential batch. + endBatch = false + base++ + gsoSize = len(buf) + setSrcControl(&msgs[base].OOB, ep) + msgs[base].Buffers[0] = buf + msgs[base].Addr = addr + dgramCnt = 1 + } + return base + 1 +} + +type getGSOFunc func(control []byte) (int, error) + +func splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int, getGSO getGSOFunc) (n int, err error) { + for i := firstMsgAt; i < len(msgs); i++ { + msg := &msgs[i] + if msg.N == 0 { + return n, err + } + var ( + gsoSize int + start int + end = msg.N + numToSplit = 1 + ) + gsoSize, err = getGSO(msg.OOB[:msg.NN]) + if err != nil { + return n, err + } + if gsoSize > 0 { + numToSplit = (msg.N + gsoSize - 1) / gsoSize + end = gsoSize + } + for j := 0; j < numToSplit; j++ { + if n > i { + return n, errors.New("splitting coalesced packet resulted in overflow") } + copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end]) + msgs[n].N = copied + msgs[n].Addr = msg.Addr + start = end + end += gsoSize + if end > msg.N { + end = msg.N + } + n++ + } + if i != n-1 { + // It is legal for bytes to move within msg.Buffers[0] as a result + // of splitting, so we only zero the source msg len when it is not + // the destination of the last split operation above. + msg.N = 0 } } - s.udpAddrPool.Put(ua) - s.ipv6MsgsPool.Put(msgs) - return err + return n, nil } diff --git a/conn/bind_std_test.go b/conn/bind_std_test.go index 1e46776..34a3c9a 100644 --- a/conn/bind_std_test.go +++ b/conn/bind_std_test.go @@ -1,6 +1,12 @@ package conn -import "testing" +import ( + "encoding/binary" + "net" + "testing" + + "golang.org/x/net/ipv6" +) func TestStdNetBindReceiveFuncAfterClose(t *testing.T) { bind := NewStdNetBind().(*StdNetBind) @@ -20,3 +26,225 @@ func TestStdNetBindReceiveFuncAfterClose(t *testing.T) { 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]) + } + } + }) + } +} diff --git a/conn/sticky_default.go b/conn/control_default.go similarity index 54% rename from conn/sticky_default.go rename to conn/control_default.go index 1fa8a0c..a8bc06a 100644 --- a/conn/sticky_default.go +++ b/conn/control_default.go @@ -1,4 +1,4 @@ -//go:build !linux || android +//go:build !(linux && !android) /* SPDX-License-Identifier: MIT * @@ -21,8 +21,9 @@ func (e *StdNetEndpoint) SrcToString() string { return "" } -// TODO: macOS, FreeBSD and other BSDs likely do support this feature set, but -// use alternatively named flags and need ports and require testing. +// TODO: macOS, FreeBSD and other BSDs likely do support the sticky sockets +// ({get,set}srcControl feature set, but use alternatively named flags and need +// ports and require testing. // getSrcFromControl parses the control for PKTINFO and if found updates ep with // the source information found. @@ -34,8 +35,17 @@ func getSrcFromControl(control []byte, ep *StdNetEndpoint) { func setSrcControl(control *[]byte, ep *StdNetEndpoint) { } -// srcControlSize returns the recommended buffer size for pooling sticky control -// data. -const srcControlSize = 0 +// getGSOSize parses control for UDP_GRO and if found returns its GSO size data. +func getGSOSize(control []byte) (int, error) { + return 0, nil +} + +// setGSOSize sets a UDP_SEGMENT in control based on gsoSize. +func setGSOSize(control *[]byte, gsoSize uint16) { +} + +// controlSize returns the recommended buffer size for pooling sticky and UDP +// offloading control data. +const controlSize = 0 const StdNetSupportsStickySockets = false diff --git a/conn/sticky_linux.go b/conn/control_linux.go similarity index 65% rename from conn/sticky_linux.go rename to conn/control_linux.go index a30ccc7..f32f26a 100644 --- a/conn/sticky_linux.go +++ b/conn/control_linux.go @@ -8,6 +8,7 @@ package conn import ( + "fmt" "net/netip" "unsafe" @@ -105,6 +106,54 @@ func setSrcControl(control *[]byte, ep *StdNetEndpoint) { *control = append(*control, ep.src...) } -var srcControlSize = unix.CmsgSpace(unix.SizeofInet6Pktinfo) +const ( + sizeOfGSOData = 2 +) + +// getGSOSize parses control for UDP_GRO and if found returns its GSO size data. +func getGSOSize(control []byte) (int, error) { + var ( + hdr unix.Cmsghdr + data []byte + rem = control + err error + ) + + for len(rem) > unix.SizeofCmsghdr { + hdr, data, rem, err = unix.ParseOneSocketControlMessage(rem) + if err != nil { + return 0, fmt.Errorf("error parsing socket control message: %w", err) + } + if hdr.Level == socketOptionLevelUDP && hdr.Type == socketOptionUDPGRO && len(data) >= sizeOfGSOData { + var gso uint16 + copy(unsafe.Slice((*byte)(unsafe.Pointer(&gso)), sizeOfGSOData), data[:sizeOfGSOData]) + return int(gso), nil + } + } + return 0, nil +} + +// setGSOSize sets a UDP_SEGMENT in control based on gsoSize. It leaves existing +// data in control untouched. +func setGSOSize(control *[]byte, gsoSize uint16) { + existingLen := len(*control) + avail := cap(*control) - existingLen + space := unix.CmsgSpace(sizeOfGSOData) + if avail < space { + return + } + *control = (*control)[:cap(*control)] + gsoControl := (*control)[existingLen:] + hdr := (*unix.Cmsghdr)(unsafe.Pointer(&(gsoControl)[0])) + hdr.Level = socketOptionLevelUDP + hdr.Type = socketOptionUDPSegment + hdr.SetLen(unix.CmsgLen(sizeOfGSOData)) + copy((gsoControl)[unix.SizeofCmsghdr:], unsafe.Slice((*byte)(unsafe.Pointer(&gsoSize)), sizeOfGSOData)) + *control = (*control)[:existingLen+space] +} + +// controlSize returns the recommended buffer size for pooling sticky and UDP +// offloading control data. +var controlSize = unix.CmsgSpace(unix.SizeofInet6Pktinfo) + unix.CmsgSpace(sizeOfGSOData) const StdNetSupportsStickySockets = true diff --git a/conn/sticky_linux_test.go b/conn/control_linux_test.go similarity index 98% rename from conn/sticky_linux_test.go rename to conn/control_linux_test.go index 679213a..3ca7d37 100644 --- a/conn/sticky_linux_test.go +++ b/conn/control_linux_test.go @@ -60,7 +60,7 @@ func Test_setSrcControl(t *testing.T) { } setSrc(ep, netip.MustParseAddr("127.0.0.1"), 5) - control := make([]byte, srcControlSize) + control := make([]byte, controlSize) setSrcControl(&control, ep) @@ -89,7 +89,7 @@ func Test_setSrcControl(t *testing.T) { } setSrc(ep, netip.MustParseAddr("::1"), 5) - control := make([]byte, srcControlSize) + control := make([]byte, controlSize) setSrcControl(&control, ep) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index a2396fe..752fbca 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -57,5 +57,13 @@ func init() { } return err }, + + // Attempt to enable UDP_GRO + func(network, address string, c syscall.RawConn) error { + c.Control(func(fd uintptr) { + _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPGRO, 1) + }) + return nil + }, ) } diff --git a/conn/errors_default.go b/conn/errors_default.go new file mode 100644 index 0000000..f1e5b90 --- /dev/null +++ b/conn/errors_default.go @@ -0,0 +1,12 @@ +//go:build !linux + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package conn + +func errShouldDisableUDPGSO(err error) bool { + return false +} diff --git a/conn/errors_linux.go b/conn/errors_linux.go new file mode 100644 index 0000000..8e61000 --- /dev/null +++ b/conn/errors_linux.go @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package conn + +import ( + "errors" + "os" + + "golang.org/x/sys/unix" +) + +func errShouldDisableUDPGSO(err error) bool { + var serr *os.SyscallError + if errors.As(err, &serr) { + // EIO is returned by udp_send_skb() if the device driver does not have + // tx checksumming enabled, which is a hard requirement of UDP_SEGMENT. + // See: + // https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man7/udp.7?id=806eabd74910447f21005160e90957bde4db0183#n228 + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/udp.c?h=v6.2&id=c9c3395d5e3dcc6daee66c6908354d47bf98cb0c#n942 + return serr.Err == unix.EIO + } + return false +} diff --git a/conn/features_default.go b/conn/features_default.go new file mode 100644 index 0000000..d53ff5f --- /dev/null +++ b/conn/features_default.go @@ -0,0 +1,15 @@ +//go:build !linux +// +build !linux + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package conn + +import "net" + +func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { + return +} diff --git a/conn/features_linux.go b/conn/features_linux.go new file mode 100644 index 0000000..513202e --- /dev/null +++ b/conn/features_linux.go @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package conn + +import ( + "net" + + "golang.org/x/sys/unix" +) + +const ( + // TODO: upstream to x/sys/unix + socketOptionLevelUDP = 17 + socketOptionUDPSegment = 103 + socketOptionUDPGRO = 104 +) + +func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { + rc, err := conn.SyscallConn() + if err != nil { + return + } + err = rc.Control(func(fd uintptr) { + _, errSyscall := unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPSegment) + if errSyscall != nil { + return + } + txOffload = true + opt, errSyscall := unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPGRO) + if errSyscall != nil { + return + } + rxOffload = opt == 1 + }) + if err != nil { + return false, false + } + return txOffload, rxOffload +} diff --git a/device/send.go b/device/send.go index a95a46f..ea34997 100644 --- a/device/send.go +++ b/device/send.go @@ -14,6 +14,7 @@ import ( "sync" "time" + "github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" @@ -525,6 +526,13 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { device.PutOutboundElement(elem) } device.PutOutboundElementsSlice(elems) + 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 From d831fef379ddf6876304385621dd61995e7e32f5 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 4 Apr 2023 13:06:08 -0700 Subject: [PATCH 004/173] device: distribute crypto work as slice of elements After reducing UDP stack traversal overhead via GSO and GRO, runtime.chanrecv() began to account for a high percentage (20% in one environment) of perf samples during a throughput benchmark. The individual packet channel ops with the crypto goroutines was the primary contributor to this overhead. Updating these channels to pass vectors, which the device package already handles at its ends, reduced this overhead substantially, and improved throughput. The iperf3 results below demonstrate the effect of this commit between two Linux computers with i5-12400 CPUs. There is roughly ~13us of round trip latency between them. The first result is with UDP GSO and GRO, and with single element channels. Starting Test: protocol: TCP, 1 streams, 131072 byte blocks [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-10.00 sec 12.3 GBytes 10.6 Gbits/sec 232 3.15 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - Test Complete. Summary Results: [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 12.3 GBytes 10.6 Gbits/sec 232 sender [ 5] 0.00-10.04 sec 12.3 GBytes 10.6 Gbits/sec receiver The second result is with channels updated to pass a slice of elements. Starting Test: protocol: TCP, 1 streams, 131072 byte blocks [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-10.00 sec 13.2 GBytes 11.3 Gbits/sec 182 3.15 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - Test Complete. Summary Results: [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 13.2 GBytes 11.3 Gbits/sec 182 sender [ 5] 0.00-10.04 sec 13.2 GBytes 11.3 Gbits/sec receiver Reviewed-by: Adrian Dewhurst Signed-off-by: Jordan Whited --- device/channels.go | 8 ++++---- device/receive.go | 42 ++++++++++++++++++++-------------------- device/send.go | 48 +++++++++++++++++++++++----------------------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/device/channels.go b/device/channels.go index 039d8df..40ee5c9 100644 --- a/device/channels.go +++ b/device/channels.go @@ -19,13 +19,13 @@ import ( // call wg.Done to remove the initial reference. // When the refcount hits 0, the queue's channel is closed. type outboundQueue struct { - c chan *QueueOutboundElement + c chan *[]*QueueOutboundElement wg sync.WaitGroup } func newOutboundQueue() *outboundQueue { q := &outboundQueue{ - c: make(chan *QueueOutboundElement, QueueOutboundSize), + c: make(chan *[]*QueueOutboundElement, QueueOutboundSize), } q.wg.Add(1) go func() { @@ -37,13 +37,13 @@ func newOutboundQueue() *outboundQueue { // A inboundQueue is similar to an outboundQueue; see those docs. type inboundQueue struct { - c chan *QueueInboundElement + c chan *[]*QueueInboundElement wg sync.WaitGroup } func newInboundQueue() *inboundQueue { q := &inboundQueue{ - c: make(chan *QueueInboundElement, QueueInboundSize), + c: make(chan *[]*QueueInboundElement, QueueInboundSize), } q.wg.Add(1) go func() { diff --git a/device/receive.go b/device/receive.go index c0cf747..744bf18 100644 --- a/device/receive.go +++ b/device/receive.go @@ -220,9 +220,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive for peer, elems := range elemsByPeer { if peer.isRunning.Load() { peer.queue.inbound.c <- elems - for _, elem := range *elems { - device.queue.decryption.c <- elem - } + device.queue.decryption.c <- elems } else { for _, elem := range *elems { device.PutMessageBuffer(elem.buffer) @@ -241,26 +239,28 @@ func (device *Device) RoutineDecryption(id int) { defer device.log.Verbosef("Routine: decryption worker %d - stopped", id) device.log.Verbosef("Routine: decryption worker %d - started", id) - for elem := range device.queue.decryption.c { - // split message into fields - counter := elem.packet[MessageTransportOffsetCounter:MessageTransportOffsetContent] - content := elem.packet[MessageTransportOffsetContent:] + for elems := range device.queue.decryption.c { + for _, elem := range *elems { + // split message into fields + counter := elem.packet[MessageTransportOffsetCounter:MessageTransportOffsetContent] + content := elem.packet[MessageTransportOffsetContent:] - // decrypt and release to consumer - var err error - elem.counter = binary.LittleEndian.Uint64(counter) - // copy counter to nonce - binary.LittleEndian.PutUint64(nonce[0x4:0xc], elem.counter) - elem.packet, err = elem.keypair.receive.Open( - content[:0], - nonce[:], - content, - nil, - ) - if err != nil { - elem.packet = nil + // decrypt and release to consumer + var err error + elem.counter = binary.LittleEndian.Uint64(counter) + // copy counter to nonce + binary.LittleEndian.PutUint64(nonce[0x4:0xc], elem.counter) + elem.packet, err = elem.keypair.receive.Open( + content[:0], + nonce[:], + content, + nil, + ) + if err != nil { + elem.packet = nil + } + elem.Unlock() } - elem.Unlock() } } diff --git a/device/send.go b/device/send.go index ea34997..7adfacf 100644 --- a/device/send.go +++ b/device/send.go @@ -385,9 +385,7 @@ top: // add to parallel and sequential queue if peer.isRunning.Load() { peer.queue.outbound.c <- elems - for _, elem := range *elems { - peer.device.queue.encryption.c <- elem - } + peer.device.queue.encryption.c <- elems } else { for _, elem := range *elems { peer.device.PutMessageBuffer(elem.buffer) @@ -447,32 +445,34 @@ func (device *Device) RoutineEncryption(id int) { defer device.log.Verbosef("Routine: encryption worker %d - stopped", id) device.log.Verbosef("Routine: encryption worker %d - started", id) - for elem := range device.queue.encryption.c { - // populate header fields - header := elem.buffer[:MessageTransportHeaderSize] + for elems := range device.queue.encryption.c { + for _, elem := range *elems { + // populate header fields + header := elem.buffer[:MessageTransportHeaderSize] - fieldType := header[0:4] - fieldReceiver := header[4:8] - fieldNonce := header[8:16] + fieldType := header[0:4] + fieldReceiver := header[4:8] + fieldNonce := header[8:16] - binary.LittleEndian.PutUint32(fieldType, MessageTransportType) - binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) - binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) + binary.LittleEndian.PutUint32(fieldType, MessageTransportType) + binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) + binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) - // pad content to multiple of 16 - paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load())) - elem.packet = append(elem.packet, paddingZeros[:paddingSize]...) + // pad content to multiple of 16 + paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load())) + elem.packet = append(elem.packet, paddingZeros[:paddingSize]...) - // encrypt content and release to consumer + // encrypt content and release to consumer - binary.LittleEndian.PutUint64(nonce[4:], elem.nonce) - elem.packet = elem.keypair.send.Seal( - header, - nonce[:], - elem.packet, - nil, - ) - elem.Unlock() + binary.LittleEndian.PutUint64(nonce[4:], elem.nonce) + elem.packet = elem.keypair.send.Seal( + header, + nonce[:], + elem.packet, + nil, + ) + elem.Unlock() + } } } From 915962ded2318b41ddc7d2664bad3ffb12a8998f Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 4 Apr 2023 13:07:11 -0700 Subject: [PATCH 005/173] tun: unwind summing loop in checksumNoFold() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $ benchstat old.txt new.txt goos: linux goarch: amd64 pkg: golang.zx2c4.com/wireguard/tun cpu: 12th Gen Intel(R) Core(TM) i5-12400 │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ Checksum/64-12 10.670n ± 2% 4.769n ± 0% -55.30% (p=0.000 n=10) Checksum/128-12 19.665n ± 2% 8.032n ± 0% -59.16% (p=0.000 n=10) Checksum/256-12 37.68n ± 1% 16.06n ± 0% -57.37% (p=0.000 n=10) Checksum/512-12 76.61n ± 3% 32.13n ± 0% -58.06% (p=0.000 n=10) Checksum/1024-12 160.55n ± 4% 64.25n ± 0% -59.98% (p=0.000 n=10) Checksum/1500-12 231.05n ± 7% 94.12n ± 0% -59.26% (p=0.000 n=10) Checksum/2048-12 309.5n ± 3% 128.5n ± 0% -58.48% (p=0.000 n=10) Checksum/4096-12 603.8n ± 4% 257.2n ± 0% -57.41% (p=0.000 n=10) Checksum/8192-12 1185.0n ± 3% 515.5n ± 0% -56.50% (p=0.000 n=10) Checksum/9000-12 1328.5n ± 5% 564.8n ± 0% -57.49% (p=0.000 n=10) Checksum/9001-12 1340.5n ± 3% 564.8n ± 0% -57.87% (p=0.000 n=10) geomean 185.3n 77.99n -57.92% Reviewed-by: Adrian Dewhurst Signed-off-by: Jordan Whited --- tun/checksum.go | 100 +++++++++++++++++++++++++++++++++++++------ tun/checksum_test.go | 35 +++++++++++++++ 2 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 tun/checksum_test.go diff --git a/tun/checksum.go b/tun/checksum.go index f4f8471..29a8fc8 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -3,23 +3,99 @@ package tun import "encoding/binary" // TODO: Explore SIMD and/or other assembly optimizations. +// TODO: Test native endian loads. See RFC 1071 section 2 part B. func checksumNoFold(b []byte, initial uint64) uint64 { ac := initial - i := 0 - n := len(b) - for n >= 4 { - ac += uint64(binary.BigEndian.Uint32(b[i : i+4])) - n -= 4 - i += 4 + + for len(b) >= 128 { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac += uint64(binary.BigEndian.Uint32(b[16:20])) + ac += uint64(binary.BigEndian.Uint32(b[20:24])) + ac += uint64(binary.BigEndian.Uint32(b[24:28])) + ac += uint64(binary.BigEndian.Uint32(b[28:32])) + ac += uint64(binary.BigEndian.Uint32(b[32:36])) + ac += uint64(binary.BigEndian.Uint32(b[36:40])) + ac += uint64(binary.BigEndian.Uint32(b[40:44])) + ac += uint64(binary.BigEndian.Uint32(b[44:48])) + ac += uint64(binary.BigEndian.Uint32(b[48:52])) + ac += uint64(binary.BigEndian.Uint32(b[52:56])) + ac += uint64(binary.BigEndian.Uint32(b[56:60])) + ac += uint64(binary.BigEndian.Uint32(b[60:64])) + ac += uint64(binary.BigEndian.Uint32(b[64:68])) + ac += uint64(binary.BigEndian.Uint32(b[68:72])) + ac += uint64(binary.BigEndian.Uint32(b[72:76])) + ac += uint64(binary.BigEndian.Uint32(b[76:80])) + ac += uint64(binary.BigEndian.Uint32(b[80:84])) + ac += uint64(binary.BigEndian.Uint32(b[84:88])) + ac += uint64(binary.BigEndian.Uint32(b[88:92])) + ac += uint64(binary.BigEndian.Uint32(b[92:96])) + ac += uint64(binary.BigEndian.Uint32(b[96:100])) + ac += uint64(binary.BigEndian.Uint32(b[100:104])) + ac += uint64(binary.BigEndian.Uint32(b[104:108])) + ac += uint64(binary.BigEndian.Uint32(b[108:112])) + ac += uint64(binary.BigEndian.Uint32(b[112:116])) + ac += uint64(binary.BigEndian.Uint32(b[116:120])) + ac += uint64(binary.BigEndian.Uint32(b[120:124])) + ac += uint64(binary.BigEndian.Uint32(b[124:128])) + b = b[128:] } - for n >= 2 { - ac += uint64(binary.BigEndian.Uint16(b[i : i+2])) - n -= 2 - i += 2 + if len(b) >= 64 { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac += uint64(binary.BigEndian.Uint32(b[16:20])) + ac += uint64(binary.BigEndian.Uint32(b[20:24])) + ac += uint64(binary.BigEndian.Uint32(b[24:28])) + ac += uint64(binary.BigEndian.Uint32(b[28:32])) + ac += uint64(binary.BigEndian.Uint32(b[32:36])) + ac += uint64(binary.BigEndian.Uint32(b[36:40])) + ac += uint64(binary.BigEndian.Uint32(b[40:44])) + ac += uint64(binary.BigEndian.Uint32(b[44:48])) + ac += uint64(binary.BigEndian.Uint32(b[48:52])) + ac += uint64(binary.BigEndian.Uint32(b[52:56])) + ac += uint64(binary.BigEndian.Uint32(b[56:60])) + ac += uint64(binary.BigEndian.Uint32(b[60:64])) + b = b[64:] } - if n == 1 { - ac += uint64(b[i]) << 8 + if len(b) >= 32 { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac += uint64(binary.BigEndian.Uint32(b[16:20])) + ac += uint64(binary.BigEndian.Uint32(b[20:24])) + ac += uint64(binary.BigEndian.Uint32(b[24:28])) + ac += uint64(binary.BigEndian.Uint32(b[28:32])) + b = b[32:] } + if len(b) >= 16 { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + b = b[16:] + } + if len(b) >= 8 { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + b = b[8:] + } + if len(b) >= 4 { + ac += uint64(binary.BigEndian.Uint32(b)) + b = b[4:] + } + if len(b) >= 2 { + ac += uint64(binary.BigEndian.Uint16(b)) + b = b[2:] + } + if len(b) == 1 { + ac += uint64(b[0]) << 8 + } + return ac } diff --git a/tun/checksum_test.go b/tun/checksum_test.go new file mode 100644 index 0000000..c1ccff5 --- /dev/null +++ b/tun/checksum_test.go @@ -0,0 +1,35 @@ +package tun + +import ( + "fmt" + "math/rand" + "testing" +) + +func BenchmarkChecksum(b *testing.B) { + lengths := []int{ + 64, + 128, + 256, + 512, + 1024, + 1500, + 2048, + 4096, + 8192, + 9000, + 9001, + } + + for _, length := range lengths { + b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { + buf := make([]byte, length) + rng := rand.New(rand.NewSource(1)) + rng.Read(buf) + b.ResetTimer() + for i := 0; i < b.N; i++ { + checksum(buf, 0) + } + }) + } +} From ceb9a09d035f373d6ca9617b5e25195c100017fb Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 4 Apr 2023 13:07:35 -0700 Subject: [PATCH 006/173] tun: reduce redundant checksumming in tcpGRO() IPv4 header and pseudo header checksums were being computed on every merge operation. Additionally, virtioNetHdr was being written at the same time. This delays those operations until after all coalescing has occurred. Reviewed-by: Adrian Dewhurst Signed-off-by: Jordan Whited --- tun/tcp_offload_linux.go | 162 ++++++++++++++++++++++++--------------- 1 file changed, 99 insertions(+), 63 deletions(-) diff --git a/tun/tcp_offload_linux.go b/tun/tcp_offload_linux.go index d64010d..6728823 100644 --- a/tun/tcp_offload_linux.go +++ b/tun/tcp_offload_linux.go @@ -269,11 +269,11 @@ func tcpChecksumValid(pkt []byte, iphLen uint8, isV6 bool) bool { type coalesceResult int const ( - coalesceInsufficientCap coalesceResult = 0 - coalescePSHEnding coalesceResult = 1 - coalesceItemInvalidCSum coalesceResult = 2 - coalescePktInvalidCSum coalesceResult = 3 - coalesceSuccess coalesceResult = 4 + coalesceInsufficientCap coalesceResult = iota + coalescePSHEnding + coalesceItemInvalidCSum + coalescePktInvalidCSum + coalesceSuccess ) // coalesceTCPPackets attempts to coalesce pkt with the packet described by @@ -339,42 +339,6 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize if gsoSize > item.gsoSize { item.gsoSize = gsoSize } - hdr := virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, // this turns into CHECKSUM_PARTIAL in the skb - hdrLen: uint16(headersLen), - gsoSize: uint16(item.gsoSize), - csumStart: uint16(item.iphLen), - csumOffset: 16, - } - - // Recalculate the total len (IPv4) or payload len (IPv6). Recalculate the - // (IPv4) header checksum. - if isV6 { - hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV6 - binary.BigEndian.PutUint16(pktHead[4:], uint16(coalescedLen)-uint16(item.iphLen)) // set new payload len - } else { - hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV4 - pktHead[10], pktHead[11] = 0, 0 // clear checksum field - binary.BigEndian.PutUint16(pktHead[2:], uint16(coalescedLen)) // set new total length - iphCSum := ^checksum(pktHead[:item.iphLen], 0) // compute checksum - binary.BigEndian.PutUint16(pktHead[10:], iphCSum) // set checksum field - } - hdr.encode(bufs[item.bufsIndex][bufsOffset-virtioNetHdrLen:]) - - // Calculate the pseudo header checksum and place it at the TCP checksum - // offset. Downstream checksum offloading will combine this with computation - // of the tcp header and payload checksum. - addrLen := 4 - addrOffset := ipv4SrcAddrOffset - if isV6 { - addrLen = 16 - addrOffset = ipv6SrcAddrOffset - } - srcAddrAt := bufsOffset + addrOffset - srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] - dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] - psum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(coalescedLen-int(item.iphLen))) - binary.BigEndian.PutUint16(pktHead[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) item.numMerged++ return coalesceSuccess @@ -390,43 +354,52 @@ const ( maxUint16 = 1<<16 - 1 ) +type tcpGROResult int + +const ( + tcpGROResultNoop tcpGROResult = iota + tcpGROResultTableInsert + tcpGROResultCoalesced +) + // tcpGRO evaluates the TCP packet at pktI in bufs for coalescing with -// existing packets tracked in table. It will return false when pktI is not -// coalesced, otherwise true. This indicates to the caller if bufs[pktI] -// should be written to the Device. -func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) (pktCoalesced bool) { +// existing packets tracked in table. It returns a tcpGROResultNoop when no +// action was taken, tcpGROResultTableInsert when the evaluated packet was +// inserted into table, and tcpGROResultCoalesced when the evaluated packet was +// coalesced with another packet in table. +func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) tcpGROResult { pkt := bufs[pktI][offset:] if len(pkt) > maxUint16 { // A valid IPv4 or IPv6 packet will never exceed this. - return false + return tcpGROResultNoop } iphLen := int((pkt[0] & 0x0F) * 4) if isV6 { iphLen = 40 ipv6HPayloadLen := int(binary.BigEndian.Uint16(pkt[4:])) if ipv6HPayloadLen != len(pkt)-iphLen { - return false + return tcpGROResultNoop } } else { totalLen := int(binary.BigEndian.Uint16(pkt[2:])) if totalLen != len(pkt) { - return false + return tcpGROResultNoop } } if len(pkt) < iphLen { - return false + return tcpGROResultNoop } tcphLen := int((pkt[iphLen+12] >> 4) * 4) if tcphLen < 20 || tcphLen > 60 { - return false + return tcpGROResultNoop } if len(pkt) < iphLen+tcphLen { - return false + return tcpGROResultNoop } if !isV6 { if pkt[6]&ipv4FlagMoreFragments != 0 || pkt[6]<<3 != 0 || pkt[7] != 0 { // no GRO support for fragmented segments for now - return false + return tcpGROResultNoop } } tcpFlags := pkt[iphLen+tcpFlagsOffset] @@ -434,14 +407,14 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) // not a candidate if any non-ACK flags (except PSH+ACK) are set if tcpFlags != tcpFlagACK { if pkt[iphLen+tcpFlagsOffset] != tcpFlagACK|tcpFlagPSH { - return false + return tcpGROResultNoop } pshSet = true } gsoSize := uint16(len(pkt) - tcphLen - iphLen) // not a candidate if payload len is 0 if gsoSize < 1 { - return false + return tcpGROResultNoop } seq := binary.BigEndian.Uint32(pkt[iphLen+4:]) srcAddrOffset := ipv4SrcAddrOffset @@ -452,7 +425,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) } items, existing := table.lookupOrInsert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) if !existing { - return false + return tcpGROResultNoop } for i := len(items) - 1; i >= 0; i-- { // In the best case of packets arriving in order iterating in reverse is @@ -470,20 +443,20 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) switch result { case coalesceSuccess: table.updateAt(item, i) - return true + return tcpGROResultCoalesced case coalesceItemInvalidCSum: // delete the item with an invalid csum table.deleteAt(item.key, i) case coalescePktInvalidCSum: // no point in inserting an item that we can't coalesce - return false + return tcpGROResultNoop default: } } } // failed to coalesce with any other packets; store the item in the flow table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) - return false + return tcpGROResultTableInsert } func isTCP4NoIPOptions(b []byte) bool { @@ -515,6 +488,64 @@ func isTCP6NoEH(b []byte) bool { return true } +// applyCoalesceAccounting updates bufs to account for coalescing based on the +// metadata found in table. +func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 bool) error { + for _, items := range table.itemsByFlow { + for _, item := range items { + if item.numMerged > 0 { + hdr := virtioNetHdr{ + flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, // this turns into CHECKSUM_PARTIAL in the skb + hdrLen: uint16(item.iphLen + item.tcphLen), + gsoSize: item.gsoSize, + csumStart: uint16(item.iphLen), + csumOffset: 16, + } + pkt := bufs[item.bufsIndex][offset:] + + // Recalculate the total len (IPv4) or payload len (IPv6). + // Recalculate the (IPv4) header checksum. + if isV6 { + hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV6 + binary.BigEndian.PutUint16(pkt[4:], uint16(len(pkt))-uint16(item.iphLen)) // set new IPv6 header payload len + } else { + hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV4 + pkt[10], pkt[11] = 0, 0 + binary.BigEndian.PutUint16(pkt[2:], uint16(len(pkt))) // set new total length + iphCSum := ^checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum + binary.BigEndian.PutUint16(pkt[10:], iphCSum) // set IPv4 header checksum field + } + err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) + if err != nil { + return err + } + + // Calculate the pseudo header checksum and place it at the TCP + // checksum offset. Downstream checksum offloading will combine + // this with computation of the tcp header and payload checksum. + addrLen := 4 + addrOffset := ipv4SrcAddrOffset + if isV6 { + addrLen = 16 + addrOffset = ipv6SrcAddrOffset + } + srcAddrAt := offset + addrOffset + srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] + dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] + psum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) + binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) + } else { + hdr := virtioNetHdr{} + err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) + if err != nil { + return err + } + } + } + } + return nil +} + // handleGRO evaluates bufs for GRO, and writes the indices of the resulting // packets into toWrite. toWrite, tcp4Table, and tcp6Table should initially be // empty (but non-nil), and are passed in to save allocs as the caller may reset @@ -524,23 +555,28 @@ func handleGRO(bufs [][]byte, offset int, tcp4Table, tcp6Table *tcpGROTable, toW if offset < virtioNetHdrLen || offset > len(bufs[i])-1 { return errors.New("invalid offset") } - var coalesced bool + var result tcpGROResult switch { case isTCP4NoIPOptions(bufs[i][offset:]): // ipv4 packets w/IP options do not coalesce - coalesced = tcpGRO(bufs, offset, i, tcp4Table, false) + result = tcpGRO(bufs, offset, i, tcp4Table, false) case isTCP6NoEH(bufs[i][offset:]): // ipv6 packets w/extension headers do not coalesce - coalesced = tcpGRO(bufs, offset, i, tcp6Table, true) + result = tcpGRO(bufs, offset, i, tcp6Table, true) } - if !coalesced { + switch result { + case tcpGROResultNoop: hdr := virtioNetHdr{} err := hdr.encode(bufs[i][offset-virtioNetHdrLen:]) if err != nil { return err } + fallthrough + case tcpGROResultTableInsert: *toWrite = append(*toWrite, i) } } - return nil + err4 := applyCoalesceAccounting(bufs, offset, tcp4Table, false) + err6 := applyCoalesceAccounting(bufs, offset, tcp6Table, true) + return errors.Join(err4, err6) } // tcpTSO splits packets from in into outBuffs, writing the size of each From cc7b29b8c60406713a50faf0175d6a0481a182f6 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 5 Apr 2023 15:40:40 -0700 Subject: [PATCH 007/173] device: move Queue{In,Out}boundElement Mutex to container type Queue{In,Out}boundElement locking can contribute to significant overhead via sync.Mutex.lockSlow() in some environments. These types are passed throughout the device package as elements in a slice, so move the per-element Mutex to a container around the slice. Signed-off-by: Jordan Whited --- device/channels.go | 32 +++++++-------- device/device.go | 10 ++--- device/peer.go | 8 ++-- device/pools.go | 44 +++++++++++---------- device/receive.go | 43 ++++++++++---------- device/send.go | 99 ++++++++++++++++++++++++---------------------- 6 files changed, 123 insertions(+), 113 deletions(-) diff --git a/device/channels.go b/device/channels.go index 40ee5c9..e526f6b 100644 --- a/device/channels.go +++ b/device/channels.go @@ -19,13 +19,13 @@ import ( // call wg.Done to remove the initial reference. // When the refcount hits 0, the queue's channel is closed. type outboundQueue struct { - c chan *[]*QueueOutboundElement + c chan *QueueOutboundElementsContainer wg sync.WaitGroup } func newOutboundQueue() *outboundQueue { q := &outboundQueue{ - c: make(chan *[]*QueueOutboundElement, QueueOutboundSize), + c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize), } q.wg.Add(1) go func() { @@ -37,13 +37,13 @@ func newOutboundQueue() *outboundQueue { // A inboundQueue is similar to an outboundQueue; see those docs. type inboundQueue struct { - c chan *[]*QueueInboundElement + c chan *QueueInboundElementsContainer wg sync.WaitGroup } func newInboundQueue() *inboundQueue { q := &inboundQueue{ - c: make(chan *[]*QueueInboundElement, QueueInboundSize), + c: make(chan *QueueInboundElementsContainer, QueueInboundSize), } q.wg.Add(1) go func() { @@ -72,7 +72,7 @@ func newHandshakeQueue() *handshakeQueue { } type autodrainingInboundQueue struct { - c chan *[]*QueueInboundElement + c chan *QueueInboundElementsContainer } // newAutodrainingInboundQueue returns a channel that will be drained when it gets GC'd. @@ -81,7 +81,7 @@ type autodrainingInboundQueue struct { // some other means, such as sending a sentinel nil values. func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue { q := &autodrainingInboundQueue{ - c: make(chan *[]*QueueInboundElement, QueueInboundSize), + c: make(chan *QueueInboundElementsContainer, QueueInboundSize), } runtime.SetFinalizer(q, device.flushInboundQueue) return q @@ -90,13 +90,13 @@ func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue { func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { for { select { - case elems := <-q.c: - for _, elem := range *elems { - elem.Lock() + case elemsContainer := <-q.c: + elemsContainer.Lock() + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutInboundElement(elem) } - device.PutInboundElementsSlice(elems) + device.PutInboundElementsContainer(elemsContainer) default: return } @@ -104,7 +104,7 @@ func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { } type autodrainingOutboundQueue struct { - c chan *[]*QueueOutboundElement + c chan *QueueOutboundElementsContainer } // newAutodrainingOutboundQueue returns a channel that will be drained when it gets GC'd. @@ -114,7 +114,7 @@ type autodrainingOutboundQueue struct { // All sends to the channel must be best-effort, because there may be no receivers. func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue { q := &autodrainingOutboundQueue{ - c: make(chan *[]*QueueOutboundElement, QueueOutboundSize), + c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize), } runtime.SetFinalizer(q, device.flushOutboundQueue) return q @@ -123,13 +123,13 @@ func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue { func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { for { select { - case elems := <-q.c: - for _, elem := range *elems { - elem.Lock() + case elemsContainer := <-q.c: + elemsContainer.Lock() + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } - device.PutOutboundElementsSlice(elems) + device.PutOutboundElementsContainer(elemsContainer) default: return } diff --git a/device/device.go b/device/device.go index 7482d9b..5c666ac 100644 --- a/device/device.go +++ b/device/device.go @@ -68,11 +68,11 @@ type Device struct { cookieChecker CookieChecker pool struct { - outboundElementsSlice *WaitPool - inboundElementsSlice *WaitPool - messageBuffers *WaitPool - inboundElements *WaitPool - outboundElements *WaitPool + inboundElementsContainer *WaitPool + outboundElementsContainer *WaitPool + messageBuffers *WaitPool + inboundElements *WaitPool + outboundElements *WaitPool } queue struct { diff --git a/device/peer.go b/device/peer.go index c7163ac..22757d4 100644 --- a/device/peer.go +++ b/device/peer.go @@ -45,9 +45,9 @@ type Peer struct { } queue struct { - staged chan *[]*QueueOutboundElement // staged packets before a handshake is available - outbound *autodrainingOutboundQueue // sequential ordering of udp transmission - inbound *autodrainingInboundQueue // sequential ordering of tun writing + staged chan *QueueOutboundElementsContainer // staged packets before a handshake is available + outbound *autodrainingOutboundQueue // sequential ordering of udp transmission + inbound *autodrainingInboundQueue // sequential ordering of tun writing } cookieGenerator CookieGenerator @@ -81,7 +81,7 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { peer.device = device peer.queue.outbound = newAutodrainingOutboundQueue(device) peer.queue.inbound = newAutodrainingInboundQueue(device) - peer.queue.staged = make(chan *[]*QueueOutboundElement, QueueStagedSize) + peer.queue.staged = make(chan *QueueOutboundElementsContainer, QueueStagedSize) // map public key _, ok := device.peers.keyMap[pk] diff --git a/device/pools.go b/device/pools.go index 02a5d6a..94f3dc7 100644 --- a/device/pools.go +++ b/device/pools.go @@ -46,13 +46,13 @@ func (p *WaitPool) Put(x any) { } func (device *Device) PopulatePools() { - device.pool.outboundElementsSlice = NewWaitPool(PreallocatedBuffersPerPool, func() any { - s := make([]*QueueOutboundElement, 0, device.BatchSize()) - return &s - }) - device.pool.inboundElementsSlice = NewWaitPool(PreallocatedBuffersPerPool, func() any { + device.pool.inboundElementsContainer = NewWaitPool(PreallocatedBuffersPerPool, func() any { s := make([]*QueueInboundElement, 0, device.BatchSize()) - return &s + return &QueueInboundElementsContainer{elems: s} + }) + device.pool.outboundElementsContainer = NewWaitPool(PreallocatedBuffersPerPool, func() any { + s := make([]*QueueOutboundElement, 0, device.BatchSize()) + return &QueueOutboundElementsContainer{elems: s} }) device.pool.messageBuffers = NewWaitPool(PreallocatedBuffersPerPool, func() any { return new([MaxMessageSize]byte) @@ -65,28 +65,32 @@ func (device *Device) PopulatePools() { }) } -func (device *Device) GetOutboundElementsSlice() *[]*QueueOutboundElement { - return device.pool.outboundElementsSlice.Get().(*[]*QueueOutboundElement) +func (device *Device) GetInboundElementsContainer() *QueueInboundElementsContainer { + c := device.pool.inboundElementsContainer.Get().(*QueueInboundElementsContainer) + c.Mutex = sync.Mutex{} + return c } -func (device *Device) PutOutboundElementsSlice(s *[]*QueueOutboundElement) { - for i := range *s { - (*s)[i] = nil +func (device *Device) PutInboundElementsContainer(c *QueueInboundElementsContainer) { + for i := range c.elems { + c.elems[i] = nil } - *s = (*s)[:0] - device.pool.outboundElementsSlice.Put(s) + c.elems = c.elems[:0] + device.pool.inboundElementsContainer.Put(c) } -func (device *Device) GetInboundElementsSlice() *[]*QueueInboundElement { - return device.pool.inboundElementsSlice.Get().(*[]*QueueInboundElement) +func (device *Device) GetOutboundElementsContainer() *QueueOutboundElementsContainer { + c := device.pool.outboundElementsContainer.Get().(*QueueOutboundElementsContainer) + c.Mutex = sync.Mutex{} + return c } -func (device *Device) PutInboundElementsSlice(s *[]*QueueInboundElement) { - for i := range *s { - (*s)[i] = nil +func (device *Device) PutOutboundElementsContainer(c *QueueOutboundElementsContainer) { + for i := range c.elems { + c.elems[i] = nil } - *s = (*s)[:0] - device.pool.inboundElementsSlice.Put(s) + c.elems = c.elems[:0] + device.pool.outboundElementsContainer.Put(c) } func (device *Device) GetMessageBuffer() *[MaxMessageSize]byte { diff --git a/device/receive.go b/device/receive.go index 744bf18..da663e9 100644 --- a/device/receive.go +++ b/device/receive.go @@ -27,7 +27,6 @@ type QueueHandshakeElement struct { } type QueueInboundElement struct { - sync.Mutex buffer *[MaxMessageSize]byte packet []byte counter uint64 @@ -35,6 +34,11 @@ type QueueInboundElement struct { endpoint conn.Endpoint } +type QueueInboundElementsContainer struct { + sync.Mutex + elems []*QueueInboundElement +} + // clearPointers clears elem fields that contain pointers. // This makes the garbage collector's life easier and // avoids accidentally keeping other objects around unnecessarily. @@ -87,7 +91,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive count int endpoints = make([]conn.Endpoint, maxBatchSize) deathSpiral int - elemsByPeer = make(map[*Peer]*[]*QueueInboundElement, maxBatchSize) + elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize) ) for i := range bufsArrs { @@ -170,15 +174,14 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive elem.keypair = keypair elem.endpoint = endpoints[i] elem.counter = 0 - elem.Mutex = sync.Mutex{} - elem.Lock() elemsForPeer, ok := elemsByPeer[peer] if !ok { - elemsForPeer = device.GetInboundElementsSlice() + elemsForPeer = device.GetInboundElementsContainer() + elemsForPeer.Lock() elemsByPeer[peer] = elemsForPeer } - *elemsForPeer = append(*elemsForPeer, elem) + elemsForPeer.elems = append(elemsForPeer.elems, elem) bufsArrs[i] = device.GetMessageBuffer() bufs[i] = bufsArrs[i][:] continue @@ -217,16 +220,16 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive default: } } - for peer, elems := range elemsByPeer { + for peer, elemsContainer := range elemsByPeer { if peer.isRunning.Load() { - peer.queue.inbound.c <- elems - device.queue.decryption.c <- elems + peer.queue.inbound.c <- elemsContainer + device.queue.decryption.c <- elemsContainer } else { - for _, elem := range *elems { + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutInboundElement(elem) } - device.PutInboundElementsSlice(elems) + device.PutInboundElementsContainer(elemsContainer) } delete(elemsByPeer, peer) } @@ -239,8 +242,8 @@ func (device *Device) RoutineDecryption(id int) { defer device.log.Verbosef("Routine: decryption worker %d - stopped", id) device.log.Verbosef("Routine: decryption worker %d - started", id) - for elems := range device.queue.decryption.c { - for _, elem := range *elems { + for elemsContainer := range device.queue.decryption.c { + for _, elem := range elemsContainer.elems { // split message into fields counter := elem.packet[MessageTransportOffsetCounter:MessageTransportOffsetContent] content := elem.packet[MessageTransportOffsetContent:] @@ -259,8 +262,8 @@ func (device *Device) RoutineDecryption(id int) { if err != nil { elem.packet = nil } - elem.Unlock() } + elemsContainer.Unlock() } } @@ -437,12 +440,12 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { bufs := make([][]byte, 0, maxBatchSize) - for elems := range peer.queue.inbound.c { - if elems == nil { + for elemsContainer := range peer.queue.inbound.c { + if elemsContainer == nil { return } - for _, elem := range *elems { - elem.Lock() + elemsContainer.Lock() + for _, elem := range elemsContainer.elems { if elem.packet == nil { // decryption failed continue @@ -515,11 +518,11 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { device.log.Errorf("Failed to write packets to TUN device: %v", err) } } - for _, elem := range *elems { + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutInboundElement(elem) } bufs = bufs[:0] - device.PutInboundElementsSlice(elems) + device.PutInboundElementsContainer(elemsContainer) } } diff --git a/device/send.go b/device/send.go index 7adfacf..95a5cbe 100644 --- a/device/send.go +++ b/device/send.go @@ -46,7 +46,6 @@ import ( */ type QueueOutboundElement struct { - sync.Mutex buffer *[MaxMessageSize]byte // slice holding the packet data packet []byte // slice of "buffer" (always!) nonce uint64 // nonce for encryption @@ -54,10 +53,14 @@ type QueueOutboundElement struct { peer *Peer // related peer } +type QueueOutboundElementsContainer struct { + sync.Mutex + elems []*QueueOutboundElement +} + func (device *Device) NewOutboundElement() *QueueOutboundElement { elem := device.GetOutboundElement() elem.buffer = device.GetMessageBuffer() - elem.Mutex = sync.Mutex{} elem.nonce = 0 // keypair and peer were cleared (if necessary) by clearPointers. return elem @@ -79,15 +82,15 @@ func (elem *QueueOutboundElement) clearPointers() { func (peer *Peer) SendKeepalive() { if len(peer.queue.staged) == 0 && peer.isRunning.Load() { elem := peer.device.NewOutboundElement() - elems := peer.device.GetOutboundElementsSlice() - *elems = append(*elems, elem) + elemsContainer := peer.device.GetOutboundElementsContainer() + elemsContainer.elems = append(elemsContainer.elems, elem) select { - case peer.queue.staged <- elems: + case peer.queue.staged <- elemsContainer: peer.device.log.Verbosef("%v - Sending keepalive packet", peer) default: peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundElement(elem) - peer.device.PutOutboundElementsSlice(elems) + peer.device.PutOutboundElementsContainer(elemsContainer) } } peer.SendStagedPackets() @@ -219,7 +222,7 @@ func (device *Device) RoutineReadFromTUN() { readErr error elems = make([]*QueueOutboundElement, batchSize) bufs = make([][]byte, batchSize) - elemsByPeer = make(map[*Peer]*[]*QueueOutboundElement, batchSize) + elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize) count = 0 sizes = make([]int, batchSize) offset = MessageTransportHeaderSize @@ -276,10 +279,10 @@ func (device *Device) RoutineReadFromTUN() { } elemsForPeer, ok := elemsByPeer[peer] if !ok { - elemsForPeer = device.GetOutboundElementsSlice() + elemsForPeer = device.GetOutboundElementsContainer() elemsByPeer[peer] = elemsForPeer } - *elemsForPeer = append(*elemsForPeer, elem) + elemsForPeer.elems = append(elemsForPeer.elems, elem) elems[i] = device.NewOutboundElement() bufs[i] = elems[i].buffer[:] } @@ -289,11 +292,11 @@ func (device *Device) RoutineReadFromTUN() { peer.StagePackets(elemsForPeer) peer.SendStagedPackets() } else { - for _, elem := range *elemsForPeer { + for _, elem := range elemsForPeer.elems { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } - device.PutOutboundElementsSlice(elemsForPeer) + device.PutOutboundElementsContainer(elemsForPeer) } delete(elemsByPeer, peer) } @@ -317,7 +320,7 @@ func (device *Device) RoutineReadFromTUN() { } } -func (peer *Peer) StagePackets(elems *[]*QueueOutboundElement) { +func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { for { select { case peer.queue.staged <- elems: @@ -326,11 +329,11 @@ func (peer *Peer) StagePackets(elems *[]*QueueOutboundElement) { } select { case tooOld := <-peer.queue.staged: - for _, elem := range *tooOld { + for _, elem := range tooOld.elems { peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } - peer.device.PutOutboundElementsSlice(tooOld) + peer.device.PutOutboundElementsContainer(tooOld) default: } } @@ -349,52 +352,52 @@ top: } for { - var elemsOOO *[]*QueueOutboundElement + var elemsContainerOOO *QueueOutboundElementsContainer select { - case elems := <-peer.queue.staged: + case elemsContainer := <-peer.queue.staged: i := 0 - for _, elem := range *elems { + for _, elem := range elemsContainer.elems { elem.peer = peer elem.nonce = keypair.sendNonce.Add(1) - 1 if elem.nonce >= RejectAfterMessages { keypair.sendNonce.Store(RejectAfterMessages) - if elemsOOO == nil { - elemsOOO = peer.device.GetOutboundElementsSlice() + if elemsContainerOOO == nil { + elemsContainerOOO = peer.device.GetOutboundElementsContainer() } - *elemsOOO = append(*elemsOOO, elem) + elemsContainerOOO.elems = append(elemsContainerOOO.elems, elem) continue } else { - (*elems)[i] = elem + elemsContainer.elems[i] = elem i++ } elem.keypair = keypair - elem.Lock() } - *elems = (*elems)[:i] + elemsContainer.Lock() + elemsContainer.elems = elemsContainer.elems[:i] - if elemsOOO != nil { - peer.StagePackets(elemsOOO) // XXX: Out of order, but we can't front-load go chans + if elemsContainerOOO != nil { + peer.StagePackets(elemsContainerOOO) // XXX: Out of order, but we can't front-load go chans } - if len(*elems) == 0 { - peer.device.PutOutboundElementsSlice(elems) + if len(elemsContainer.elems) == 0 { + peer.device.PutOutboundElementsContainer(elemsContainer) goto top } // add to parallel and sequential queue if peer.isRunning.Load() { - peer.queue.outbound.c <- elems - peer.device.queue.encryption.c <- elems + peer.queue.outbound.c <- elemsContainer + peer.device.queue.encryption.c <- elemsContainer } else { - for _, elem := range *elems { + for _, elem := range elemsContainer.elems { peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } - peer.device.PutOutboundElementsSlice(elems) + peer.device.PutOutboundElementsContainer(elemsContainer) } - if elemsOOO != nil { + if elemsContainerOOO != nil { goto top } default: @@ -406,12 +409,12 @@ top: func (peer *Peer) FlushStagedPackets() { for { select { - case elems := <-peer.queue.staged: - for _, elem := range *elems { + case elemsContainer := <-peer.queue.staged: + for _, elem := range elemsContainer.elems { peer.device.PutMessageBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } - peer.device.PutOutboundElementsSlice(elems) + peer.device.PutOutboundElementsContainer(elemsContainer) default: return } @@ -433,7 +436,7 @@ func calculatePaddingSize(packetSize, mtu int) int { return paddedSize - lastUnit } -/* Encrypts the elements in the queue +/* Encrypts the elems in the queue * and marks them for sequential consumption (by releasing the mutex) * * Obs. One instance per core @@ -445,8 +448,8 @@ func (device *Device) RoutineEncryption(id int) { defer device.log.Verbosef("Routine: encryption worker %d - stopped", id) device.log.Verbosef("Routine: encryption worker %d - started", id) - for elems := range device.queue.encryption.c { - for _, elem := range *elems { + for elemsContainer := range device.queue.encryption.c { + for _, elem := range elemsContainer.elems { // populate header fields header := elem.buffer[:MessageTransportHeaderSize] @@ -471,8 +474,8 @@ func (device *Device) RoutineEncryption(id int) { elem.packet, nil, ) - elem.Unlock() } + elemsContainer.Unlock() } } @@ -486,28 +489,28 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { bufs := make([][]byte, 0, maxBatchSize) - for elems := range peer.queue.outbound.c { + for elemsContainer := range peer.queue.outbound.c { bufs = bufs[:0] - if elems == nil { + if elemsContainer == nil { return } if !peer.isRunning.Load() { - // peer has been stopped; return re-usable elems to the shared pool. + // peer has been stopped; return re-usable elemsContainer 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. - for _, elem := range *elems { - elem.Lock() + elemsContainer.Lock() + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } continue } dataSent := false - for _, elem := range *elems { - elem.Lock() + elemsContainer.Lock() + for _, elem := range elemsContainer.elems { if len(elem.packet) != MessageKeepaliveSize { dataSent = true } @@ -521,11 +524,11 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { if dataSent { peer.timersDataSent() } - for _, elem := range *elems { + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } - device.PutOutboundElementsSlice(elems) + device.PutOutboundElementsContainer(elemsContainer) if err != nil { var errGSO conn.ErrUDPGSODisabled if errors.As(err, &errGSO) { From ec6f23b33e03f304681b0a440f318636d002a380 Mon Sep 17 00:00:00 2001 From: Adrian Dewhurst Date: Thu, 18 May 2023 10:09:53 -0400 Subject: [PATCH 008/173] tun: checksum tests and benchmarks Signed-off-by: Adrian Dewhurst --- tun/checksum_generic_test.go | 9 + tun/checksum_test.go | 577 ++++++++++++++++++++++++++++++++++- 2 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 tun/checksum_generic_test.go diff --git a/tun/checksum_generic_test.go b/tun/checksum_generic_test.go new file mode 100644 index 0000000..a0c9457 --- /dev/null +++ b/tun/checksum_generic_test.go @@ -0,0 +1,9 @@ +package tun + +var archChecksumFuncs = []archChecksumDetails{ + { + name: "generic", + available: true, + f: checksum, + }, +} diff --git a/tun/checksum_test.go b/tun/checksum_test.go index c1ccff5..b3a3583 100644 --- a/tun/checksum_test.go +++ b/tun/checksum_test.go @@ -2,33 +2,596 @@ package tun import ( "fmt" + "math" "math/rand" + "net/netip" + "sort" + "syscall" "testing" + "unsafe" + + "gvisor.dev/gvisor/pkg/tcpip" + gvisorChecksum "gvisor.dev/gvisor/pkg/tcpip/checksum" + "gvisor.dev/gvisor/pkg/tcpip/header" ) +type archChecksumDetails struct { + name string + available bool + f func([]byte, uint64) uint16 +} + +func deterministicRandomBytes(seed int64, length int) []byte { + rng := rand.New(rand.NewSource(seed)) + buf := make([]byte, length) + n, err := rng.Read(buf) + if err != nil { + panic(err) + } + if n != length { + panic("incomplete random buffer") + } + return buf +} + +func getPageAlignedRandomBytes(seed int64, length int) []byte { + alignment := syscall.Getpagesize() + buf := deterministicRandomBytes(seed, length+(alignment-1)) + bufPtr := uintptr(unsafe.Pointer(&buf[0])) + alignedBufPtr := (bufPtr + uintptr(alignment-1)) & ^uintptr(alignment-1) + alignedStart := int(alignedBufPtr - bufPtr) + return buf[alignedStart:] +} + +func TestChecksum(t *testing.T) { + alignedBuf := getPageAlignedRandomBytes(10, 8192) + allOnes := make([]byte, 65535) + for i := range allOnes { + allOnes[i] = 0xff + } + allFE := make([]byte, 65535) + for i := range allFE { + allFE[i] = 0xfe + } + + tests := []struct { + name string + data []byte + initial uint16 + want uint16 + }{ + { + name: "empty", + data: []byte{}, + initial: 0, + want: 0, + }, + { + name: "max initial", + data: []byte{}, + initial: math.MaxUint16, + want: 0xffff, + }, + { + name: "odd length", + data: []byte{0x01, 0x02, 0x01}, + initial: 0, + want: 0x0202, + }, + { + name: "tiny", + data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02}, + initial: 0, + want: 0x0306, + }, + { + name: "initial", + data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02}, + initial: 0x1000, + want: 0x1306, + }, + // cleanup0 through cleanup15 is 1024 (handled by large SIMD loops) + + // 32 (handled by small SIMD loops) + n, where n ranges from 0 to 15 + // to cover all of the leftover byte sizes that are possible after small + // SIMD loops that handle 16 bytes. + { + name: "cleanup0", + data: deterministicRandomBytes(1, 1056), + initial: 0, + want: 0x11ec, + }, + { + name: "cleanup1", + data: deterministicRandomBytes(1, 1057), + initial: 0, + want: 0xc5ec, + }, + { + name: "cleanup2", + data: deterministicRandomBytes(1, 1058), + initial: 0, + want: 0xc6ad, + }, + { + name: "cleanup3", + data: deterministicRandomBytes(1, 1059), + initial: 0, + want: 0x86ae, + }, + { + name: "cleanup4", + data: deterministicRandomBytes(1, 1060), + initial: 0, + want: 0x878e, + }, + { + name: "cleanup5", + data: deterministicRandomBytes(1, 1061), + initial: 0, + want: 0xdb8e, + }, + { + name: "cleanup6", + data: deterministicRandomBytes(1, 1062), + initial: 0, + want: 0xdbd5, + }, + { + name: "cleanup7", + data: deterministicRandomBytes(1, 1063), + initial: 0, + want: 0xcfd6, + }, + { + name: "cleanup8", + data: deterministicRandomBytes(1, 1064), + initial: 0, + want: 0xd090, + }, + { + name: "cleanup9", + data: deterministicRandomBytes(1, 1065), + initial: 0, + want: 0x0791, + }, + { + name: "cleanup10", + data: deterministicRandomBytes(1, 1066), + initial: 0, + want: 0x079f, + }, + { + name: "cleanup11", + data: deterministicRandomBytes(1, 1067), + initial: 0, + want: 0xba9f, + }, + { + name: "cleanup12", + data: deterministicRandomBytes(1, 1068), + initial: 0, + want: 0xbb0c, + }, + { + name: "cleanup13", + data: deterministicRandomBytes(1, 1069), + initial: 0, + want: 0x770d, + }, + { + name: "cleanup14", + data: deterministicRandomBytes(1, 1070), + initial: 0, + want: 0x780a, + }, + { + name: "cleanup15", + data: deterministicRandomBytes(1, 1071), + initial: 0, + want: 0x640b, + }, + // small1 through small15 covers small sizes that are not large enough + // to do overlapped reads. + { + name: "small1", + data: deterministicRandomBytes(2, 1), + initial: 0x1122, + want: 0x4022, + }, + { + name: "small2", + data: deterministicRandomBytes(2, 2), + initial: 0x1122, + want: 0x40a4, + }, + { + name: "small3", + data: deterministicRandomBytes(2, 3), + initial: 0x1122, + want: 0xc2a4, + }, + { + name: "small4", + data: deterministicRandomBytes(2, 4), + initial: 0x1122, + want: 0xc36f, + }, + { + name: "small5", + data: deterministicRandomBytes(2, 5), + initial: 0x1122, + want: 0xa570, + }, + { + name: "small6", + data: deterministicRandomBytes(2, 6), + initial: 0x1122, + want: 0xa669, + }, + { + name: "small7", + data: deterministicRandomBytes(2, 7), + initial: 0x1122, + want: 0x0f6a, + }, + { + name: "small8", + data: deterministicRandomBytes(2, 8), + initial: 0x1122, + want: 0x0fd9, + }, + { + name: "small9", + data: deterministicRandomBytes(2, 9), + initial: 0x1122, + want: 0x40d9, + }, + { + name: "small10", + data: deterministicRandomBytes(2, 10), + initial: 0x1122, + want: 0x411d, + }, + { + name: "small11", + data: deterministicRandomBytes(2, 11), + initial: 0x1122, + want: 0x011e, + }, + { + name: "small12", + data: deterministicRandomBytes(2, 12), + initial: 0x1122, + want: 0x01c8, + }, + { + name: "small13", + data: deterministicRandomBytes(2, 13), + initial: 0x1122, + want: 0x4dc8, + }, + { + name: "small14", + data: deterministicRandomBytes(2, 14), + initial: 0x1122, + want: 0x4eb5, + }, + { + name: "small15", + data: deterministicRandomBytes(2, 15), + initial: 0x1122, + want: 0xa4b5, + }, + // other small-ish sizes + { + name: "small16", + data: deterministicRandomBytes(1, 16), + initial: 0, + want: 0x02fa, + }, + { + name: "small32", + data: deterministicRandomBytes(1, 32), + initial: 0, + want: 0x03ee, + }, + { + name: "small64", + data: deterministicRandomBytes(1, 64), + initial: 0, + want: 0x3f85, + }, + { + name: "medium", + data: deterministicRandomBytes(1, 1400), + initial: 0, + want: 0xbea5, + }, + { + name: "big", + data: deterministicRandomBytes(2, 65000), + initial: 0, + want: 0x3ba7, + }, + { + name: "big-initial", + data: deterministicRandomBytes(2, 65000), + initial: 0x1234, + want: 0x4ddb, + }, + { + // big-small-loop is intended to exercise a few iterations of a big + // initial loop of 128 bytes or larger + a smaller loop of 16 bytes + // + some leftover + name: "big-small-loop", + data: deterministicRandomBytes(3, 1094), + initial: 0x9999, + want: 0xe65b, + }, + { + name: "page-aligned", + data: alignedBuf[:4096], + initial: 0, + want: 0x963b, + }, + { + name: "32-aligned", + data: alignedBuf[32:4128], + initial: 0, + want: 0x30c4, + }, + { + name: "16-aligned", + data: alignedBuf[16:4112], + initial: 0, + want: 0xaeff, + }, + { + name: "8-aligned", + data: alignedBuf[8:4104], + initial: 0, + want: 0x6c3b, + }, + { + name: "4-aligned", + data: alignedBuf[4:4100], + initial: 0, + want: 0x2e4a, + }, + { + name: "2-aligned", + data: alignedBuf[2:4098], + initial: 0, + want: 0xc702, + }, + { + name: "unaligned", + data: alignedBuf[1:4097], + initial: 0, + want: 0x3bc7, + }, + { + name: "unalignedAndOdd", + data: alignedBuf[1:4096], + initial: 0, + want: 0x3b13, + }, + { + name: "fe1282", + data: allFE[:1282], + initial: 0, + want: 0x7c7c, + }, + { + name: "fe", + data: allFE, + initial: 0, + want: 0x7e81, + }, + { + name: "maximum", + data: allOnes, + initial: 0, + want: 0xff00, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, fd := range archChecksumFuncs { + t.Run(fd.name, func(t *testing.T) { + if !fd.available { + t.Skip("can not run on this system") + } + if got := fd.f(tt.data, uint64(tt.initial)); got != tt.want { + t.Errorf("%s checksum = %04x, want %04x", fd.name, got, tt.want) + } + }) + } + t.Run("reference", func(t *testing.T) { + if got := gvisorChecksum.Checksum(tt.data, tt.initial); got != tt.want { + t.Errorf("reference checksum = %04x, want %04x", got, tt.want) + } + }) + }) + } +} + +func TestPseudoHeaderChecksumNoFold(t *testing.T) { + tests := []struct { + name string + protocol uint8 + srcAddr []byte + dstAddr []byte + totalLen uint16 + want uint16 + }{ + { + name: "ipv4", + protocol: syscall.IPPROTO_TCP, + srcAddr: netip.MustParseAddr("192.168.1.1").AsSlice(), + dstAddr: netip.MustParseAddr("192.168.1.2").AsSlice(), + totalLen: 1492, + want: 0x892e, + }, + { + name: "ipv6", + protocol: syscall.IPPROTO_TCP, + srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(), + dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(), + totalLen: 1492, + want: 0x947f, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotNoFold := pseudoHeaderChecksumNoFold(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) + got := checksum([]byte{}, gotNoFold) + if got != tt.want { + t.Errorf("pseudoHeaderChecksumNoFold() = %x, folds to %04x, want %04x", gotNoFold, got, tt.want) + } + + got = header.PseudoHeaderChecksum( + tcpip.TransportProtocolNumber(tt.protocol), + tcpip.AddrFromSlice(tt.srcAddr), + tcpip.AddrFromSlice(tt.dstAddr), + tt.totalLen) + if got != tt.want { + t.Errorf("header.PseudoHeaderChecksum() = %04x, want %04x", got, tt.want) + } + }) + } +} + +func FuzzChecksum(f *testing.F) { + buf := getPageAlignedRandomBytes(1234, 65536) + + f.Add([]byte{}, uint16(0)) + f.Add([]byte{}, uint16(0x1234)) + f.Add([]byte{}, uint16(0)) + f.Add(buf[:15], uint16(0x1234)) + f.Add(buf[:256], uint16(0x1234)) + f.Add(buf[:1280], uint16(0x1234)) + f.Add(buf[:1288], uint16(0x1234)) + f.Add(buf[1:1050], uint16(0x1234)) + + f.Fuzz(func(t *testing.T, data []byte, initial uint16) { + want := gvisorChecksum.Checksum(data, initial) + + for _, fd := range archChecksumFuncs { + t.Run(fd.name, func(t *testing.T) { + if !fd.available { + t.Skip("can not run on this system") + } + if got := fd.f(data, uint64(initial)); got != want { + t.Errorf("%s checksum = %04x, want %04x", fd.name, got, want) + } + }) + } + }) +} + +var result uint16 +var result64 uint64 + func BenchmarkChecksum(b *testing.B) { + offsets := []int{ // offsets from page alignment + 0, + 1, + 2, + 4, + 8, + 16, + } lengths := []int{ + 0, + 7, + 15, + 16, + 31, 64, + 90, + 95, 128, 256, 512, 1024, + 1240, 1500, 2048, 4096, 8192, 9000, 9001, + 16384, + 65536, } + if !sort.IntsAreSorted(offsets) { + b.Fatal("offsets are not sorted") + } + largestLength := lengths[len(lengths)-1] + if !sort.IntsAreSorted(lengths) { + b.Fatal("lengths are not sorted") + } + largestOffset := lengths[len(offsets)-1] + alignedBuf := getPageAlignedRandomBytes(1, largestOffset+largestLength) + var r uint16 + for _, offset := range offsets { + name := fmt.Sprintf("%vAligned", offset) + if offset == 0 { + name = "pageAligned" + } + offsetBuf := alignedBuf[offset:] + b.Run(name, func(b *testing.B) { + for _, length := range lengths { + b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { + for _, fd := range archChecksumFuncs { + b.Run(fd.name, func(b *testing.B) { + if !fd.available { + b.Skip("can not run on this system") + } + b.SetBytes(int64(length)) + for i := 0; i < b.N; i++ { + r += fd.f(offsetBuf[:length], 0) + } + }) + } + }) + } + }) + } + result = r +} - for _, length := range lengths { - b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { - buf := make([]byte, length) - rng := rand.New(rand.NewSource(1)) - rng.Read(buf) - b.ResetTimer() +func BenchmarkPseudoHeaderChecksum(b *testing.B) { + tests := []struct { + name string + protocol uint8 + srcAddr []byte + dstAddr []byte + totalLen uint16 + want uint16 + }{ + { + name: "ipv4", + protocol: syscall.IPPROTO_TCP, + srcAddr: []byte{192, 168, 1, 1}, + dstAddr: []byte{192, 168, 1, 2}, + totalLen: 1492, + want: 0x892e, + }, + { + name: "ipv6", + protocol: syscall.IPPROTO_TCP, + srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(), + dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(), + totalLen: 1492, + want: 0x892e, + }, + } + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { for i := 0; i < b.N; i++ { - checksum(buf, 0) + result64 += pseudoHeaderChecksumNoFold(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) } }) } From 88b11b4a0dde05ecd823e896c198014b1652e7c9 Mon Sep 17 00:00:00 2001 From: Adrian Dewhurst Date: Thu, 30 Mar 2023 21:48:43 -0400 Subject: [PATCH 009/173] tun: AMD64 optimized checksum This adds AMD64 assembly implementations of IP checksum computation, one for baseline AMD64 and the other for v3 AMD64 (AVX2 and BMI2). All performance numbers reported are from a Ryzen 7 4750U but similar improvements are expected for a wide range of processors. The generic IP checksum implementation has also been further improved to be significantly faster using bits.AddUint64 (for a 64KiB buffer the throughput improves from 15,000MiB/s to 27,600MiB/s; similar gains are also reported on ARM64 but I do not have specific numbers). The baseline AMD64 implementation for a 64KiB buffer reports 32,700MiB/s and the AVX2 implementation is slightly over 107,000MiB/s. Unfortunately, for very small sizes (e.g. the expected size for an IPv4 header) setting up SIMD computation involves some overhead that makes computing a checksum for small buffers slower than a non-SIMD implementation. Even more unfortunately, testing for this at runtimen in Go and calling a func optimized for small buffers mitigates most of the improvement due to call overhead. The break even point is around 256 byte buffers; IPv4 headers are no more than 60 bytes including extensions. IPv6 headers do not have a checksum but are a fixed size of 40 bytes. As a result, the generated assembly code uses an alternate approach for buffers of less than 256 bytes. Additionally, buffers of less than 32 bytes need to be handled specially because the strategy for reading buffers that are not a multiple of 8 bytes fails when the buffer is too small. As suggested by additional benchmarking, pseudo header computation has been rewritten to be faster (benchmark time reduced by 1/2 to 1/4). Updates tailscale/corp#9755 Signed-off-by: Adrian Dewhurst --- tun/checksum.go | 764 ++++++++++++++++++++++++---- tun/checksum_amd64.go | 20 + tun/checksum_amd64_test.go | 45 ++ tun/checksum_generated_amd64.go | 18 + tun/checksum_generated_amd64.s | 851 ++++++++++++++++++++++++++++++++ tun/checksum_generic.go | 15 + tun/checksum_generic_test.go | 21 +- tun/checksum_test.go | 56 ++- tun/generate_amd64.go | 579 ++++++++++++++++++++++ tun/tcp_offload_linux.go | 12 +- 10 files changed, 2266 insertions(+), 115 deletions(-) create mode 100644 tun/checksum_amd64.go create mode 100644 tun/checksum_amd64_test.go create mode 100644 tun/checksum_generated_amd64.go create mode 100644 tun/checksum_generated_amd64.s create mode 100644 tun/checksum_generic.go create mode 100644 tun/generate_amd64.go diff --git a/tun/checksum.go b/tun/checksum.go index 29a8fc8..ee3f359 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -1,118 +1,710 @@ package tun -import "encoding/binary" +import ( + "encoding/binary" + "math/bits" + "strconv" -// TODO: Explore SIMD and/or other assembly optimizations. -// TODO: Test native endian loads. See RFC 1071 section 2 part B. -func checksumNoFold(b []byte, initial uint64) uint64 { - ac := initial + "golang.org/x/sys/cpu" +) + +// checksumGeneric64 is a reference implementation of checksum using 64 bit +// arithmetic for use in testing or when an architecture-specific implementation +// is not available. +func checksumGeneric64(b []byte, initial uint16) uint16 { + var ac uint64 + var carry uint64 + + if cpu.IsBigEndian { + ac = uint64(initial) + } else { + ac = uint64(bits.ReverseBytes16(initial)) + } for len(b) >= 128 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) - ac += uint64(binary.BigEndian.Uint32(b[64:68])) - ac += uint64(binary.BigEndian.Uint32(b[68:72])) - ac += uint64(binary.BigEndian.Uint32(b[72:76])) - ac += uint64(binary.BigEndian.Uint32(b[76:80])) - ac += uint64(binary.BigEndian.Uint32(b[80:84])) - ac += uint64(binary.BigEndian.Uint32(b[84:88])) - ac += uint64(binary.BigEndian.Uint32(b[88:92])) - ac += uint64(binary.BigEndian.Uint32(b[92:96])) - ac += uint64(binary.BigEndian.Uint32(b[96:100])) - ac += uint64(binary.BigEndian.Uint32(b[100:104])) - ac += uint64(binary.BigEndian.Uint32(b[104:108])) - ac += uint64(binary.BigEndian.Uint32(b[108:112])) - ac += uint64(binary.BigEndian.Uint32(b[112:116])) - ac += uint64(binary.BigEndian.Uint32(b[116:120])) - ac += uint64(binary.BigEndian.Uint32(b[120:124])) - ac += uint64(binary.BigEndian.Uint32(b[124:128])) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[56:64]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[64:72]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[72:80]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[80:88]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[88:96]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[96:104]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[104:112]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[112:120]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[120:128]), carry) + } else { + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[56:64]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[64:72]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[72:80]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[80:88]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[88:96]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[96:104]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[104:112]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[112:120]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[120:128]), carry) + } b = b[128:] } if len(b) >= 64 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[56:64]), carry) + } else { + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[56:64]), carry) + } b = b[64:] } if len(b) >= 32 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[24:32]), carry) + } else { + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[24:32]), carry) + } b = b[32:] } if len(b) >= 16 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b[8:16]), carry) + } else { + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[:8]), carry) + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b[8:16]), carry) + } b = b[16:] } if len(b) >= 8 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, binary.BigEndian.Uint64(b), carry) + } else { + ac, carry = bits.Add64(ac, binary.LittleEndian.Uint64(b), carry) + } b = b[8:] } if len(b) >= 4 { - ac += uint64(binary.BigEndian.Uint32(b)) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, uint64(binary.BigEndian.Uint32(b)), carry) + } else { + ac, carry = bits.Add64(ac, uint64(binary.LittleEndian.Uint32(b)), carry) + } b = b[4:] } if len(b) >= 2 { - ac += uint64(binary.BigEndian.Uint16(b)) + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, uint64(binary.BigEndian.Uint16(b)), carry) + } else { + ac, carry = bits.Add64(ac, uint64(binary.LittleEndian.Uint16(b)), carry) + } b = b[2:] } - if len(b) == 1 { - ac += uint64(b[0]) << 8 + if len(b) >= 1 { + if cpu.IsBigEndian { + ac, carry = bits.Add64(ac, uint64(b[0])<<8, carry) + } else { + ac, carry = bits.Add64(ac, uint64(b[0]), carry) + } } - return ac + folded := ipChecksumFold64(ac, carry) + if !cpu.IsBigEndian { + folded = bits.ReverseBytes16(folded) + } + return folded } -func checksum(b []byte, initial uint64) uint16 { - ac := checksumNoFold(b, initial) - ac = (ac >> 16) + (ac & 0xffff) - ac = (ac >> 16) + (ac & 0xffff) - ac = (ac >> 16) + (ac & 0xffff) - ac = (ac >> 16) + (ac & 0xffff) - return uint16(ac) +// checksumGeneric32 is a reference implementation of checksum using 32 bit +// arithmetic for use in testing or when an architecture-specific implementation +// is not available. +func checksumGeneric32(b []byte, initial uint16) uint16 { + var ac uint32 + var carry uint32 + + if cpu.IsBigEndian { + ac = uint32(initial) + } else { + ac = uint32(bits.ReverseBytes16(initial)) + } + + for len(b) >= 64 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[:8]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[12:16]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[16:20]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[20:24]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[24:28]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[28:32]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[32:36]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[36:40]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[40:44]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[44:48]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[48:52]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[52:56]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[56:60]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[60:64]), carry) + } else { + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[:8]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[12:16]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[16:20]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[20:24]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[24:28]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[28:32]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[32:36]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[36:40]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[40:44]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[44:48]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[48:52]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[52:56]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[56:60]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[60:64]), carry) + } + b = b[64:] + } + if len(b) >= 32 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[12:16]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[16:20]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[20:24]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[24:28]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[28:32]), carry) + } else { + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[12:16]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[16:20]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[20:24]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[24:28]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[28:32]), carry) + } + b = b[32:] + } + if len(b) >= 16 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[12:16]), carry) + } else { + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[4:8]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[8:12]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[12:16]), carry) + } + b = b[16:] + } + if len(b) >= 8 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b[4:8]), carry) + } else { + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[:4]), carry) + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b[4:8]), carry) + } + b = b[8:] + } + if len(b) >= 4 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, binary.BigEndian.Uint32(b), carry) + } else { + ac, carry = bits.Add32(ac, binary.LittleEndian.Uint32(b), carry) + } + b = b[4:] + } + if len(b) >= 2 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, uint32(binary.BigEndian.Uint16(b)), carry) + } else { + ac, carry = bits.Add32(ac, uint32(binary.LittleEndian.Uint16(b)), carry) + } + b = b[2:] + } + if len(b) >= 1 { + if cpu.IsBigEndian { + ac, carry = bits.Add32(ac, uint32(b[0])<<8, carry) + } else { + ac, carry = bits.Add32(ac, uint32(b[0]), carry) + } + } + + folded := ipChecksumFold32(ac, carry) + if !cpu.IsBigEndian { + folded = bits.ReverseBytes16(folded) + } + return folded } -func pseudoHeaderChecksumNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint64 { - sum := checksumNoFold(srcAddr, 0) - sum = checksumNoFold(dstAddr, sum) - sum = checksumNoFold([]byte{0, protocol}, sum) - tmp := make([]byte, 2) - binary.BigEndian.PutUint16(tmp, totalLen) - return checksumNoFold(tmp, sum) +// checksumGeneric32Alternate is an alternate reference implementation of +// checksum using 32 bit arithmetic for use in testing or when an +// architecture-specific implementation is not available. +func checksumGeneric32Alternate(b []byte, initial uint16) uint16 { + var ac uint32 + + if cpu.IsBigEndian { + ac = uint32(initial) + } else { + ac = uint32(bits.ReverseBytes16(initial)) + } + + for len(b) >= 64 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b[:2])) + ac += uint32(binary.BigEndian.Uint16(b[2:4])) + ac += uint32(binary.BigEndian.Uint16(b[4:6])) + ac += uint32(binary.BigEndian.Uint16(b[6:8])) + ac += uint32(binary.BigEndian.Uint16(b[8:10])) + ac += uint32(binary.BigEndian.Uint16(b[10:12])) + ac += uint32(binary.BigEndian.Uint16(b[12:14])) + ac += uint32(binary.BigEndian.Uint16(b[14:16])) + ac += uint32(binary.BigEndian.Uint16(b[16:18])) + ac += uint32(binary.BigEndian.Uint16(b[18:20])) + ac += uint32(binary.BigEndian.Uint16(b[20:22])) + ac += uint32(binary.BigEndian.Uint16(b[22:24])) + ac += uint32(binary.BigEndian.Uint16(b[24:26])) + ac += uint32(binary.BigEndian.Uint16(b[26:28])) + ac += uint32(binary.BigEndian.Uint16(b[28:30])) + ac += uint32(binary.BigEndian.Uint16(b[30:32])) + ac += uint32(binary.BigEndian.Uint16(b[32:34])) + ac += uint32(binary.BigEndian.Uint16(b[34:36])) + ac += uint32(binary.BigEndian.Uint16(b[36:38])) + ac += uint32(binary.BigEndian.Uint16(b[38:40])) + ac += uint32(binary.BigEndian.Uint16(b[40:42])) + ac += uint32(binary.BigEndian.Uint16(b[42:44])) + ac += uint32(binary.BigEndian.Uint16(b[44:46])) + ac += uint32(binary.BigEndian.Uint16(b[46:48])) + ac += uint32(binary.BigEndian.Uint16(b[48:50])) + ac += uint32(binary.BigEndian.Uint16(b[50:52])) + ac += uint32(binary.BigEndian.Uint16(b[52:54])) + ac += uint32(binary.BigEndian.Uint16(b[54:56])) + ac += uint32(binary.BigEndian.Uint16(b[56:58])) + ac += uint32(binary.BigEndian.Uint16(b[58:60])) + ac += uint32(binary.BigEndian.Uint16(b[60:62])) + ac += uint32(binary.BigEndian.Uint16(b[62:64])) + } else { + ac += uint32(binary.LittleEndian.Uint16(b[:2])) + ac += uint32(binary.LittleEndian.Uint16(b[2:4])) + ac += uint32(binary.LittleEndian.Uint16(b[4:6])) + ac += uint32(binary.LittleEndian.Uint16(b[6:8])) + ac += uint32(binary.LittleEndian.Uint16(b[8:10])) + ac += uint32(binary.LittleEndian.Uint16(b[10:12])) + ac += uint32(binary.LittleEndian.Uint16(b[12:14])) + ac += uint32(binary.LittleEndian.Uint16(b[14:16])) + ac += uint32(binary.LittleEndian.Uint16(b[16:18])) + ac += uint32(binary.LittleEndian.Uint16(b[18:20])) + ac += uint32(binary.LittleEndian.Uint16(b[20:22])) + ac += uint32(binary.LittleEndian.Uint16(b[22:24])) + ac += uint32(binary.LittleEndian.Uint16(b[24:26])) + ac += uint32(binary.LittleEndian.Uint16(b[26:28])) + ac += uint32(binary.LittleEndian.Uint16(b[28:30])) + ac += uint32(binary.LittleEndian.Uint16(b[30:32])) + ac += uint32(binary.LittleEndian.Uint16(b[32:34])) + ac += uint32(binary.LittleEndian.Uint16(b[34:36])) + ac += uint32(binary.LittleEndian.Uint16(b[36:38])) + ac += uint32(binary.LittleEndian.Uint16(b[38:40])) + ac += uint32(binary.LittleEndian.Uint16(b[40:42])) + ac += uint32(binary.LittleEndian.Uint16(b[42:44])) + ac += uint32(binary.LittleEndian.Uint16(b[44:46])) + ac += uint32(binary.LittleEndian.Uint16(b[46:48])) + ac += uint32(binary.LittleEndian.Uint16(b[48:50])) + ac += uint32(binary.LittleEndian.Uint16(b[50:52])) + ac += uint32(binary.LittleEndian.Uint16(b[52:54])) + ac += uint32(binary.LittleEndian.Uint16(b[54:56])) + ac += uint32(binary.LittleEndian.Uint16(b[56:58])) + ac += uint32(binary.LittleEndian.Uint16(b[58:60])) + ac += uint32(binary.LittleEndian.Uint16(b[60:62])) + ac += uint32(binary.LittleEndian.Uint16(b[62:64])) + } + b = b[64:] + } + if len(b) >= 32 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b[:2])) + ac += uint32(binary.BigEndian.Uint16(b[2:4])) + ac += uint32(binary.BigEndian.Uint16(b[4:6])) + ac += uint32(binary.BigEndian.Uint16(b[6:8])) + ac += uint32(binary.BigEndian.Uint16(b[8:10])) + ac += uint32(binary.BigEndian.Uint16(b[10:12])) + ac += uint32(binary.BigEndian.Uint16(b[12:14])) + ac += uint32(binary.BigEndian.Uint16(b[14:16])) + ac += uint32(binary.BigEndian.Uint16(b[16:18])) + ac += uint32(binary.BigEndian.Uint16(b[18:20])) + ac += uint32(binary.BigEndian.Uint16(b[20:22])) + ac += uint32(binary.BigEndian.Uint16(b[22:24])) + ac += uint32(binary.BigEndian.Uint16(b[24:26])) + ac += uint32(binary.BigEndian.Uint16(b[26:28])) + ac += uint32(binary.BigEndian.Uint16(b[28:30])) + ac += uint32(binary.BigEndian.Uint16(b[30:32])) + } else { + ac += uint32(binary.LittleEndian.Uint16(b[:2])) + ac += uint32(binary.LittleEndian.Uint16(b[2:4])) + ac += uint32(binary.LittleEndian.Uint16(b[4:6])) + ac += uint32(binary.LittleEndian.Uint16(b[6:8])) + ac += uint32(binary.LittleEndian.Uint16(b[8:10])) + ac += uint32(binary.LittleEndian.Uint16(b[10:12])) + ac += uint32(binary.LittleEndian.Uint16(b[12:14])) + ac += uint32(binary.LittleEndian.Uint16(b[14:16])) + ac += uint32(binary.LittleEndian.Uint16(b[16:18])) + ac += uint32(binary.LittleEndian.Uint16(b[18:20])) + ac += uint32(binary.LittleEndian.Uint16(b[20:22])) + ac += uint32(binary.LittleEndian.Uint16(b[22:24])) + ac += uint32(binary.LittleEndian.Uint16(b[24:26])) + ac += uint32(binary.LittleEndian.Uint16(b[26:28])) + ac += uint32(binary.LittleEndian.Uint16(b[28:30])) + ac += uint32(binary.LittleEndian.Uint16(b[30:32])) + } + b = b[32:] + } + if len(b) >= 16 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b[:2])) + ac += uint32(binary.BigEndian.Uint16(b[2:4])) + ac += uint32(binary.BigEndian.Uint16(b[4:6])) + ac += uint32(binary.BigEndian.Uint16(b[6:8])) + ac += uint32(binary.BigEndian.Uint16(b[8:10])) + ac += uint32(binary.BigEndian.Uint16(b[10:12])) + ac += uint32(binary.BigEndian.Uint16(b[12:14])) + ac += uint32(binary.BigEndian.Uint16(b[14:16])) + } else { + ac += uint32(binary.LittleEndian.Uint16(b[:2])) + ac += uint32(binary.LittleEndian.Uint16(b[2:4])) + ac += uint32(binary.LittleEndian.Uint16(b[4:6])) + ac += uint32(binary.LittleEndian.Uint16(b[6:8])) + ac += uint32(binary.LittleEndian.Uint16(b[8:10])) + ac += uint32(binary.LittleEndian.Uint16(b[10:12])) + ac += uint32(binary.LittleEndian.Uint16(b[12:14])) + ac += uint32(binary.LittleEndian.Uint16(b[14:16])) + } + b = b[16:] + } + if len(b) >= 8 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b[:2])) + ac += uint32(binary.BigEndian.Uint16(b[2:4])) + ac += uint32(binary.BigEndian.Uint16(b[4:6])) + ac += uint32(binary.BigEndian.Uint16(b[6:8])) + } else { + ac += uint32(binary.LittleEndian.Uint16(b[:2])) + ac += uint32(binary.LittleEndian.Uint16(b[2:4])) + ac += uint32(binary.LittleEndian.Uint16(b[4:6])) + ac += uint32(binary.LittleEndian.Uint16(b[6:8])) + } + b = b[8:] + } + if len(b) >= 4 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b[:2])) + ac += uint32(binary.BigEndian.Uint16(b[2:4])) + } else { + ac += uint32(binary.LittleEndian.Uint16(b[:2])) + ac += uint32(binary.LittleEndian.Uint16(b[2:4])) + } + b = b[4:] + } + if len(b) >= 2 { + if cpu.IsBigEndian { + ac += uint32(binary.BigEndian.Uint16(b)) + } else { + ac += uint32(binary.LittleEndian.Uint16(b)) + } + b = b[2:] + } + if len(b) >= 1 { + if cpu.IsBigEndian { + ac += uint32(b[0]) << 8 + } else { + ac += uint32(b[0]) + } + } + + folded := ipChecksumFold32(ac, 0) + if !cpu.IsBigEndian { + folded = bits.ReverseBytes16(folded) + } + return folded +} + +// checksumGeneric64Alternate is an alternate reference implementation of +// checksum using 64 bit arithmetic for use in testing or when an +// architecture-specific implementation is not available. +func checksumGeneric64Alternate(b []byte, initial uint16) uint16 { + var ac uint64 + + if cpu.IsBigEndian { + ac = uint64(initial) + } else { + ac = uint64(bits.ReverseBytes16(initial)) + } + + for len(b) >= 64 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac += uint64(binary.BigEndian.Uint32(b[16:20])) + ac += uint64(binary.BigEndian.Uint32(b[20:24])) + ac += uint64(binary.BigEndian.Uint32(b[24:28])) + ac += uint64(binary.BigEndian.Uint32(b[28:32])) + ac += uint64(binary.BigEndian.Uint32(b[32:36])) + ac += uint64(binary.BigEndian.Uint32(b[36:40])) + ac += uint64(binary.BigEndian.Uint32(b[40:44])) + ac += uint64(binary.BigEndian.Uint32(b[44:48])) + ac += uint64(binary.BigEndian.Uint32(b[48:52])) + ac += uint64(binary.BigEndian.Uint32(b[52:56])) + ac += uint64(binary.BigEndian.Uint32(b[56:60])) + ac += uint64(binary.BigEndian.Uint32(b[60:64])) + } else { + ac += uint64(binary.LittleEndian.Uint32(b[:4])) + ac += uint64(binary.LittleEndian.Uint32(b[4:8])) + ac += uint64(binary.LittleEndian.Uint32(b[8:12])) + ac += uint64(binary.LittleEndian.Uint32(b[12:16])) + ac += uint64(binary.LittleEndian.Uint32(b[16:20])) + ac += uint64(binary.LittleEndian.Uint32(b[20:24])) + ac += uint64(binary.LittleEndian.Uint32(b[24:28])) + ac += uint64(binary.LittleEndian.Uint32(b[28:32])) + ac += uint64(binary.LittleEndian.Uint32(b[32:36])) + ac += uint64(binary.LittleEndian.Uint32(b[36:40])) + ac += uint64(binary.LittleEndian.Uint32(b[40:44])) + ac += uint64(binary.LittleEndian.Uint32(b[44:48])) + ac += uint64(binary.LittleEndian.Uint32(b[48:52])) + ac += uint64(binary.LittleEndian.Uint32(b[52:56])) + ac += uint64(binary.LittleEndian.Uint32(b[56:60])) + ac += uint64(binary.LittleEndian.Uint32(b[60:64])) + } + b = b[64:] + } + if len(b) >= 32 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac += uint64(binary.BigEndian.Uint32(b[16:20])) + ac += uint64(binary.BigEndian.Uint32(b[20:24])) + ac += uint64(binary.BigEndian.Uint32(b[24:28])) + ac += uint64(binary.BigEndian.Uint32(b[28:32])) + } else { + ac += uint64(binary.LittleEndian.Uint32(b[:4])) + ac += uint64(binary.LittleEndian.Uint32(b[4:8])) + ac += uint64(binary.LittleEndian.Uint32(b[8:12])) + ac += uint64(binary.LittleEndian.Uint32(b[12:16])) + ac += uint64(binary.LittleEndian.Uint32(b[16:20])) + ac += uint64(binary.LittleEndian.Uint32(b[20:24])) + ac += uint64(binary.LittleEndian.Uint32(b[24:28])) + ac += uint64(binary.LittleEndian.Uint32(b[28:32])) + } + b = b[32:] + } + if len(b) >= 16 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac += uint64(binary.BigEndian.Uint32(b[8:12])) + ac += uint64(binary.BigEndian.Uint32(b[12:16])) + } else { + ac += uint64(binary.LittleEndian.Uint32(b[:4])) + ac += uint64(binary.LittleEndian.Uint32(b[4:8])) + ac += uint64(binary.LittleEndian.Uint32(b[8:12])) + ac += uint64(binary.LittleEndian.Uint32(b[12:16])) + } + b = b[16:] + } + if len(b) >= 8 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint32(b[:4])) + ac += uint64(binary.BigEndian.Uint32(b[4:8])) + } else { + ac += uint64(binary.LittleEndian.Uint32(b[:4])) + ac += uint64(binary.LittleEndian.Uint32(b[4:8])) + } + b = b[8:] + } + if len(b) >= 4 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint32(b)) + } else { + ac += uint64(binary.LittleEndian.Uint32(b)) + } + b = b[4:] + } + if len(b) >= 2 { + if cpu.IsBigEndian { + ac += uint64(binary.BigEndian.Uint16(b)) + } else { + ac += uint64(binary.LittleEndian.Uint16(b)) + } + b = b[2:] + } + if len(b) >= 1 { + if cpu.IsBigEndian { + ac += uint64(b[0]) << 8 + } else { + ac += uint64(b[0]) + } + } + + folded := ipChecksumFold64(ac, 0) + if !cpu.IsBigEndian { + folded = bits.ReverseBytes16(folded) + } + return folded +} + +func ipChecksumFold64(unfolded uint64, initialCarry uint64) uint16 { + sum, carry := bits.Add32(uint32(unfolded>>32), uint32(unfolded&0xffff_ffff), uint32(initialCarry)) + // if carry != 0, sum <= 0xffff_fffe, otherwise sum <= 0xffff_ffff + // therefore (sum >> 16) + (sum & 0xffff) + carry <= 0x1_fffe; so there is + // no need to save the carry flag + sum = (sum >> 16) + (sum & 0xffff) + carry + // sum <= 0x1_fffe therefore this is the last fold needed: + // if (sum >> 16) > 0 then + // (sum >> 16) == 1 && (sum & 0xffff) <= 0xfffe and therefore + // the addition will not overflow + // otherwise (sum >> 16) == 0 and sum will be unchanged + sum = (sum >> 16) + (sum & 0xffff) + return uint16(sum) +} + +func ipChecksumFold32(unfolded uint32, initialCarry uint32) uint16 { + sum := (unfolded >> 16) + (unfolded & 0xffff) + initialCarry + // sum <= 0x1_ffff: + // 0xffff + 0xffff = 0x1_fffe + // initialCarry is 0 or 1, for a combined maximum of 0x1_ffff + sum = (sum >> 16) + (sum & 0xffff) + // sum <= 0x1_0000 therefore this is the last fold needed: + // if (sum >> 16) > 0 then + // (sum >> 16) == 1 && (sum & 0xffff) == 0 and therefore + // the addition will not overflow + // otherwise (sum >> 16) == 0 and sum will be unchanged + sum = (sum >> 16) + (sum & 0xffff) + return uint16(sum) +} + +func addrPartialChecksum64(addr []byte, initial, carryIn uint64) (sum, carry uint64) { + sum, carry = initial, carryIn + switch len(addr) { + case 4: // IPv4 + if cpu.IsBigEndian { + sum, carry = bits.Add64(sum, uint64(binary.BigEndian.Uint32(addr)), carry) + } else { + sum, carry = bits.Add64(sum, uint64(binary.LittleEndian.Uint32(addr)), carry) + } + case 16: // IPv6 + if cpu.IsBigEndian { + sum, carry = bits.Add64(sum, binary.BigEndian.Uint64(addr), carry) + sum, carry = bits.Add64(sum, binary.BigEndian.Uint64(addr[8:]), carry) + } else { + sum, carry = bits.Add64(sum, binary.LittleEndian.Uint64(addr), carry) + sum, carry = bits.Add64(sum, binary.LittleEndian.Uint64(addr[8:]), carry) + } + default: + panic("bad addr length") + } + return sum, carry +} + +func addrPartialChecksum32(addr []byte, initial, carryIn uint32) (sum, carry uint32) { + sum, carry = initial, carryIn + switch len(addr) { + case 4: // IPv4 + if cpu.IsBigEndian { + sum, carry = bits.Add32(sum, binary.BigEndian.Uint32(addr), carry) + } else { + sum, carry = bits.Add32(sum, binary.LittleEndian.Uint32(addr), carry) + } + case 16: // IPv6 + if cpu.IsBigEndian { + sum, carry = bits.Add32(sum, binary.BigEndian.Uint32(addr), carry) + sum, carry = bits.Add32(sum, binary.BigEndian.Uint32(addr[4:8]), carry) + sum, carry = bits.Add32(sum, binary.BigEndian.Uint32(addr[8:12]), carry) + sum, carry = bits.Add32(sum, binary.BigEndian.Uint32(addr[12:16]), carry) + } else { + sum, carry = bits.Add32(sum, binary.LittleEndian.Uint32(addr), carry) + sum, carry = bits.Add32(sum, binary.LittleEndian.Uint32(addr[4:8]), carry) + sum, carry = bits.Add32(sum, binary.LittleEndian.Uint32(addr[8:12]), carry) + sum, carry = bits.Add32(sum, binary.LittleEndian.Uint32(addr[12:16]), carry) + } + default: + panic("bad addr length") + } + return sum, carry +} + +func pseudoHeaderChecksum64(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + var sum uint64 + if cpu.IsBigEndian { + sum = uint64(totalLen) + uint64(protocol) + } else { + sum = uint64(bits.ReverseBytes16(totalLen)) + uint64(protocol)<<8 + } + sum, carry := addrPartialChecksum64(srcAddr, sum, 0) + sum, carry = addrPartialChecksum64(dstAddr, sum, carry) + + foldedSum := ipChecksumFold64(sum, carry) + if !cpu.IsBigEndian { + foldedSum = bits.ReverseBytes16(foldedSum) + } + return foldedSum +} + +func pseudoHeaderChecksum32(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + var sum uint32 + if cpu.IsBigEndian { + sum = uint32(totalLen) + uint32(protocol) + } else { + sum = uint32(bits.ReverseBytes16(totalLen)) + uint32(protocol)<<8 + } + sum, carry := addrPartialChecksum32(srcAddr, sum, 0) + sum, carry = addrPartialChecksum32(dstAddr, sum, carry) + + foldedSum := ipChecksumFold32(sum, carry) + if !cpu.IsBigEndian { + foldedSum = bits.ReverseBytes16(foldedSum) + } + return foldedSum +} + +func pseudoHeaderChecksum(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + if strconv.IntSize < 64 { + return pseudoHeaderChecksum32(protocol, srcAddr, dstAddr, totalLen) + } + return pseudoHeaderChecksum64(protocol, srcAddr, dstAddr, totalLen) } diff --git a/tun/checksum_amd64.go b/tun/checksum_amd64.go new file mode 100644 index 0000000..5e87693 --- /dev/null +++ b/tun/checksum_amd64.go @@ -0,0 +1,20 @@ +package tun + +import "golang.org/x/sys/cpu" + +// checksum computes an IP checksum starting with the provided initial value. +// The length of data should be at least 128 bytes for best performance. Smaller +// buffers will still compute a correct result. For best performance with +// smaller buffers, use shortChecksum(). +var checksum = checksumAMD64 + +func init() { + if cpu.X86.HasAVX && cpu.X86.HasAVX2 && cpu.X86.HasBMI2 { + checksum = checksumAVX2 + return + } + if cpu.X86.HasSSE2 { + checksum = checksumSSE2 + return + } +} diff --git a/tun/checksum_amd64_test.go b/tun/checksum_amd64_test.go new file mode 100644 index 0000000..7a0b681 --- /dev/null +++ b/tun/checksum_amd64_test.go @@ -0,0 +1,45 @@ +//go:build amd64 + +package tun + +import ( + "golang.org/x/sys/cpu" +) + +var archChecksumFuncs = []archChecksumDetails{ + { + name: "generic32", + available: true, + f: checksumGeneric32, + }, + { + name: "generic64", + available: true, + f: checksumGeneric64, + }, + { + name: "generic32Alternate", + available: true, + f: checksumGeneric32Alternate, + }, + { + name: "generic64Alternate", + available: true, + f: checksumGeneric64Alternate, + }, + { + name: "AMD64", + available: true, + f: checksumAMD64, + }, + { + name: "SSE2", + available: cpu.X86.HasSSE2, + f: checksumSSE2, + }, + { + name: "AVX2", + available: cpu.X86.HasAVX && cpu.X86.HasAVX2 && cpu.X86.HasBMI2, + f: checksumAVX2, + }, +} diff --git a/tun/checksum_generated_amd64.go b/tun/checksum_generated_amd64.go new file mode 100644 index 0000000..b4a2941 --- /dev/null +++ b/tun/checksum_generated_amd64.go @@ -0,0 +1,18 @@ +// Code generated by command: go run generate_amd64.go -out checksum_generated_amd64.s -stubs checksum_generated_amd64.go. DO NOT EDIT. + +package tun + +// checksumAVX2 computes an IP checksum using amd64 v3 instructions (AVX2, BMI2) +// +//go:noescape +func checksumAVX2(b []byte, initial uint16) uint16 + +// checksumSSE2 computes an IP checksum using amd64 baseline instructions (SSE2) +// +//go:noescape +func checksumSSE2(b []byte, initial uint16) uint16 + +// checksumAMD64 computes an IP checksum using amd64 baseline instructions +// +//go:noescape +func checksumAMD64(b []byte, initial uint16) uint16 diff --git a/tun/checksum_generated_amd64.s b/tun/checksum_generated_amd64.s new file mode 100644 index 0000000..5f2e4c5 --- /dev/null +++ b/tun/checksum_generated_amd64.s @@ -0,0 +1,851 @@ +// Code generated by command: go run generate_amd64.go -out checksum_generated_amd64.s -stubs checksum_generated_amd64.go. DO NOT EDIT. + +#include "textflag.h" + +DATA xmmLoadMasks<>+0(SB)/16, $"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" +DATA xmmLoadMasks<>+16(SB)/16, $"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff" +DATA xmmLoadMasks<>+32(SB)/16, $"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff" +DATA xmmLoadMasks<>+48(SB)/16, $"\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff" +DATA xmmLoadMasks<>+64(SB)/16, $"\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +DATA xmmLoadMasks<>+80(SB)/16, $"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +DATA xmmLoadMasks<>+96(SB)/16, $"\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +GLOBL xmmLoadMasks<>(SB), RODATA|NOPTR, $112 + +// func checksumAVX2(b []byte, initial uint16) uint16 +// Requires: AVX, AVX2, BMI2 +TEXT ·checksumAVX2(SB), NOSPLIT|NOFRAME, $0-34 + MOVWQZX initial+24(FP), AX + XCHGB AH, AL + MOVQ b_base+0(FP), DX + MOVQ b_len+8(FP), BX + + // handle odd length buffers; they are difficult to handle in general + TESTQ $0x00000001, BX + JZ lengthIsEven + MOVBQZX -1(DX)(BX*1), CX + DECQ BX + ADDQ CX, AX + +lengthIsEven: + // handle tiny buffers (<=31 bytes) specially + CMPQ BX, $0x1f + JGT bufferIsNotTiny + XORQ CX, CX + XORQ SI, SI + XORQ DI, DI + + // shift twice to start because length is guaranteed to be even + // n = n >> 2; CF = originalN & 2 + SHRQ $0x02, BX + JNC handleTiny4 + + // tmp2 = binary.LittleEndian.Uint16(buf[:2]); buf = buf[2:] + MOVWQZX (DX), CX + ADDQ $0x02, DX + +handleTiny4: + // n = n >> 1; CF = originalN & 4 + SHRQ $0x01, BX + JNC handleTiny8 + + // tmp4 = binary.LittleEndian.Uint32(buf[:4]); buf = buf[4:] + MOVLQZX (DX), SI + ADDQ $0x04, DX + +handleTiny8: + // n = n >> 1; CF = originalN & 8 + SHRQ $0x01, BX + JNC handleTiny16 + + // tmp8 = binary.LittleEndian.Uint64(buf[:8]); buf = buf[8:] + MOVQ (DX), DI + ADDQ $0x08, DX + +handleTiny16: + // n = n >> 1; CF = originalN & 16 + // n == 0 now, otherwise we would have branched after comparing with tinyBufferSize + SHRQ $0x01, BX + JNC handleTinyFinish + ADDQ (DX), AX + ADCQ 8(DX), AX + +handleTinyFinish: + // CF should be included from the previous add, so we use ADCQ. + // If we arrived via the JNC above, then CF=0 due to the branch condition, + // so ADCQ will still produce the correct result. + ADCQ CX, AX + ADCQ SI, AX + ADCQ DI, AX + JMP foldAndReturn + +bufferIsNotTiny: + // skip all SIMD for small buffers + CMPQ BX, $0x00000100 + JGE startSIMD + + // Accumulate carries in this register. It is never expected to overflow. + XORQ SI, SI + + // We will perform an overlapped read for buffers with length not a multiple of 8. + // Overlapped in this context means some memory will be read twice, but a shift will + // eliminate the duplicated data. This extra read is performed at the end of the buffer to + // preserve any alignment that may exist for the start of the buffer. + MOVQ BX, CX + SHRQ $0x03, BX + ANDQ $0x07, CX + JZ handleRemaining8 + LEAQ (DX)(BX*8), DI + MOVQ -8(DI)(CX*1), DI + + // Shift out the duplicated data: overlapRead = overlapRead >> (64 - leftoverBytes*8) + SHLQ $0x03, CX + NEGQ CX + ADDQ $0x40, CX + SHRQ CL, DI + ADDQ DI, AX + ADCQ $0x00, SI + +handleRemaining8: + SHRQ $0x01, BX + JNC handleRemaining16 + ADDQ (DX), AX + ADCQ $0x00, SI + ADDQ $0x08, DX + +handleRemaining16: + SHRQ $0x01, BX + JNC handleRemaining32 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ $0x00, SI + ADDQ $0x10, DX + +handleRemaining32: + SHRQ $0x01, BX + JNC handleRemaining64 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ $0x00, SI + ADDQ $0x20, DX + +handleRemaining64: + SHRQ $0x01, BX + JNC handleRemaining128 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ $0x00, SI + ADDQ $0x40, DX + +handleRemaining128: + SHRQ $0x01, BX + JNC handleRemainingComplete + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ 64(DX), AX + ADCQ 72(DX), AX + ADCQ 80(DX), AX + ADCQ 88(DX), AX + ADCQ 96(DX), AX + ADCQ 104(DX), AX + ADCQ 112(DX), AX + ADCQ 120(DX), AX + ADCQ $0x00, SI + ADDQ $0x80, DX + +handleRemainingComplete: + ADDQ SI, AX + JMP foldAndReturn + +startSIMD: + VPXOR Y0, Y0, Y0 + VPXOR Y1, Y1, Y1 + VPXOR Y2, Y2, Y2 + VPXOR Y3, Y3, Y3 + MOVQ BX, CX + + // Update number of bytes remaining after the loop completes + ANDQ $0xff, BX + + // Number of 256 byte iterations + SHRQ $0x08, CX + JZ smallLoop + +bigLoop: + VPMOVZXWD (DX), Y4 + VPADDD Y4, Y0, Y0 + VPMOVZXWD 16(DX), Y4 + VPADDD Y4, Y1, Y1 + VPMOVZXWD 32(DX), Y4 + VPADDD Y4, Y2, Y2 + VPMOVZXWD 48(DX), Y4 + VPADDD Y4, Y3, Y3 + VPMOVZXWD 64(DX), Y4 + VPADDD Y4, Y0, Y0 + VPMOVZXWD 80(DX), Y4 + VPADDD Y4, Y1, Y1 + VPMOVZXWD 96(DX), Y4 + VPADDD Y4, Y2, Y2 + VPMOVZXWD 112(DX), Y4 + VPADDD Y4, Y3, Y3 + VPMOVZXWD 128(DX), Y4 + VPADDD Y4, Y0, Y0 + VPMOVZXWD 144(DX), Y4 + VPADDD Y4, Y1, Y1 + VPMOVZXWD 160(DX), Y4 + VPADDD Y4, Y2, Y2 + VPMOVZXWD 176(DX), Y4 + VPADDD Y4, Y3, Y3 + VPMOVZXWD 192(DX), Y4 + VPADDD Y4, Y0, Y0 + VPMOVZXWD 208(DX), Y4 + VPADDD Y4, Y1, Y1 + VPMOVZXWD 224(DX), Y4 + VPADDD Y4, Y2, Y2 + VPMOVZXWD 240(DX), Y4 + VPADDD Y4, Y3, Y3 + ADDQ $0x00000100, DX + DECQ CX + JNZ bigLoop + CMPQ BX, $0x10 + JLT doneSmallLoop + + // now read a single 16 byte unit of data at a time +smallLoop: + VPMOVZXWD (DX), Y4 + VPADDD Y4, Y0, Y0 + ADDQ $0x10, DX + SUBQ $0x10, BX + CMPQ BX, $0x10 + JGE smallLoop + +doneSmallLoop: + CMPQ BX, $0x00 + JE doneSIMD + + // There are between 1 and 15 bytes remaining. Perform an overlapped read. + LEAQ xmmLoadMasks<>+0(SB), CX + VMOVDQU -16(DX)(BX*1), X4 + VPAND -16(CX)(BX*8), X4, X4 + VPMOVZXWD X4, Y4 + VPADDD Y4, Y0, Y0 + +doneSIMD: + // Multi-chain loop is done, combine the accumulators + VPADDD Y1, Y0, Y0 + VPADDD Y2, Y0, Y0 + VPADDD Y3, Y0, Y0 + + // extract the YMM into a pair of XMM and sum them + VEXTRACTI128 $0x01, Y0, X1 + VPADDD X0, X1, X0 + + // extract the XMM into GP64 + VPEXTRQ $0x00, X0, CX + VPEXTRQ $0x01, X0, DX + + // no more AVX code, clear upper registers to avoid SSE slowdowns + VZEROUPPER + ADDQ CX, AX + ADCQ DX, AX + +foldAndReturn: + // add CF and fold + RORXQ $0x20, AX, CX + ADCL CX, AX + RORXL $0x10, AX, CX + ADCW CX, AX + ADCW $0x00, AX + XCHGB AH, AL + MOVW AX, ret+32(FP) + RET + +// func checksumSSE2(b []byte, initial uint16) uint16 +// Requires: SSE2 +TEXT ·checksumSSE2(SB), NOSPLIT|NOFRAME, $0-34 + MOVWQZX initial+24(FP), AX + XCHGB AH, AL + MOVQ b_base+0(FP), DX + MOVQ b_len+8(FP), BX + + // handle odd length buffers; they are difficult to handle in general + TESTQ $0x00000001, BX + JZ lengthIsEven + MOVBQZX -1(DX)(BX*1), CX + DECQ BX + ADDQ CX, AX + +lengthIsEven: + // handle tiny buffers (<=31 bytes) specially + CMPQ BX, $0x1f + JGT bufferIsNotTiny + XORQ CX, CX + XORQ SI, SI + XORQ DI, DI + + // shift twice to start because length is guaranteed to be even + // n = n >> 2; CF = originalN & 2 + SHRQ $0x02, BX + JNC handleTiny4 + + // tmp2 = binary.LittleEndian.Uint16(buf[:2]); buf = buf[2:] + MOVWQZX (DX), CX + ADDQ $0x02, DX + +handleTiny4: + // n = n >> 1; CF = originalN & 4 + SHRQ $0x01, BX + JNC handleTiny8 + + // tmp4 = binary.LittleEndian.Uint32(buf[:4]); buf = buf[4:] + MOVLQZX (DX), SI + ADDQ $0x04, DX + +handleTiny8: + // n = n >> 1; CF = originalN & 8 + SHRQ $0x01, BX + JNC handleTiny16 + + // tmp8 = binary.LittleEndian.Uint64(buf[:8]); buf = buf[8:] + MOVQ (DX), DI + ADDQ $0x08, DX + +handleTiny16: + // n = n >> 1; CF = originalN & 16 + // n == 0 now, otherwise we would have branched after comparing with tinyBufferSize + SHRQ $0x01, BX + JNC handleTinyFinish + ADDQ (DX), AX + ADCQ 8(DX), AX + +handleTinyFinish: + // CF should be included from the previous add, so we use ADCQ. + // If we arrived via the JNC above, then CF=0 due to the branch condition, + // so ADCQ will still produce the correct result. + ADCQ CX, AX + ADCQ SI, AX + ADCQ DI, AX + JMP foldAndReturn + +bufferIsNotTiny: + // skip all SIMD for small buffers + CMPQ BX, $0x00000100 + JGE startSIMD + + // Accumulate carries in this register. It is never expected to overflow. + XORQ SI, SI + + // We will perform an overlapped read for buffers with length not a multiple of 8. + // Overlapped in this context means some memory will be read twice, but a shift will + // eliminate the duplicated data. This extra read is performed at the end of the buffer to + // preserve any alignment that may exist for the start of the buffer. + MOVQ BX, CX + SHRQ $0x03, BX + ANDQ $0x07, CX + JZ handleRemaining8 + LEAQ (DX)(BX*8), DI + MOVQ -8(DI)(CX*1), DI + + // Shift out the duplicated data: overlapRead = overlapRead >> (64 - leftoverBytes*8) + SHLQ $0x03, CX + NEGQ CX + ADDQ $0x40, CX + SHRQ CL, DI + ADDQ DI, AX + ADCQ $0x00, SI + +handleRemaining8: + SHRQ $0x01, BX + JNC handleRemaining16 + ADDQ (DX), AX + ADCQ $0x00, SI + ADDQ $0x08, DX + +handleRemaining16: + SHRQ $0x01, BX + JNC handleRemaining32 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ $0x00, SI + ADDQ $0x10, DX + +handleRemaining32: + SHRQ $0x01, BX + JNC handleRemaining64 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ $0x00, SI + ADDQ $0x20, DX + +handleRemaining64: + SHRQ $0x01, BX + JNC handleRemaining128 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ $0x00, SI + ADDQ $0x40, DX + +handleRemaining128: + SHRQ $0x01, BX + JNC handleRemainingComplete + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ 64(DX), AX + ADCQ 72(DX), AX + ADCQ 80(DX), AX + ADCQ 88(DX), AX + ADCQ 96(DX), AX + ADCQ 104(DX), AX + ADCQ 112(DX), AX + ADCQ 120(DX), AX + ADCQ $0x00, SI + ADDQ $0x80, DX + +handleRemainingComplete: + ADDQ SI, AX + JMP foldAndReturn + +startSIMD: + PXOR X0, X0 + PXOR X1, X1 + PXOR X2, X2 + PXOR X3, X3 + PXOR X4, X4 + MOVQ BX, CX + + // Update number of bytes remaining after the loop completes + ANDQ $0xff, BX + + // Number of 256 byte iterations + SHRQ $0x08, CX + JZ smallLoop + +bigLoop: + MOVOU (DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X2 + MOVOU 16(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X1 + PADDD X6, X3 + MOVOU 32(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X2 + PADDD X6, X0 + MOVOU 48(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X3 + PADDD X6, X1 + MOVOU 64(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X2 + MOVOU 80(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X1 + PADDD X6, X3 + MOVOU 96(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X2 + PADDD X6, X0 + MOVOU 112(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X3 + PADDD X6, X1 + MOVOU 128(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X2 + MOVOU 144(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X1 + PADDD X6, X3 + MOVOU 160(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X2 + PADDD X6, X0 + MOVOU 176(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X3 + PADDD X6, X1 + MOVOU 192(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X2 + MOVOU 208(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X1 + PADDD X6, X3 + MOVOU 224(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X2 + PADDD X6, X0 + MOVOU 240(DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X3 + PADDD X6, X1 + ADDQ $0x00000100, DX + DECQ CX + JNZ bigLoop + CMPQ BX, $0x10 + JLT doneSmallLoop + + // now read a single 16 byte unit of data at a time +smallLoop: + MOVOU (DX), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X1 + ADDQ $0x10, DX + SUBQ $0x10, BX + CMPQ BX, $0x10 + JGE smallLoop + +doneSmallLoop: + CMPQ BX, $0x00 + JE doneSIMD + + // There are between 1 and 15 bytes remaining. Perform an overlapped read. + LEAQ xmmLoadMasks<>+0(SB), CX + MOVOU -16(DX)(BX*1), X5 + PAND -16(CX)(BX*8), X5 + MOVOA X5, X6 + PUNPCKHWL X4, X5 + PUNPCKLWL X4, X6 + PADDD X5, X0 + PADDD X6, X1 + +doneSIMD: + // Multi-chain loop is done, combine the accumulators + PADDD X1, X0 + PADDD X2, X0 + PADDD X3, X0 + + // extract the XMM into GP64 + MOVQ X0, CX + PSRLDQ $0x08, X0 + MOVQ X0, DX + ADDQ CX, AX + ADCQ DX, AX + +foldAndReturn: + // add CF and fold + MOVL AX, CX + ADCQ $0x00, CX + SHRQ $0x20, AX + ADDQ CX, AX + MOVWQZX AX, CX + SHRQ $0x10, AX + ADDQ CX, AX + MOVW AX, CX + SHRQ $0x10, AX + ADDW CX, AX + ADCW $0x00, AX + XCHGB AH, AL + MOVW AX, ret+32(FP) + RET + +// func checksumAMD64(b []byte, initial uint16) uint16 +TEXT ·checksumAMD64(SB), NOSPLIT|NOFRAME, $0-34 + MOVWQZX initial+24(FP), AX + XCHGB AH, AL + MOVQ b_base+0(FP), DX + MOVQ b_len+8(FP), BX + + // handle odd length buffers; they are difficult to handle in general + TESTQ $0x00000001, BX + JZ lengthIsEven + MOVBQZX -1(DX)(BX*1), CX + DECQ BX + ADDQ CX, AX + +lengthIsEven: + // handle tiny buffers (<=31 bytes) specially + CMPQ BX, $0x1f + JGT bufferIsNotTiny + XORQ CX, CX + XORQ SI, SI + XORQ DI, DI + + // shift twice to start because length is guaranteed to be even + // n = n >> 2; CF = originalN & 2 + SHRQ $0x02, BX + JNC handleTiny4 + + // tmp2 = binary.LittleEndian.Uint16(buf[:2]); buf = buf[2:] + MOVWQZX (DX), CX + ADDQ $0x02, DX + +handleTiny4: + // n = n >> 1; CF = originalN & 4 + SHRQ $0x01, BX + JNC handleTiny8 + + // tmp4 = binary.LittleEndian.Uint32(buf[:4]); buf = buf[4:] + MOVLQZX (DX), SI + ADDQ $0x04, DX + +handleTiny8: + // n = n >> 1; CF = originalN & 8 + SHRQ $0x01, BX + JNC handleTiny16 + + // tmp8 = binary.LittleEndian.Uint64(buf[:8]); buf = buf[8:] + MOVQ (DX), DI + ADDQ $0x08, DX + +handleTiny16: + // n = n >> 1; CF = originalN & 16 + // n == 0 now, otherwise we would have branched after comparing with tinyBufferSize + SHRQ $0x01, BX + JNC handleTinyFinish + ADDQ (DX), AX + ADCQ 8(DX), AX + +handleTinyFinish: + // CF should be included from the previous add, so we use ADCQ. + // If we arrived via the JNC above, then CF=0 due to the branch condition, + // so ADCQ will still produce the correct result. + ADCQ CX, AX + ADCQ SI, AX + ADCQ DI, AX + JMP foldAndReturn + +bufferIsNotTiny: + // Number of 256 byte iterations into loop counter + MOVQ BX, CX + + // Update number of bytes remaining after the loop completes + ANDQ $0xff, BX + SHRQ $0x08, CX + JZ startCleanup + CLC + XORQ SI, SI + XORQ DI, DI + XORQ R8, R8 + XORQ R9, R9 + XORQ R10, R10 + XORQ R11, R11 + XORQ R12, R12 + +bigLoop: + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ $0x00, SI + ADDQ 32(DX), DI + ADCQ 40(DX), DI + ADCQ 48(DX), DI + ADCQ 56(DX), DI + ADCQ $0x00, R8 + ADDQ 64(DX), R9 + ADCQ 72(DX), R9 + ADCQ 80(DX), R9 + ADCQ 88(DX), R9 + ADCQ $0x00, R10 + ADDQ 96(DX), R11 + ADCQ 104(DX), R11 + ADCQ 112(DX), R11 + ADCQ 120(DX), R11 + ADCQ $0x00, R12 + ADDQ 128(DX), AX + ADCQ 136(DX), AX + ADCQ 144(DX), AX + ADCQ 152(DX), AX + ADCQ $0x00, SI + ADDQ 160(DX), DI + ADCQ 168(DX), DI + ADCQ 176(DX), DI + ADCQ 184(DX), DI + ADCQ $0x00, R8 + ADDQ 192(DX), R9 + ADCQ 200(DX), R9 + ADCQ 208(DX), R9 + ADCQ 216(DX), R9 + ADCQ $0x00, R10 + ADDQ 224(DX), R11 + ADCQ 232(DX), R11 + ADCQ 240(DX), R11 + ADCQ 248(DX), R11 + ADCQ $0x00, R12 + ADDQ $0x00000100, DX + SUBQ $0x01, CX + JNZ bigLoop + ADDQ SI, AX + ADCQ DI, AX + ADCQ R8, AX + ADCQ R9, AX + ADCQ R10, AX + ADCQ R11, AX + ADCQ R12, AX + + // accumulate CF (twice, in case the first time overflows) + ADCQ $0x00, AX + ADCQ $0x00, AX + +startCleanup: + // Accumulate carries in this register. It is never expected to overflow. + XORQ SI, SI + + // We will perform an overlapped read for buffers with length not a multiple of 8. + // Overlapped in this context means some memory will be read twice, but a shift will + // eliminate the duplicated data. This extra read is performed at the end of the buffer to + // preserve any alignment that may exist for the start of the buffer. + MOVQ BX, CX + SHRQ $0x03, BX + ANDQ $0x07, CX + JZ handleRemaining8 + LEAQ (DX)(BX*8), DI + MOVQ -8(DI)(CX*1), DI + + // Shift out the duplicated data: overlapRead = overlapRead >> (64 - leftoverBytes*8) + SHLQ $0x03, CX + NEGQ CX + ADDQ $0x40, CX + SHRQ CL, DI + ADDQ DI, AX + ADCQ $0x00, SI + +handleRemaining8: + SHRQ $0x01, BX + JNC handleRemaining16 + ADDQ (DX), AX + ADCQ $0x00, SI + ADDQ $0x08, DX + +handleRemaining16: + SHRQ $0x01, BX + JNC handleRemaining32 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ $0x00, SI + ADDQ $0x10, DX + +handleRemaining32: + SHRQ $0x01, BX + JNC handleRemaining64 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ $0x00, SI + ADDQ $0x20, DX + +handleRemaining64: + SHRQ $0x01, BX + JNC handleRemaining128 + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ $0x00, SI + ADDQ $0x40, DX + +handleRemaining128: + SHRQ $0x01, BX + JNC handleRemainingComplete + ADDQ (DX), AX + ADCQ 8(DX), AX + ADCQ 16(DX), AX + ADCQ 24(DX), AX + ADCQ 32(DX), AX + ADCQ 40(DX), AX + ADCQ 48(DX), AX + ADCQ 56(DX), AX + ADCQ 64(DX), AX + ADCQ 72(DX), AX + ADCQ 80(DX), AX + ADCQ 88(DX), AX + ADCQ 96(DX), AX + ADCQ 104(DX), AX + ADCQ 112(DX), AX + ADCQ 120(DX), AX + ADCQ $0x00, SI + ADDQ $0x80, DX + +handleRemainingComplete: + ADDQ SI, AX + +foldAndReturn: + // add CF and fold + MOVL AX, CX + ADCQ $0x00, CX + SHRQ $0x20, AX + ADDQ CX, AX + MOVWQZX AX, CX + SHRQ $0x10, AX + ADDQ CX, AX + MOVW AX, CX + SHRQ $0x10, AX + ADDW CX, AX + ADCW $0x00, AX + XCHGB AH, AL + MOVW AX, ret+32(FP) + RET diff --git a/tun/checksum_generic.go b/tun/checksum_generic.go new file mode 100644 index 0000000..d0bfb69 --- /dev/null +++ b/tun/checksum_generic.go @@ -0,0 +1,15 @@ +// This file contains IP checksum algorithms that are not specific to any +// architecture and don't use hardware acceleration. + +//go:build !amd64 + +package tun + +import "strconv" + +func checksum(data []byte, initial uint16) uint16 { + if strconv.IntSize < 64 { + return checksumGeneric32(data, initial) + } + return checksumGeneric64(data, initial) +} diff --git a/tun/checksum_generic_test.go b/tun/checksum_generic_test.go index a0c9457..401a7bb 100644 --- a/tun/checksum_generic_test.go +++ b/tun/checksum_generic_test.go @@ -1,9 +1,26 @@ +//go:build !amd64 + package tun var archChecksumFuncs = []archChecksumDetails{ { - name: "generic", + name: "generic32", available: true, - f: checksum, + f: checksumGeneric32, + }, + { + name: "generic32Alternate", + available: true, + f: checksumGeneric32Alternate, + }, + { + name: "generic64", + available: true, + f: checksumGeneric64, + }, + { + name: "generic64Alternate", + available: true, + f: checksumGeneric64Alternate, }, } diff --git a/tun/checksum_test.go b/tun/checksum_test.go index b3a3583..c40efc9 100644 --- a/tun/checksum_test.go +++ b/tun/checksum_test.go @@ -18,7 +18,7 @@ import ( type archChecksumDetails struct { name string available bool - f func([]byte, uint64) uint16 + f func([]byte, uint16) uint16 } func deterministicRandomBytes(seed int64, length int) []byte { @@ -402,7 +402,7 @@ func TestChecksum(t *testing.T) { if !fd.available { t.Skip("can not run on this system") } - if got := fd.f(tt.data, uint64(tt.initial)); got != tt.want { + if got := fd.f(tt.data, tt.initial); got != tt.want { t.Errorf("%s checksum = %04x, want %04x", fd.name, got, tt.want) } }) @@ -444,20 +444,28 @@ func TestPseudoHeaderChecksumNoFold(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotNoFold := pseudoHeaderChecksumNoFold(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - got := checksum([]byte{}, gotNoFold) - if got != tt.want { - t.Errorf("pseudoHeaderChecksumNoFold() = %x, folds to %04x, want %04x", gotNoFold, got, tt.want) - } - - got = header.PseudoHeaderChecksum( - tcpip.TransportProtocolNumber(tt.protocol), - tcpip.AddrFromSlice(tt.srcAddr), - tcpip.AddrFromSlice(tt.dstAddr), - tt.totalLen) - if got != tt.want { - t.Errorf("header.PseudoHeaderChecksum() = %04x, want %04x", got, tt.want) - } + t.Run("pseudoHeaderChecksum32", func(t *testing.T) { + got := pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) + if got != tt.want { + t.Errorf("got %04x, want %04x", got, tt.want) + } + }) + t.Run("pseudoHeaderChecksum64", func(t *testing.T) { + got := pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) + if got != tt.want { + t.Errorf("got %04x, want %04x", got, tt.want) + } + }) + t.Run("reference", func(t *testing.T) { + got := header.PseudoHeaderChecksum( + tcpip.TransportProtocolNumber(tt.protocol), + tcpip.AddrFromSlice(tt.srcAddr), + tcpip.AddrFromSlice(tt.dstAddr), + tt.totalLen) + if got != tt.want { + t.Errorf("got %04x, want %04x", got, tt.want) + } + }) }) } } @@ -482,7 +490,7 @@ func FuzzChecksum(f *testing.F) { if !fd.available { t.Skip("can not run on this system") } - if got := fd.f(data, uint64(initial)); got != want { + if got := fd.f(data, initial); got != want { t.Errorf("%s checksum = %04x, want %04x", fd.name, got, want) } }) @@ -491,7 +499,6 @@ func FuzzChecksum(f *testing.F) { } var result uint16 -var result64 uint64 func BenchmarkChecksum(b *testing.B) { offsets := []int{ // offsets from page alignment @@ -590,9 +597,16 @@ func BenchmarkPseudoHeaderChecksum(b *testing.B) { } for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { - result64 += pseudoHeaderChecksumNoFold(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - } + b.Run("pseudoHeaderChecksum32", func(b *testing.B) { + for i := 0; i < b.N; i++ { + result += pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) + } + }) + b.Run("pseudoHeaderChecksum64", func(b *testing.B) { + for i := 0; i < b.N; i++ { + result += pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) + } + }) }) } } diff --git a/tun/generate_amd64.go b/tun/generate_amd64.go new file mode 100644 index 0000000..543e211 --- /dev/null +++ b/tun/generate_amd64.go @@ -0,0 +1,579 @@ +//go:build ignore + +//go:generate go run generate_amd64.go -out checksum_generated_amd64.s -stubs checksum_generated_amd64.go + +package main + +import ( + "fmt" + "math" + "math/bits" + + . "github.com/mmcloughlin/avo/build" + "github.com/mmcloughlin/avo/operand" + "github.com/mmcloughlin/avo/reg" +) + +const checksumSignature = "func(b []byte, initial uint16) uint16" + +func loadParams() (accum, buf, n reg.GPVirtual) { + accum, buf, n = GP64(), GP64(), GP64() + Load(Param("initial"), accum) + XCHGB(accum.As8H(), accum.As8L()) + Load(Param("b").Base(), buf) + Load(Param("b").Len(), n) + return +} + +type simdStrategy int + +const ( + sse2 = iota + avx2 +) + +const tinyBufferSize = 31 // A buffer is tiny if it has at most 31 bytes. + +func generateSIMDChecksum(name, doc string, minSIMDSize, chains int, strategy simdStrategy) { + TEXT(name, NOSPLIT|NOFRAME, checksumSignature) + Pragma("noescape") + Doc(doc) + + accum64, buf, n := loadParams() + + handleOddLength(n, buf, accum64) + // no chance of overflow because accum64 was initialized by a uint16 and + // handleOddLength adds at most a uint8 + handleTinyBuffers(n, buf, accum64, operand.LabelRef("foldAndReturn"), operand.LabelRef("bufferIsNotTiny")) + Label("bufferIsNotTiny") + + const simdReadSize = 16 + + if minSIMDSize > tinyBufferSize { + Comment("skip all SIMD for small buffers") + if minSIMDSize <= math.MaxUint8 { + CMPQ(n, operand.U8(minSIMDSize)) + } else { + CMPQ(n, operand.U32(minSIMDSize)) + } + JGE(operand.LabelRef("startSIMD")) + + handleRemaining(n, buf, accum64, minSIMDSize-1) + JMP(operand.LabelRef("foldAndReturn")) + } + + Label("startSIMD") + + // chains is the number of accumulators to use. This improves speed via + // reduced data dependency. We combine the accumulators once when the big + // loop is complete. + simdAccumulate := make([]reg.VecVirtual, chains) + for i := range simdAccumulate { + switch strategy { + case sse2: + simdAccumulate[i] = XMM() + PXOR(simdAccumulate[i], simdAccumulate[i]) + case avx2: + simdAccumulate[i] = YMM() + VPXOR(simdAccumulate[i], simdAccumulate[i], simdAccumulate[i]) + } + } + var zero reg.VecVirtual + if strategy == sse2 { + zero = XMM() + PXOR(zero, zero) + } + + // Number of loads per big loop + const unroll = 16 + // Number of bytes + loopSize := uint64(simdReadSize * unroll) + if bits.Len64(loopSize) != bits.Len64(loopSize-1)+1 { + panic("loopSize is not a power of 2") + } + loopCount := GP64() + + MOVQ(n, loopCount) + Comment("Update number of bytes remaining after the loop completes") + ANDQ(operand.Imm(loopSize-1), n) + Comment(fmt.Sprintf("Number of %d byte iterations", loopSize)) + SHRQ(operand.Imm(uint64(bits.Len64(loopSize-1))), loopCount) + JZ(operand.LabelRef("smallLoop")) + Label("bigLoop") + for i := 0; i < unroll; i++ { + chain := i % chains + switch strategy { + case sse2: + sse2AccumulateStep(i*simdReadSize, buf, zero, simdAccumulate[chain], simdAccumulate[(chain+chains/2)%chains]) + case avx2: + avx2AccumulateStep(i*simdReadSize, buf, simdAccumulate[chain]) + } + } + ADDQ(operand.U32(loopSize), buf) + DECQ(loopCount) + JNZ(operand.LabelRef("bigLoop")) + + Label("bigCleanup") + + CMPQ(n, operand.Imm(uint64(simdReadSize))) + JLT(operand.LabelRef("doneSmallLoop")) + + Commentf("now read a single %d byte unit of data at a time", simdReadSize) + Label("smallLoop") + + switch strategy { + case sse2: + sse2AccumulateStep(0, buf, zero, simdAccumulate[0], simdAccumulate[1]) + case avx2: + avx2AccumulateStep(0, buf, simdAccumulate[0]) + } + ADDQ(operand.Imm(uint64(simdReadSize)), buf) + SUBQ(operand.Imm(uint64(simdReadSize)), n) + CMPQ(n, operand.Imm(uint64(simdReadSize))) + JGE(operand.LabelRef("smallLoop")) + + Label("doneSmallLoop") + CMPQ(n, operand.Imm(0)) + JE(operand.LabelRef("doneSIMD")) + + Commentf("There are between 1 and %d bytes remaining. Perform an overlapped read.", simdReadSize-1) + + maskDataPtr := GP64() + LEAQ(operand.NewDataAddr(operand.NewStaticSymbol("xmmLoadMasks"), 0), maskDataPtr) + dataAddr := operand.Mem{Index: n, Scale: 1, Base: buf, Disp: -simdReadSize} + // scale 8 is only correct here because n is guaranteed to be even and we + // do not generate masks for odd lengths + maskAddr := operand.Mem{Base: maskDataPtr, Index: n, Scale: 8, Disp: -16} + remainder := XMM() + + switch strategy { + case sse2: + MOVOU(dataAddr, remainder) + PAND(maskAddr, remainder) + low := XMM() + MOVOA(remainder, low) + PUNPCKHWL(zero, remainder) + PUNPCKLWL(zero, low) + PADDD(remainder, simdAccumulate[0]) + PADDD(low, simdAccumulate[1]) + case avx2: + // Note: this is very similar to the sse2 path but MOVOU has a massive + // performance hit if used here, presumably due to switching between SSE + // and AVX2 modes. + VMOVDQU(dataAddr, remainder) + VPAND(maskAddr, remainder, remainder) + + temp := YMM() + VPMOVZXWD(remainder, temp) + VPADDD(temp, simdAccumulate[0], simdAccumulate[0]) + } + + Label("doneSIMD") + + Comment("Multi-chain loop is done, combine the accumulators") + for i := range simdAccumulate { + if i == 0 { + continue + } + switch strategy { + case sse2: + PADDD(simdAccumulate[i], simdAccumulate[0]) + case avx2: + VPADDD(simdAccumulate[i], simdAccumulate[0], simdAccumulate[0]) + } + } + + if strategy == avx2 { + Comment("extract the YMM into a pair of XMM and sum them") + tmp := YMM() + VEXTRACTI128(operand.Imm(1), simdAccumulate[0], tmp.AsX()) + + xAccumulate := XMM() + VPADDD(simdAccumulate[0].AsX(), tmp.AsX(), xAccumulate) + simdAccumulate = []reg.VecVirtual{xAccumulate} + } + + Comment("extract the XMM into GP64") + low, high := GP64(), GP64() + switch strategy { + case sse2: + MOVQ(simdAccumulate[0], low) + PSRLDQ(operand.Imm(8), simdAccumulate[0]) + MOVQ(simdAccumulate[0], high) + case avx2: + VPEXTRQ(operand.Imm(0), simdAccumulate[0], low) + VPEXTRQ(operand.Imm(1), simdAccumulate[0], high) + + Comment("no more AVX code, clear upper registers to avoid SSE slowdowns") + VZEROUPPER() + } + ADDQ(low, accum64) + ADCQ(high, accum64) + Label("foldAndReturn") + foldWithCF(accum64, strategy == avx2) + XCHGB(accum64.As8H(), accum64.As8L()) + Store(accum64.As16(), ReturnIndex(0)) + RET() +} + +// handleOddLength generates instructions to incorporate the last byte into +// accum64 if the length is odd. CF may be set if accum64 overflows; be sure to +// handle that if overflow is possible. +func handleOddLength(n, buf, accum64 reg.GPVirtual) { + Comment("handle odd length buffers; they are difficult to handle in general") + TESTQ(operand.U32(1), n) + JZ(operand.LabelRef("lengthIsEven")) + + tmp := GP64() + MOVBQZX(operand.Mem{Base: buf, Index: n, Scale: 1, Disp: -1}, tmp) + DECQ(n) + ADDQ(tmp, accum64) + + Label("lengthIsEven") +} + +func sse2AccumulateStep(offset int, buf reg.GPVirtual, zero, accumulate1, accumulate2 reg.VecVirtual) { + high, low := XMM(), XMM() + MOVOU(operand.Mem{Disp: offset, Base: buf}, high) + MOVOA(high, low) + PUNPCKHWL(zero, high) + PUNPCKLWL(zero, low) + PADDD(high, accumulate1) + PADDD(low, accumulate2) +} + +func avx2AccumulateStep(offset int, buf reg.GPVirtual, accumulate reg.VecVirtual) { + tmp := YMM() + VPMOVZXWD(operand.Mem{Disp: offset, Base: buf}, tmp) + VPADDD(tmp, accumulate, accumulate) +} + +func generateAMD64Checksum(name, doc string) { + TEXT(name, NOSPLIT|NOFRAME, checksumSignature) + Pragma("noescape") + Doc(doc) + + accum64, buf, n := loadParams() + + handleOddLength(n, buf, accum64) + // no chance of overflow because accum64 was initialized by a uint16 and + // handleOddLength adds at most a uint8 + handleTinyBuffers(n, buf, accum64, operand.LabelRef("foldAndReturn"), operand.LabelRef("bufferIsNotTiny")) + Label("bufferIsNotTiny") + + const ( + // numChains is the number of accumulators and carry counters to use. + // This improves speed via reduced data dependency. We combine the + // accumulators and carry counters once when the loop is complete. + numChains = 4 + unroll = 32 // The number of 64-bit reads to perform per iteration of the loop. + loopSize = 8 * unroll // The number of bytes read per iteration of the loop. + ) + if bits.Len(loopSize) != bits.Len(loopSize-1)+1 { + panic("loopSize is not a power of 2") + } + loopCount := GP64() + + Comment(fmt.Sprintf("Number of %d byte iterations into loop counter", loopSize)) + MOVQ(n, loopCount) + Comment("Update number of bytes remaining after the loop completes") + ANDQ(operand.Imm(loopSize-1), n) + SHRQ(operand.Imm(uint64(bits.Len(loopSize-1))), loopCount) + JZ(operand.LabelRef("startCleanup")) + CLC() + + chains := make([]struct { + accum reg.GPVirtual + carries reg.GPVirtual + }, numChains) + for i := range chains { + if i == 0 { + chains[i].accum = accum64 + } else { + chains[i].accum = GP64() + XORQ(chains[i].accum, chains[i].accum) + } + chains[i].carries = GP64() + XORQ(chains[i].carries, chains[i].carries) + } + + Label("bigLoop") + + var curChain int + for i := 0; i < unroll; i++ { + // It is significantly faster to use a ADCX/ADOX pair instead of plain + // ADC, which results in two dependency chains, however those require + // ADX support, which was added after AVX2. If AVX2 is available, that's + // even better than ADCX/ADOX. + // + // However, multiple dependency chains using multiple accumulators and + // occasionally storing CF into temporary counters seems to work almost + // as well. + addr := operand.Mem{Disp: i * 8, Base: buf} + + if i%4 == 0 { + if i > 0 { + ADCQ(operand.Imm(0), chains[curChain].carries) + curChain = (curChain + 1) % len(chains) + } + ADDQ(addr, chains[curChain].accum) + } else { + ADCQ(addr, chains[curChain].accum) + } + } + ADCQ(operand.Imm(0), chains[curChain].carries) + ADDQ(operand.U32(loopSize), buf) + SUBQ(operand.Imm(1), loopCount) + JNZ(operand.LabelRef("bigLoop")) + for i := range chains { + if i == 0 { + ADDQ(chains[i].carries, accum64) + continue + } + ADCQ(chains[i].accum, accum64) + ADCQ(chains[i].carries, accum64) + } + + accumulateCF(accum64) + + Label("startCleanup") + handleRemaining(n, buf, accum64, loopSize-1) + Label("foldAndReturn") + foldWithCF(accum64, false) + + XCHGB(accum64.As8H(), accum64.As8L()) + Store(accum64.As16(), ReturnIndex(0)) + RET() +} + +// handleTinyBuffers computes checksums if the buffer length (the n parameter) +// is less than 32. After computing the checksum, a jump to returnLabel will +// be executed. Otherwise, if the buffer length is at least 32, nothing will be +// modified; a jump to continueLabel will be executed instead. +// +// When jumping to returnLabel, CF may be set and must be accommodated e.g. +// using foldWithCF or accumulateCF. +// +// Anecdotally, this appears to be faster than attempting to coordinate an +// overlapped read (which would also require special handling for buffers +// smaller than 8). +func handleTinyBuffers(n, buf, accum reg.GPVirtual, returnLabel, continueLabel operand.LabelRef) { + Comment("handle tiny buffers (<=31 bytes) specially") + CMPQ(n, operand.Imm(tinyBufferSize)) + JGT(continueLabel) + + tmp2, tmp4, tmp8 := GP64(), GP64(), GP64() + XORQ(tmp2, tmp2) + XORQ(tmp4, tmp4) + XORQ(tmp8, tmp8) + + Comment("shift twice to start because length is guaranteed to be even", + "n = n >> 2; CF = originalN & 2") + SHRQ(operand.Imm(2), n) + JNC(operand.LabelRef("handleTiny4")) + Comment("tmp2 = binary.LittleEndian.Uint16(buf[:2]); buf = buf[2:]") + MOVWQZX(operand.Mem{Base: buf}, tmp2) + ADDQ(operand.Imm(2), buf) + + Label("handleTiny4") + Comment("n = n >> 1; CF = originalN & 4") + SHRQ(operand.Imm(1), n) + JNC(operand.LabelRef("handleTiny8")) + Comment("tmp4 = binary.LittleEndian.Uint32(buf[:4]); buf = buf[4:]") + MOVLQZX(operand.Mem{Base: buf}, tmp4) + ADDQ(operand.Imm(4), buf) + + Label("handleTiny8") + Comment("n = n >> 1; CF = originalN & 8") + SHRQ(operand.Imm(1), n) + JNC(operand.LabelRef("handleTiny16")) + Comment("tmp8 = binary.LittleEndian.Uint64(buf[:8]); buf = buf[8:]") + MOVQ(operand.Mem{Base: buf}, tmp8) + ADDQ(operand.Imm(8), buf) + + Label("handleTiny16") + Comment("n = n >> 1; CF = originalN & 16", + "n == 0 now, otherwise we would have branched after comparing with tinyBufferSize") + SHRQ(operand.Imm(1), n) + JNC(operand.LabelRef("handleTinyFinish")) + ADDQ(operand.Mem{Base: buf}, accum) + ADCQ(operand.Mem{Base: buf, Disp: 8}, accum) + + Label("handleTinyFinish") + Comment("CF should be included from the previous add, so we use ADCQ.", + "If we arrived via the JNC above, then CF=0 due to the branch condition,", + "so ADCQ will still produce the correct result.") + ADCQ(tmp2, accum) + ADCQ(tmp4, accum) + ADCQ(tmp8, accum) + + JMP(returnLabel) +} + +// handleRemaining generates a series of conditional unrolled additions, +// starting with 8 bytes long and doubling each time until the length reaches +// max. This is the reverse order of what may be intuitive, but makes the branch +// conditions convenient to compute: perform one right shift each time and test +// against CF. +// +// When done, CF may be set and must be accommodated e.g., using foldWithCF or +// accumulateCF. +// +// If n is not a multiple of 8, an extra 64 bit read at the end of the buffer +// will be performed, overlapping with data that will be read later. The +// duplicate data will be shifted off. +// +// The original buffer length must have been at least 8 bytes long, even if +// n < 8, otherwise this will access memory before the start of the buffer, +// which may be unsafe. +func handleRemaining(n, buf, accum64 reg.GPVirtual, max int) { + Comment("Accumulate carries in this register. It is never expected to overflow.") + carries := GP64() + XORQ(carries, carries) + + Comment("We will perform an overlapped read for buffers with length not a multiple of 8.", + "Overlapped in this context means some memory will be read twice, but a shift will", + "eliminate the duplicated data. This extra read is performed at the end of the buffer to", + "preserve any alignment that may exist for the start of the buffer.") + leftover := reg.RCX + MOVQ(n, leftover) + SHRQ(operand.Imm(3), n) // n is now the number of 64 bit reads remaining + ANDQ(operand.Imm(0x7), leftover) // leftover is now the number of bytes to read from the end + JZ(operand.LabelRef("handleRemaining8")) + endBuf := GP64() + // endBuf is the position near the end of the buffer that is just past the + // last multiple of 8: (buf + len(buf)) & ^0x7 + LEAQ(operand.Mem{Base: buf, Index: n, Scale: 8}, endBuf) + + overlapRead := GP64() + // equivalent to overlapRead = binary.LittleEndian.Uint64(buf[len(buf)-8:len(buf)]) + MOVQ(operand.Mem{Base: endBuf, Index: leftover, Scale: 1, Disp: -8}, overlapRead) + + Comment("Shift out the duplicated data: overlapRead = overlapRead >> (64 - leftoverBytes*8)") + SHLQ(operand.Imm(3), leftover) // leftover = leftover * 8 + NEGQ(leftover) // leftover = -leftover; this completes the (-leftoverBytes*8) part of the expression + ADDQ(operand.Imm(64), leftover) // now we have (64 - leftoverBytes*8) + SHRQ(reg.CL, overlapRead) // shift right by (64 - leftoverBytes*8); CL is the low 8 bits of leftover (set to RCX above) and variable shift only accepts CL + + ADDQ(overlapRead, accum64) + ADCQ(operand.Imm(0), carries) + + for curBytes := 8; curBytes <= max; curBytes *= 2 { + Label(fmt.Sprintf("handleRemaining%d", curBytes)) + SHRQ(operand.Imm(1), n) + if curBytes*2 <= max { + JNC(operand.LabelRef(fmt.Sprintf("handleRemaining%d", curBytes*2))) + } else { + JNC(operand.LabelRef("handleRemainingComplete")) + } + + numLoads := curBytes / 8 + for i := 0; i < numLoads; i++ { + addr := operand.Mem{Base: buf, Disp: i * 8} + // It is possible to add the multiple dependency chains trick here + // that generateAMD64Checksum uses but anecdotally it does not + // appear to outweigh the cost. + if i == 0 { + ADDQ(addr, accum64) + continue + } + ADCQ(addr, accum64) + } + ADCQ(operand.Imm(0), carries) + + if curBytes > math.MaxUint8 { + ADDQ(operand.U32(uint64(curBytes)), buf) + } else { + ADDQ(operand.U8(uint64(curBytes)), buf) + } + if curBytes*2 >= max { + continue + } + JMP(operand.LabelRef(fmt.Sprintf("handleRemaining%d", curBytes*2))) + } + Label("handleRemainingComplete") + ADDQ(carries, accum64) +} + +func accumulateCF(accum64 reg.GPVirtual) { + Comment("accumulate CF (twice, in case the first time overflows)") + // accum64 += CF + ADCQ(operand.Imm(0), accum64) + // accum64 += CF again if the previous add overflowed. The previous add was + // 0 or 1. If it overflowed, then accum64 == 0, so adding another 1 can + // never overflow. + ADCQ(operand.Imm(0), accum64) +} + +// foldWithCF generates instructions to fold accum (a GP64) into a 16-bit value +// according to ones-complement arithmetic. BMI2 instructions will be used if +// allowBMI2 is true (requires fewer instructions). +func foldWithCF(accum reg.GPVirtual, allowBMI2 bool) { + Comment("add CF and fold") + + // CF|accum max value starts as 0x1_ffff_ffff_ffff_ffff + + tmp := GP64() + if allowBMI2 { + // effectively, tmp = accum >> 32 (technically, this is a rotate) + RORXQ(operand.Imm(32), accum, tmp) + // accum as uint32 = uint32(accum) + uint32(tmp64) + CF; max value 0xffff_ffff + CF set + ADCL(tmp.As32(), accum.As32()) + // effectively, tmp64 as uint32 = uint32(accum) >> 16 (also a rotate) + RORXL(operand.Imm(16), accum.As32(), tmp.As32()) + // accum as uint16 = uint16(accum) + uint16(tmp) + CF; max value 0xffff + CF unset or 0xfffe + CF set + ADCW(tmp.As16(), accum.As16()) + } else { + // tmp = uint32(accum); max value 0xffff_ffff + // MOVL clears the upper 32 bits of a GP64 so this is equivalent to the + // non-existent MOVLQZX. + MOVL(accum.As32(), tmp.As32()) + // tmp += CF; max value 0x1_0000_0000, CF unset + ADCQ(operand.Imm(0), tmp) + // accum = accum >> 32; max value 0xffff_ffff + SHRQ(operand.Imm(32), accum) + // accum = accum + tmp; max value 0x1_ffff_ffff + CF unset + ADDQ(tmp, accum) + // tmp = uint16(accum); max value 0xffff + MOVWQZX(accum.As16(), tmp) + // accum = accum >> 16; max value 0x1_ffff + SHRQ(operand.Imm(16), accum) + // accum = accum + tmp; max value 0x2_fffe + CF unset + ADDQ(tmp, accum) + // tmp as uint16 = uint16(accum); max value 0xffff + MOVW(accum.As16(), tmp.As16()) + // accum = accum >> 16; max value 0x2 + SHRQ(operand.Imm(16), accum) + // accum as uint16 = uint16(accum) + uint16(tmp); max value 0xffff + CF unset or 0x2 + CF set + ADDW(tmp.As16(), accum.As16()) + } + // accum as uint16 += CF; will not overflow: either CF was 0 or accum <= 0xfffe + ADCW(operand.Imm(0), accum.As16()) +} + +func generateLoadMasks() { + var offset int + // xmmLoadMasks is a table of masks that can be used with PAND to zero all but the last N bytes in an XMM, N=2,4,6,8,10,12,14 + GLOBL("xmmLoadMasks", RODATA|NOPTR) + + for n := 2; n < 16; n += 2 { + var pattern [16]byte + for i := 0; i < len(pattern); i++ { + if i < len(pattern)-n { + pattern[i] = 0 + continue + } + pattern[i] = 0xff + } + DATA(offset, operand.String(pattern[:])) + offset += len(pattern) + } +} + +func main() { + generateLoadMasks() + generateSIMDChecksum("checksumAVX2", "checksumAVX2 computes an IP checksum using amd64 v3 instructions (AVX2, BMI2)", 256, 4, avx2) + generateSIMDChecksum("checksumSSE2", "checksumSSE2 computes an IP checksum using amd64 baseline instructions (SSE2)", 256, 4, sse2) + generateAMD64Checksum("checksumAMD64", "checksumAMD64 computes an IP checksum using amd64 baseline instructions") + Generate() +} diff --git a/tun/tcp_offload_linux.go b/tun/tcp_offload_linux.go index 6728823..b023bbd 100644 --- a/tun/tcp_offload_linux.go +++ b/tun/tcp_offload_linux.go @@ -260,8 +260,8 @@ func tcpChecksumValid(pkt []byte, iphLen uint8, isV6 bool) bool { addrSize = 16 } tcpTotalLen := uint16(len(pkt) - int(iphLen)) - tcpCSumNoFold := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], tcpTotalLen) - return ^checksum(pkt[iphLen:], tcpCSumNoFold) == 0 + tcpCSum := pseudoHeaderChecksum(unix.IPPROTO_TCP, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], tcpTotalLen) + return ^checksum(pkt[iphLen:], tcpCSum) == 0 } // coalesceResult represents the result of attempting to coalesce two TCP @@ -532,7 +532,7 @@ func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 srcAddrAt := offset + addrOffset srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] - psum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) + psum := pseudoHeaderChecksum(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) } else { hdr := virtioNetHdr{} @@ -643,8 +643,8 @@ func tcpTSO(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffs // TCP checksum tcpHLen := int(hdr.hdrLen - hdr.csumStart) tcpLenForPseudo := uint16(tcpHLen + segmentDataLen) - tcpCSumNoFold := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], tcpLenForPseudo) - tcpCSum := ^checksum(out[hdr.csumStart:totalLen], tcpCSumNoFold) + tcpCSum := pseudoHeaderChecksum(unix.IPPROTO_TCP, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], tcpLenForPseudo) + tcpCSum = ^checksum(out[hdr.csumStart:totalLen], tcpCSum) binary.BigEndian.PutUint16(out[hdr.csumStart+hdr.csumOffset:], tcpCSum) nextSegmentDataAt += int(hdr.gsoSize) @@ -658,6 +658,6 @@ func gsoNoneChecksum(in []byte, cSumStart, cSumOffset uint16) error { // checksum we compute. This is typically the pseudo-header checksum. initial := binary.BigEndian.Uint16(in[cSumAt:]) in[cSumAt], in[cSumAt+1] = 0, 0 - binary.BigEndian.PutUint16(in[cSumAt:], ^checksum(in[cSumStart:], uint64(initial))) + binary.BigEndian.PutUint16(in[cSumAt:], ^checksum(in[cSumStart:], initial)) return nil } From 6cd5922a04de9370d48cf742751d4cdcc6ce65e1 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 24 Aug 2023 14:39:23 -0700 Subject: [PATCH 010/173] all: adjust build tags for plan9 Signed-off-by: Brad Fitzpatrick --- conn/bind_std.go | 7 ++++++- conn/controlfns_unix.go | 2 +- conn/erraddrinuse.go | 14 ++++++++++++++ ipc/{uapi_wasm.go => uapi_fake.go} | 4 +++- rwcancel/rwcancel.go | 2 +- rwcancel/rwcancel_stub.go | 2 +- 6 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 conn/erraddrinuse.go rename ipc/{uapi_wasm.go => uapi_fake.go} (72%) diff --git a/conn/bind_std.go b/conn/bind_std.go index cc5cf23..428e528 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -136,6 +136,11 @@ func listenNet(network string, port int) (*net.UDPConn, int, error) { return conn.(*net.UDPConn), uaddr.Port, nil } +// errEADDRINUSE is syscall.EADDRINUSE, boxed into an interface once +// in erraddrinuse.go on almost all platforms. For other platforms, +// it's at least non-nil. +var errEADDRINUSE error = errors.New("") + func (s *StdNetBind) Open(uport uint16) ([]ReceiveFunc, uint16, error) { s.mu.Lock() defer s.mu.Unlock() @@ -162,7 +167,7 @@ again: // Listen on the same port as we're using for ipv4. v6conn, port, err = listenNet("udp6", port) - if uport == 0 && errors.Is(err, syscall.EADDRINUSE) && tries < 100 { + if uport == 0 && errors.Is(err, errEADDRINUSE) && tries < 100 { v4conn.Close() tries++ goto again diff --git a/conn/controlfns_unix.go b/conn/controlfns_unix.go index 91692c0..5cc4d98 100644 --- a/conn/controlfns_unix.go +++ b/conn/controlfns_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !linux && !wasm +//go:build !windows && !linux && !wasm && !plan9 /* SPDX-License-Identifier: MIT * diff --git a/conn/erraddrinuse.go b/conn/erraddrinuse.go new file mode 100644 index 0000000..751660e --- /dev/null +++ b/conn/erraddrinuse.go @@ -0,0 +1,14 @@ +//go:build !plan9 + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package conn + +import "syscall" + +func init() { + errEADDRINUSE = syscall.EADDRINUSE +} diff --git a/ipc/uapi_wasm.go b/ipc/uapi_fake.go similarity index 72% rename from ipc/uapi_wasm.go rename to ipc/uapi_fake.go index fa84684..e68863d 100644 --- a/ipc/uapi_wasm.go +++ b/ipc/uapi_fake.go @@ -1,3 +1,5 @@ +//go:build wasm || plan9 + /* SPDX-License-Identifier: MIT * * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. @@ -5,7 +7,7 @@ package ipc -// Made up sentinel error codes for {js,wasip1}/wasm. +// Made up sentinel error codes for {js,wasip1}/wasm, and plan9. const ( IpcErrorIO = 1 IpcErrorInvalid = 2 diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index e397c0e..dd649d4 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm +//go:build !windows && !wasm && !plan9 /* SPDX-License-Identifier: MIT * diff --git a/rwcancel/rwcancel_stub.go b/rwcancel/rwcancel_stub.go index 2a98b2b..4623801 100644 --- a/rwcancel/rwcancel_stub.go +++ b/rwcancel/rwcancel_stub.go @@ -1,4 +1,4 @@ -//go:build windows || wasm +//go:build windows || wasm || plan9 // SPDX-License-Identifier: MIT From 202a3401e7f83a2f809fefaa8231d79e0e97edae Mon Sep 17 00:00:00 2001 From: Andrea Barisani Date: Wed, 30 Aug 2023 14:05:01 +0200 Subject: [PATCH 011/173] adjust build tags for tamago --- conn/controlfns_unix.go | 2 +- ipc/uapi_tamago.go | 17 +++++++++++++++++ rwcancel/rwcancel.go | 2 +- rwcancel/rwcancel_stub.go | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 ipc/uapi_tamago.go diff --git a/conn/controlfns_unix.go b/conn/controlfns_unix.go index 5cc4d98..144a880 100644 --- a/conn/controlfns_unix.go +++ b/conn/controlfns_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !linux && !wasm && !plan9 +//go:build !windows && !linux && !wasm && !plan9 && !tamago /* SPDX-License-Identifier: MIT * diff --git a/ipc/uapi_tamago.go b/ipc/uapi_tamago.go new file mode 100644 index 0000000..85a725a --- /dev/null +++ b/ipc/uapi_tamago.go @@ -0,0 +1,17 @@ +//go:build tamago + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package ipc + +// Made up sentinel error codes for tamago platform. +const ( + IpcErrorIO = 1 + IpcErrorInvalid = 2 + IpcErrorPortInUse = 3 + IpcErrorUnknown = 4 + IpcErrorProtocol = 5 +) diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index dd649d4..ceb87e4 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm && !plan9 +//go:build !windows && !wasm && !plan9 && !tamago /* SPDX-License-Identifier: MIT * diff --git a/rwcancel/rwcancel_stub.go b/rwcancel/rwcancel_stub.go index 4623801..60ae9af 100644 --- a/rwcancel/rwcancel_stub.go +++ b/rwcancel/rwcancel_stub.go @@ -1,4 +1,4 @@ -//go:build windows || wasm || plan9 +//go:build windows || wasm || plan9 || tamago // SPDX-License-Identifier: MIT From 2f6748dc88e777ff6eed22f5ce5d7658c6bb9410 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Wed, 27 Sep 2023 14:52:21 -0700 Subject: [PATCH 012/173] tun: fix crash when ForceMTU is called after close Close closes the events channel, resulting in a panic from send on closed channel. Reported-By: Brad Fitzpatrick Link: https://github.com/tailscale/tailscale/issues/9555 Signed-off-by: James Tucker --- tun/tun_windows.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tun/tun_windows.go b/tun/tun_windows.go index 0cb4ce1..34f2980 100644 --- a/tun/tun_windows.go +++ b/tun/tun_windows.go @@ -127,6 +127,9 @@ func (tun *NativeTun) MTU() (int, error) { // TODO: This is a temporary hack. We really need to be monitoring the interface in real time and adapting to MTU changes. func (tun *NativeTun) ForceMTU(mtu int) { + if tun.close.Load() { + return + } update := tun.forcedMTU != mtu tun.forcedMTU = mtu if update { From 8f1a6a10b2a74ae86920a74e7f0989e9f57df990 Mon Sep 17 00:00:00 2001 From: Mark Puha Date: Fri, 6 Oct 2023 02:11:27 +0530 Subject: [PATCH 013/173] Advanced security (#2) * Advanced security header layer & config --- .gitignore | 2 +- conn/bind_windows.go | 2 +- conn/bindtest/bindtest.go | 2 +- device/bind_test.go | 2 +- device/device.go | 278 ++++++++++++++++++++++++++- device/device_test.go | 150 ++++++++++++--- device/keypair.go | 2 +- device/noise-protocol.go | 26 ++- device/noise_test.go | 4 +- device/peer.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/receive.go | 59 ++++-- device/send.go | 104 ++++++++-- device/sticky_default.go | 4 +- device/sticky_linux.go | 4 +- device/tun.go | 2 +- device/uapi.go | 146 ++++++++++++-- device/util.go | 25 +++ device/util_test.go | 27 +++ go.mod | 3 +- go.sum | 2 + ipc/namedpipe/namedpipe_test.go | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_windows.go | 2 +- main.go | 8 +- main_windows.go | 8 +- tun/netstack/examples/http_client.go | 6 +- tun/netstack/examples/http_server.go | 6 +- tun/netstack/examples/ping_client.go | 6 +- tun/netstack/tun.go | 2 +- tun/tcp_offload_linux.go | 2 +- tun/tcp_offload_linux_test.go | 2 +- tun/tun_linux.go | 4 +- tun/tuntest/tuntest.go | 2 +- 35 files changed, 781 insertions(+), 121 deletions(-) create mode 100644 device/util.go create mode 100644 device/util_test.go diff --git a/.gitignore b/.gitignore index e460293..71549f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -wireguard-go +wireguard-go \ No newline at end of file diff --git a/conn/bind_windows.go b/conn/bind_windows.go index d5095e0..9bad0ee 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -17,7 +17,7 @@ import ( "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/conn/winrio" + "github.com/amnezia-vpn/amnezia-wg/conn/winrio" ) const ( diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 74e7add..713c371 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -12,7 +12,7 @@ import ( "net/netip" "os" - "golang.zx2c4.com/wireguard/conn" + "github.com/amnezia-vpn/amnezia-wg/conn" ) type ChannelBind struct { diff --git a/device/bind_test.go b/device/bind_test.go index 302a521..eae36c2 100644 --- a/device/bind_test.go +++ b/device/bind_test.go @@ -8,7 +8,7 @@ package device import ( "errors" - "golang.zx2c4.com/wireguard/conn" + "github.com/amnezia-vpn/amnezia-wg/conn" ) type DummyDatagram struct { diff --git a/device/device.go b/device/device.go index 1af9fe0..10365d1 100644 --- a/device/device.go +++ b/device/device.go @@ -11,10 +11,12 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/ratelimiter" - "golang.zx2c4.com/wireguard/rwcancel" - "golang.zx2c4.com/wireguard/tun" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/ipc" + "github.com/amnezia-vpn/amnezia-wg/ratelimiter" + "github.com/amnezia-vpn/amnezia-wg/rwcancel" + "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/tevino/abool/v2" ) type Device struct { @@ -89,6 +91,22 @@ type Device struct { ipcMutex sync.RWMutex closed chan struct{} log *Logger + + isASecOn abool.AtomicBool + aSecMux sync.RWMutex + aSecCfg aSecCfgType +} + +type aSecCfgType struct { + junkPacketCount int + junkPacketMinSize int + junkPacketMaxSize int + initPacketJunkSize int + responsePacketJunkSize int + initPacketMagicHeader uint32 + responsePacketMagicHeader uint32 + underloadPacketMagicHeader uint32 + transportPacketMagicHeader uint32 } // deviceState represents the state of a Device. @@ -162,7 +180,8 @@ func (device *Device) changeState(want deviceState) (err error) { 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 } @@ -526,7 +545,7 @@ func (device *Device) BindUpdate() error { // start receiving routines device.net.stopping.Add(len(recvFns)) device.queue.decryption.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.decryption - device.queue.handshake.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.handshake + device.queue.handshake.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.handshake batchSize := netc.bind.BatchSize() for _, fn := range recvFns { go device.RoutineReceiveIncoming(batchSize, fn) @@ -542,3 +561,250 @@ func (device *Device) BindClose() error { device.net.Unlock() return err } +func (device *Device) isAdvancedSecurityOn() bool { + return device.isASecOn.IsSet() +} + +func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { + + if tempASecCfg.junkPacketCount == 0 && + tempASecCfg.junkPacketMaxSize == 0 && + tempASecCfg.junkPacketMinSize == 0 && + tempASecCfg.initPacketJunkSize == 0 && + tempASecCfg.responsePacketJunkSize == 0 && + tempASecCfg.initPacketMagicHeader == 0 && + tempASecCfg.responsePacketMagicHeader == 0 && + tempASecCfg.underloadPacketMagicHeader == 0 && + tempASecCfg.transportPacketMagicHeader == 0 { + return err + } + + isASecOn := false + device.aSecMux.Lock() + if tempASecCfg.junkPacketCount < 0 { + err = ipcErrorf( + ipc.IpcErrorInvalid, + "JunkPacketCount should be non negative", + ) + } + device.aSecCfg.junkPacketCount = tempASecCfg.junkPacketCount + if tempASecCfg.junkPacketCount != 0 { + isASecOn = true + } + + device.aSecCfg.junkPacketMinSize = tempASecCfg.junkPacketMinSize + if tempASecCfg.junkPacketMinSize != 0 { + isASecOn = true + } + + if device.aSecCfg.junkPacketCount > 0 && + tempASecCfg.junkPacketMaxSize == tempASecCfg.junkPacketMinSize { + + tempASecCfg.junkPacketMaxSize++ // to make rand gen work + } + + if tempASecCfg.junkPacketMaxSize >= MaxSegmentSize{ + device.aSecCfg.junkPacketMinSize = 0 + device.aSecCfg.junkPacketMaxSize = 1 + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d; %w", + tempASecCfg.junkPacketMaxSize, + MaxSegmentSize, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", + tempASecCfg.junkPacketMaxSize, + MaxSegmentSize, + ) + } + } else if tempASecCfg.junkPacketMaxSize < tempASecCfg.junkPacketMinSize { + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + "maxSize: %d; should be greater than minSize: %d; %w", + tempASecCfg.junkPacketMaxSize, + tempASecCfg.junkPacketMinSize, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + "maxSize: %d; should be greater than minSize: %d", + tempASecCfg.junkPacketMaxSize, + tempASecCfg.junkPacketMinSize, + ) + } + } else { + device.aSecCfg.junkPacketMaxSize = tempASecCfg.junkPacketMaxSize + } + + if tempASecCfg.junkPacketMaxSize != 0 { + isASecOn = true + } + + if MessageInitiationSize+tempASecCfg.initPacketJunkSize >= MaxSegmentSize { + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d; %w`, + tempASecCfg.initPacketJunkSize, + MaxSegmentSize, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempASecCfg.initPacketJunkSize, + MaxSegmentSize, + ) + } + } else { + device.aSecCfg.initPacketJunkSize = tempASecCfg.initPacketJunkSize + } + + if tempASecCfg.initPacketJunkSize != 0 { + isASecOn = true + } + + if MessageResponseSize+tempASecCfg.responsePacketJunkSize >= MaxSegmentSize { + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d; %w`, + tempASecCfg.responsePacketJunkSize, + MaxSegmentSize, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempASecCfg.responsePacketJunkSize, + MaxSegmentSize, + ) + } + } else { + device.aSecCfg.responsePacketJunkSize = tempASecCfg.responsePacketJunkSize + } + + if tempASecCfg.responsePacketJunkSize != 0 { + isASecOn = true + } + + if tempASecCfg.initPacketMagicHeader > 4 { + isASecOn = true + device.log.Verbosef("UAPI: Updating init_packet_magic_header") + device.aSecCfg.initPacketMagicHeader = tempASecCfg.initPacketMagicHeader + MessageInitiationType = device.aSecCfg.initPacketMagicHeader + } else { + device.log.Verbosef("UAPI: Using default init type") + MessageInitiationType = 1 + } + + if tempASecCfg.responsePacketMagicHeader > 4 { + isASecOn = true + device.log.Verbosef("UAPI: Updating response_packet_magic_header") + device.aSecCfg.responsePacketMagicHeader = tempASecCfg.responsePacketMagicHeader + MessageResponseType = device.aSecCfg.responsePacketMagicHeader + } else { + device.log.Verbosef("UAPI: Using default response type") + MessageResponseType = 2 + } + + if tempASecCfg.underloadPacketMagicHeader > 4 { + isASecOn = true + device.log.Verbosef("UAPI: Updating underload_packet_magic_header") + device.aSecCfg.underloadPacketMagicHeader = tempASecCfg.underloadPacketMagicHeader + MessageCookieReplyType = device.aSecCfg.underloadPacketMagicHeader + } else { + device.log.Verbosef("UAPI: Using default underload type") + MessageCookieReplyType = 3 + } + + if tempASecCfg.transportPacketMagicHeader > 4 { + isASecOn = true + device.log.Verbosef("UAPI: Updating transport_packet_magic_header") + device.aSecCfg.transportPacketMagicHeader = tempASecCfg.transportPacketMagicHeader + MessageTransportType = device.aSecCfg.transportPacketMagicHeader + } else { + device.log.Verbosef("UAPI: Using default transport type") + MessageTransportType = 4 + } + + isSameMap := map[uint32]bool{} + isSameMap[MessageInitiationType] = true + isSameMap[MessageResponseType] = true + isSameMap[MessageCookieReplyType] = true + isSameMap[MessageTransportType] = true + + // size will be different if same values + if len(isSameMap) != 4 { + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d; %w`, + MessageInitiationType, + MessageResponseType, + MessageCookieReplyType, + MessageTransportType, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d`, + MessageInitiationType, + MessageResponseType, + MessageCookieReplyType, + MessageTransportType, + ) + } + } + + newInitSize := MessageInitiationSize + device.aSecCfg.initPacketJunkSize + newResponseSize := MessageResponseSize + device.aSecCfg.responsePacketJunkSize + + if newInitSize == newResponseSize { + if err != nil { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `new init size:%d; and new response size:%d; should differ; %w`, + newInitSize, + newResponseSize, + err, + ) + } else { + err = ipcErrorf( + ipc.IpcErrorInvalid, + `new init size:%d; and new response size:%d; should differ`, + newInitSize, + newResponseSize, + ) + } + } else { + packetSizeToMsgType = map[int]uint32{ + newInitSize: MessageInitiationType, + newResponseSize: MessageResponseType, + MessageCookieReplySize: MessageCookieReplyType, + MessageTransportSize: MessageTransportType, + } + + msgTypeToJunkSize = map[uint32]int{ + MessageInitiationType: device.aSecCfg.initPacketJunkSize, + MessageResponseType: device.aSecCfg.responsePacketJunkSize, + MessageCookieReplyType: 0, + MessageTransportType: 0, + } + } + + device.isASecOn.SetTo(isASecOn) + device.aSecMux.Unlock() + + return err +} diff --git a/device/device_test.go b/device/device_test.go index fff172b..afa1dc3 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -20,10 +20,10 @@ import ( "testing" "time" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/conn/bindtest" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/tuntest" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/conn/bindtest" + "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amnezia-wg/tun/tuntest" ) // uapiCfg returns a string that contains cfg formatted use with IpcSet. @@ -91,6 +91,65 @@ func genConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { return } +func genASecurityConfigs(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", + "jc", "5", + "jmin", "500", + "jmax", "501", + "s1", "30", + "s2", "40", + "h1", "123456", + "h2", "67543", + "h4", "32345", + "h3", "123123", + "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", + "jc", "5", + "jmin", "500", + "jmax", "501", + "s1", "30", + "s2", "40", + "h1", "123456", + "h2", "67543", + "h4", "32345", + "h3", "123123", + "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 @@ -115,7 +174,11 @@ func (d SendDirection) String() string { return "pong" } -func (pair *testPair) Send(tb testing.TB, ping SendDirection, done chan struct{}) { +func (pair *testPair) Send( + tb testing.TB, + ping SendDirection, + done chan struct{}, +) { tb.Helper() p0, p1 := pair[0], pair[1] if !ping { @@ -149,8 +212,16 @@ func (pair *testPair) Send(tb testing.TB, ping SendDirection, done chan struct{} } // genTestPair creates a testPair. -func genTestPair(tb testing.TB, realSocket bool) (pair testPair) { - cfg, endpointCfg := genConfigs(tb) +func genTestPair( + tb testing.TB, + realSocket, withASecurity bool, +) (pair testPair) { + var cfg, endpointCfg [2]string + if withASecurity { + cfg, endpointCfg = genASecurityConfigs(tb) + } else { + cfg, endpointCfg = genConfigs(tb) + } var binds [2]conn.Bind if realSocket { binds[0], binds[1] = conn.NewDefaultBind(), conn.NewDefaultBind() @@ -166,7 +237,7 @@ func genTestPair(tb testing.TB, realSocket bool) (pair testPair) { 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))) + 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() @@ -194,7 +265,18 @@ func genTestPair(tb testing.TB, realSocket bool) (pair testPair) { func TestTwoDevicePing(t *testing.T) { goroutineLeakCheck(t) - pair := genTestPair(t, true) + pair := genTestPair(t, true, false) + 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 TestTwoDevicePingASecurity(t *testing.T) { + goroutineLeakCheck(t) + pair := genTestPair(t, true, true) t.Run("ping 1.0.0.1", func(t *testing.T) { pair.Send(t, Ping, nil) }) @@ -209,10 +291,10 @@ func TestUpDown(t *testing.T) { const otrials = 10 for n := 0; n < otrials; n++ { - pair := genTestPair(t, false) + pair := genTestPair(t, false, 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[:]))) + pair[i].dev.IpcSet(fmt.Sprintf("public_key=%s\npersistent_keepalive_interval=1\n",hex.EncodeToString(k[:]))) } } var wg sync.WaitGroup @@ -243,7 +325,7 @@ func TestUpDown(t *testing.T) { // 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) + pair := genTestPair(t, true, false) done := make(chan struct{}) const warmupIters = 10 @@ -324,7 +406,7 @@ func TestConcurrencySafety(t *testing.T) { } func BenchmarkLatency(b *testing.B) { - pair := genTestPair(b, true) + pair := genTestPair(b, true, false) // Establish a connection. pair.Send(b, Ping, nil) @@ -338,7 +420,7 @@ func BenchmarkLatency(b *testing.B) { } func BenchmarkThroughput(b *testing.B) { - pair := genTestPair(b, true) + pair := genTestPair(b, true, false) // Establish a connection. pair.Send(b, Ping, nil) @@ -382,7 +464,7 @@ func BenchmarkThroughput(b *testing.B) { } func BenchmarkUAPIGet(b *testing.B) { - pair := genTestPair(b, true) + pair := genTestPair(b, true, false) pair.Send(b, Ping, nil) pair.Send(b, Pong, nil) b.ReportAllocs() @@ -423,29 +505,41 @@ type fakeBindSized struct { size int } -func (b *fakeBindSized) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) { +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) 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 } + +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) 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 (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{} diff --git a/device/keypair.go b/device/keypair.go index e3540d7..73e69af 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -11,7 +11,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/replay" + "github.com/amnezia-vpn/amnezia-wg/replay" ) /* Due to limitations in Go and /x/crypto there is currently diff --git a/device/noise-protocol.go b/device/noise-protocol.go index e8f6145..75c1d87 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - "golang.zx2c4.com/wireguard/tai64n" + "github.com/amnezia-vpn/amnezia-wg/tai64n" ) type handshakeState int @@ -52,11 +52,11 @@ const ( WGLabelCookie = "cookie--" ) -const ( - MessageInitiationType = 1 - MessageResponseType = 2 - MessageCookieReplyType = 3 - MessageTransportType = 4 +var ( + MessageInitiationType uint32 = 1 + MessageResponseType uint32 = 2 + MessageCookieReplyType uint32 = 3 + MessageTransportType uint32 = 4 ) const ( @@ -75,6 +75,10 @@ const ( MessageTransportOffsetContent = 16 ) +var packetSizeToMsgType map[int]uint32 + +var msgTypeToJunkSize map[uint32]int + /* Type is an 8-bit field, followed by 3 nul bytes, * by marshalling the messages in little-endian byteorder * we can treat these as a 32-bit unsigned int (for now) @@ -193,10 +197,12 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e handshake.mixHash(handshake.remoteStatic[:]) + device.aSecMux.RLock() msg := MessageInitiation{ Type: MessageInitiationType, Ephemeral: handshake.localEphemeral.publicKey(), } + device.aSecMux.RUnlock() handshake.mixKey(msg.Ephemeral[:]) handshake.mixHash(msg.Ephemeral[:]) @@ -250,9 +256,12 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { chainKey [blake2s.Size]byte ) + device.aSecMux.RLock() if msg.Type != MessageInitiationType { + device.aSecMux.RUnlock() return nil } + device.aSecMux.RUnlock() device.staticIdentity.RLock() defer device.staticIdentity.RUnlock() @@ -367,7 +376,9 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } var msg MessageResponse + device.aSecMux.RLock() msg.Type = MessageResponseType + device.aSecMux.RUnlock() msg.Sender = handshake.localIndex msg.Receiver = handshake.remoteIndex @@ -417,9 +428,12 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { + device.aSecMux.RLock() if msg.Type != MessageResponseType { + device.aSecMux.RUnlock() return nil } + device.aSecMux.RUnlock() // lookup handshake by receiver diff --git a/device/noise_test.go b/device/noise_test.go index 2dd5324..2363365 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -10,8 +10,8 @@ import ( "encoding/binary" "testing" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/tun/tuntest" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/tun/tuntest" ) func TestCurveWrappers(t *testing.T) { diff --git a/device/peer.go b/device/peer.go index 0ac4896..72c7d1a 100644 --- a/device/peer.go +++ b/device/peer.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" + "github.com/amnezia-vpn/amnezia-wg/conn" ) type Peer struct { diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index 3d80ead..4adb687 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -5,7 +5,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/amnezia-vpn/amnezia-wg/conn" /* Reduce memory consumption for Android */ diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index ea763d0..4ee2966 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -7,7 +7,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/amnezia-vpn/amnezia-wg/conn" const ( QueueStagedSize = conn.IdealBatchSize diff --git a/device/receive.go b/device/receive.go index e24d29f..ca71539 100644 --- a/device/receive.go +++ b/device/receive.go @@ -13,10 +13,10 @@ import ( "sync" "time" + "github.com/amnezia-vpn/amnezia-wg/conn" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/conn" ) type QueueHandshakeElement struct { @@ -66,7 +66,10 @@ func (peer *Peer) keepKeyFreshReceiving() { * Every time the bind is updated a new routine is started for * 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() defer func() { device.log.Verbosef("Routine: receive incoming %s - stopped", recvName) @@ -122,6 +125,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive } deathSpiral = 0 + device.aSecMux.RLock() // handle each packet in the batch for i, size := range sizes[:count] { if size < MinMessageSize { @@ -131,8 +135,29 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive // check size of packet packet := bufsArrs[i][:size] - msgType := binary.LittleEndian.Uint32(packet[:4]) - + var msgType uint32 + if device.isAdvancedSecurityOn() { + if assumedMsgType, ok := packetSizeToMsgType[size]; ok { + junkSize := msgTypeToJunkSize[assumedMsgType] + // transport size can align with other header types; + // making sure we have the right msgType + msgType = binary.LittleEndian.Uint32(packet[junkSize:junkSize+4]) + if msgType == assumedMsgType { + packet = packet[junkSize:] + } else { + device.log.Verbosef("Transport packet lined up with another msg type") + msgType = binary.LittleEndian.Uint32(packet[:4]) + } + } else { + msgType = binary.LittleEndian.Uint32(packet[:4]) + if msgType != MessageTransportType { + device.log.Verbosef("ASec: Received message with unknown type") + continue + } + } + } else { + msgType = binary.LittleEndian.Uint32(packet[:4]) + } switch msgType { // check if transport @@ -217,6 +242,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive default: } } + device.aSecMux.RUnlock() for peer, elems := range elemsByPeer { if peer.isRunning.Load() { peer.queue.inbound.c <- elems @@ -275,6 +301,8 @@ func (device *Device) RoutineHandshake(id int) { for elem := range device.queue.handshake.c { + device.aSecMux.RLock() + // handle cookie fields and ratelimiting switch elem.msgType { @@ -302,9 +330,14 @@ func (device *Device) RoutineHandshake(id int) { // consume reply 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) { - device.log.Verbosef("Could not decrypt invalid cookie response") + device.log.Verbosef( + "Could not decrypt invalid cookie response", + ) } } @@ -346,9 +379,7 @@ func (device *Device) RoutineHandshake(id int) { switch elem.msgType { case MessageInitiationType: - // unmarshal - var msg MessageInitiation reader := bytes.NewReader(elem.packet) err := binary.Read(reader, binary.LittleEndian, &msg) @@ -358,7 +389,6 @@ func (device *Device) RoutineHandshake(id int) { } // consume initiation - peer := device.ConsumeMessageInitiation(&msg) if peer == nil { device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString()) @@ -423,6 +453,7 @@ func (device *Device) RoutineHandshake(id int) { peer.SendKeepalive() } skip: + device.aSecMux.RUnlock() device.PutMessageBuffer(elem.buffer) } } @@ -503,11 +534,17 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { } default: - device.log.Verbosef("Packet with invalid IP version from %v", peer) + device.log.Verbosef( + "Packet with invalid IP version from %v", + peer, + ) continue } - bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)]) + bufs = append( + bufs, + elem.buffer[:MessageTransportOffsetContent+len(elem.packet)], + ) } if len(bufs) > 0 { _, err := device.tun.device.Write(bufs, MessageTransportOffsetContent) diff --git a/device/send.go b/device/send.go index d22bf26..6f70d54 100644 --- a/device/send.go +++ b/device/send.go @@ -9,15 +9,16 @@ import ( "bytes" "encoding/binary" "errors" + "math/rand" "net" "os" "sync" "time" + "github.com/amnezia-vpn/amnezia-wg/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/tun" ) /* Outbound flow @@ -119,17 +120,44 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { peer.device.log.Errorf("%v - Failed to create initiation message: %v", peer, err) return err } - + var sendBuffer [][]byte + // so only packet processed for cookie generation + var junkedHeader []byte + if peer.device.isAdvancedSecurityOn() { + peer.device.aSecMux.RLock() + junks, err := peer.createJunkPackets() + if err != nil { + peer.device.aSecMux.RUnlock() + peer.device.log.Errorf("%v - %v", peer, err) + return err + } + sendBuffer = append(sendBuffer, junks...) + if peer.device.aSecCfg.initPacketJunkSize != 0 { + buf := make([]byte, 0, peer.device.aSecCfg.initPacketJunkSize) + writer := bytes.NewBuffer(buf[:0]) + err = appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) + if err != nil { + peer.device.aSecMux.RUnlock() + peer.device.log.Errorf("%v - %v", peer, err) + return err + } + junkedHeader = writer.Bytes() + } + peer.device.aSecMux.RUnlock() + } var buf [MessageInitiationSize]byte writer := bytes.NewBuffer(buf[:0]) binary.Write(writer, binary.LittleEndian, msg) packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) + junkedHeader = append(junkedHeader, packet...) peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - - err = peer.SendBuffers([][]byte{packet}) + + sendBuffer = append(sendBuffer, junkedHeader) + + err = peer.SendBuffers(sendBuffer) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -150,12 +178,29 @@ func (peer *Peer) SendHandshakeResponse() error { peer.device.log.Errorf("%v - Failed to create response message: %v", peer, err) return err } - + var junkedHeader []byte + if peer.device.isAdvancedSecurityOn() { + peer.device.aSecMux.RLock() + if peer.device.aSecCfg.responsePacketJunkSize != 0 { + buf := make([]byte, 0, peer.device.aSecCfg.responsePacketJunkSize) + writer := bytes.NewBuffer(buf[:0]) + err = appendJunk(writer, peer.device.aSecCfg.responsePacketJunkSize) + if err != nil { + peer.device.aSecMux.RUnlock() + peer.device.log.Errorf("%v - %v", peer, err) + return err + } + junkedHeader = writer.Bytes() + } + peer.device.aSecMux.RUnlock() + } var buf [MessageResponseSize]byte writer := bytes.NewBuffer(buf[:0]) + binary.Write(writer, binary.LittleEndian, response) packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) + junkedHeader = append(junkedHeader, packet...) err = peer.BeginSymmetricSession() if err != nil { @@ -168,18 +213,24 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketSent() // TODO: allocation could be avoided - err = peer.SendBuffers([][]byte{packet}) + err = peer.SendBuffers([][]byte{junkedHeader}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } return err } -func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) error { +func (device *Device) SendHandshakeCookie( + initiatingElem *QueueHandshakeElement, +) error { device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8]) - reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes()) + reply, err := device.cookieChecker.CreateReply( + initiatingElem.packet, + sender, + initiatingElem.endpoint.DstToBytes(), + ) if err != nil { device.log.Errorf("Failed to create cookie reply: %v", err) return err @@ -404,6 +455,31 @@ top: } } +func (peer *Peer) createJunkPackets() ([][]byte, error) { + if peer.device.aSecCfg.junkPacketCount == 0 { + return nil, nil + } + + junks := make([][]byte, 0, peer.device.aSecCfg.junkPacketCount) + for i := 0; i < peer.device.aSecCfg.junkPacketCount; i++ { + packetSize := rand.Intn( + peer.device.aSecCfg.junkPacketMaxSize-peer.device.aSecCfg.junkPacketMinSize, + ) + peer.device.aSecCfg.junkPacketMinSize + + junk, err := randomJunkWithSize(packetSize) + if err != nil { + peer.device.log.Errorf( + "%v - Failed to create junk packet: %v", + peer, + err, + ) + return nil, err + } + junks = append(junks, junk) + } + return junks, nil +} + func (peer *Peer) FlushStagedPackets() { for { select { @@ -459,18 +535,16 @@ func (device *Device) RoutineEncryption(id int) { binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) // pad content to multiple of 16 - paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load())) + paddingSize := calculatePaddingSize( + len(elem.packet), + int(device.tun.mtu.Load()), + ) elem.packet = append(elem.packet, paddingZeros[:paddingSize]...) // encrypt content and release to consumer binary.LittleEndian.PutUint64(nonce[4:], elem.nonce) - elem.packet = elem.keypair.send.Seal( - header, - nonce[:], - elem.packet, - nil, - ) + elem.packet = elem.keypair.send.Seal(header, nonce[:], elem.packet, nil) elem.Unlock() } } diff --git a/device/sticky_default.go b/device/sticky_default.go index 1038256..940702c 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -3,8 +3,8 @@ package device import ( - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/sticky_linux.go b/device/sticky_linux.go index f9230f8..5c17480 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -20,8 +20,8 @@ import ( "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/tun.go b/device/tun.go index 2a2ace9..efc543d 100644 --- a/device/tun.go +++ b/device/tun.go @@ -8,7 +8,7 @@ package device import ( "fmt" - "golang.zx2c4.com/wireguard/tun" + "github.com/amnezia-vpn/amnezia-wg/tun" ) const DefaultMTU = 1420 diff --git a/device/uapi.go b/device/uapi.go index 617dcd3..bfd005a 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "golang.zx2c4.com/wireguard/ipc" + "github.com/amnezia-vpn/amnezia-wg/ipc" ) type IPCError struct { @@ -97,6 +97,36 @@ func (device *Device) IpcGetOperation(w io.Writer) error { sendf("fwmark=%d", device.net.fwmark) } + if device.isAdvancedSecurityOn() { + if device.aSecCfg.junkPacketCount != 0 { + sendf("jc=%d", device.aSecCfg.junkPacketCount) + } + if device.aSecCfg.junkPacketMinSize != 0 { + sendf("jmin=%d", device.aSecCfg.junkPacketMinSize) + } + if device.aSecCfg.junkPacketMaxSize != 0 { + sendf("jmax=%d", device.aSecCfg.junkPacketMaxSize) + } + if device.aSecCfg.initPacketJunkSize != 0 { + sendf("s1=%d", device.aSecCfg.initPacketJunkSize) + } + if device.aSecCfg.responsePacketJunkSize != 0 { + sendf("s2=%d", device.aSecCfg.responsePacketJunkSize) + } + if device.aSecCfg.initPacketMagicHeader != 0 { + sendf("h1=%d", device.aSecCfg.initPacketMagicHeader) + } + if device.aSecCfg.responsePacketMagicHeader != 0 { + sendf("h2=%d", device.aSecCfg.responsePacketMagicHeader) + } + if device.aSecCfg.underloadPacketMagicHeader != 0 { + sendf("h3=%d", device.aSecCfg.underloadPacketMagicHeader) + } + if device.aSecCfg.transportPacketMagicHeader != 0 { + sendf("h4=%d", device.aSecCfg.transportPacketMagicHeader) + } + } + for _, peer := range device.peers.keyMap { // Serialize peer state. // Do the work in an anonymous function so that we can use defer. @@ -121,10 +151,13 @@ func (device *Device) IpcGetOperation(w io.Writer) error { sendf("rx_bytes=%d", peer.rxBytes.Load()) sendf("persistent_keepalive_interval=%d", peer.persistentKeepaliveInterval.Load()) - device.allowedips.EntriesForPeer(peer, func(prefix netip.Prefix) bool { - sendf("allowed_ip=%s", prefix.String()) - return true - }) + device.allowedips.EntriesForPeer( + peer, + func(prefix netip.Prefix) bool { + sendf("allowed_ip=%s", prefix.String()) + return true + }, + ) }() } }() @@ -152,17 +185,26 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { peer := new(ipcSetPeer) deviceConfig := true + tempASecCfg := aSecCfgType{} scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if line == "" { // Blank line means terminate operation. + err := device.handlePostConfig(&tempASecCfg) + if err != nil { + return err + } peer.handlePostConfig() return nil } key, value, ok := strings.Cut(line, "=") 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" { @@ -180,7 +222,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { var err error if deviceConfig { - err = device.handleDeviceLine(key, value) + err = device.handleDeviceLine(key, value, &tempASecCfg) } else { err = device.handlePeerLine(peer, key, value) } @@ -188,6 +230,10 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return err } } + err = device.handlePostConfig(&tempASecCfg) + if err != nil { + return err + } peer.handlePostConfig() if err := scanner.Err(); err != nil { @@ -196,7 +242,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return nil } -func (device *Device) handleDeviceLine(key, value string) error { +func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgType) error { switch key { case "private_key": var sk NoisePrivateKey @@ -242,8 +288,75 @@ func (device *Device) handleDeviceLine(key, value string) error { device.log.Verbosef("UAPI: Removing all peers") device.RemoveAllPeers() + case "jc": + junkPacketCount, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_count %w", err) + } + device.log.Verbosef("UAPI: Updating junk_packet_count") + tempASecCfg.junkPacketCount = junkPacketCount + + case "jmin": + junkPacketMinSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_min_size %w", err) + } + device.log.Verbosef("UAPI: Updating junk_packet_min_size") + tempASecCfg.junkPacketMinSize = junkPacketMinSize + + case "jmax": + junkPacketMaxSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_max_size %w", err) + } + device.log.Verbosef("UAPI: Updating junk_packet_max_size") + tempASecCfg.junkPacketMaxSize = junkPacketMaxSize + + case "s1": + initPacketJunkSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse init_packet_junk_size %w", err) + } + device.log.Verbosef("UAPI: Updating init_packet_junk_size") + tempASecCfg.initPacketJunkSize = initPacketJunkSize + + case "s2": + responsePacketJunkSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse response_packet_junk_size %w", err) + } + device.log.Verbosef("UAPI: Updating response_packet_junk_size") + tempASecCfg.responsePacketJunkSize = responsePacketJunkSize + + case "h1": + initPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse init_packet_magic_header %w", err) + } + tempASecCfg.initPacketMagicHeader = uint32(initPacketMagicHeader) + + case "h2": + responsePacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse response_packet_magic_header %w", err) + } + tempASecCfg.responsePacketMagicHeader = uint32(responsePacketMagicHeader) + + case "h3": + underloadPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse underload_packet_magic_header %w", err) + } + tempASecCfg.underloadPacketMagicHeader = uint32(underloadPacketMagicHeader) + + case "h4": + transportPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse transport_packet_magic_header %w", err) + } + tempASecCfg.transportPacketMagicHeader = uint32(transportPacketMagicHeader) default: - return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) + return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v",key) } return nil @@ -262,7 +375,8 @@ func (peer *ipcSetPeer) handlePostConfig() { return } if peer.created { - peer.disableRoaming = peer.device.net.brokenRoaming && peer.endpoint != nil + peer.disableRoaming = peer.device.net.brokenRoaming && + peer.endpoint != nil } if peer.device.isUp() { peer.Start() @@ -273,7 +387,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. var publicKey NoisePublicKey err := publicKey.FromHex(value) @@ -303,7 +420,10 @@ func (device *Device) handlePublicKeyLine(peer *ipcSetPeer, value string) error return nil } -func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error { +func (device *Device) handlePeerLine( + peer *ipcSetPeer, + key, value string, +) error { switch key { case "update_only": // allow disabling of creation @@ -343,7 +463,7 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error device.log.Verbosef("%v - UAPI: Updating endpoint", peer.Peer) endpoint, err := device.net.bind.ParseEndpoint(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "failed to set endpoint %v: %w", value, err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to set endpoint %v: %w", value, err) } peer.Lock() defer peer.Unlock() diff --git a/device/util.go b/device/util.go new file mode 100644 index 0000000..aab8ab7 --- /dev/null +++ b/device/util.go @@ -0,0 +1,25 @@ +package device + +import ( + "bytes" + crand "crypto/rand" + "fmt" +) + +func appendJunk(writer *bytes.Buffer, size int) error { + headerJunk, err := randomJunkWithSize(size) + if err != nil { + return fmt.Errorf("failed to create header junk: %v", err) + } + _, err = writer.Write(headerJunk) + if err != nil { + return fmt.Errorf("failed to write header junk: %v", err) + } + return nil +} + +func randomJunkWithSize(size int) ([]byte, error) { + junk := make([]byte, size) + _, err := crand.Read(junk) + return junk, err +} diff --git a/device/util_test.go b/device/util_test.go new file mode 100644 index 0000000..c061eef --- /dev/null +++ b/device/util_test.go @@ -0,0 +1,27 @@ +package device + +import ( + "bytes" + "fmt" + "testing" +) + +func Test_randomJunktWithSize(t *testing.T) { + junk, err := randomJunkWithSize(30) + fmt.Println(string(junk), len(junk), err) +} + +func Test_appendJunk(t *testing.T) { + t.Run("", func(t *testing.T) { + s := "apple" + buffer := bytes.NewBuffer([]byte(s)) + err := appendJunk(buffer, 30) + if err != nil && + buffer.Len() != len(s)+30 { + t.Errorf("appendWithJunk() size don't match") + } + read := make([]byte, 50) + buffer.Read(read) + fmt.Println(string(read)) + }) +} diff --git a/go.mod b/go.mod index c04e1bb..4a3c9c6 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module golang.zx2c4.com/wireguard +module github.com/amnezia-vpn/amnezia-wg go 1.20 require ( + github.com/tevino/abool/v2 v2.1.0 golang.org/x/crypto v0.6.0 golang.org/x/net v0.7.0 golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89 diff --git a/go.sum b/go.sum index cfeaee6..3707808 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= +github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= diff --git a/ipc/namedpipe/namedpipe_test.go b/ipc/namedpipe/namedpipe_test.go index 998453b..d4799e1 100644 --- a/ipc/namedpipe/namedpipe_test.go +++ b/ipc/namedpipe/namedpipe_test.go @@ -20,8 +20,8 @@ import ( "testing" "time" + "github.com/amnezia-vpn/amnezia-wg/ipc/namedpipe" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/ipc/namedpipe" ) func randomPipePath() string { diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 1562a18..721c404 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -9,8 +9,8 @@ import ( "net" "os" + "github.com/amnezia-vpn/amnezia-wg/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/rwcancel" ) type UAPIListener struct { diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index aa023c9..97a4123 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -8,8 +8,8 @@ package ipc import ( "net" + "github.com/amnezia-vpn/amnezia-wg/ipc/namedpipe" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/ipc/namedpipe" ) // TODO: replace these with actual standard windows error numbers from the win package diff --git a/main.go b/main.go index e016116..ea7ef4e 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "runtime" "strconv" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/device" + "github.com/amnezia-vpn/amnezia-wg/ipc" + "github.com/amnezia-vpn/amnezia-wg/tun" "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 ( diff --git a/main_windows.go b/main_windows.go index a4dc46f..d00b146 100644 --- a/main_windows.go +++ b/main_windows.go @@ -12,11 +12,11 @@ import ( "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/ipc" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/device" + "github.com/amnezia-vpn/amnezia-wg/ipc" - "golang.zx2c4.com/wireguard/tun" + "github.com/amnezia-vpn/amnezia-wg/tun" ) const ( diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go index ccd32ed..ed40904 100644 --- a/tun/netstack/examples/http_client.go +++ b/tun/netstack/examples/http_client.go @@ -13,9 +13,9 @@ import ( "net/http" "net/netip" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/device" + "github.com/amnezia-vpn/amnezia-wg/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go index f5b7a8f..d5e7094 100644 --- a/tun/netstack/examples/http_server.go +++ b/tun/netstack/examples/http_server.go @@ -14,9 +14,9 @@ import ( "net/http" "net/netip" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/device" + "github.com/amnezia-vpn/amnezia-wg/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go index 2eef0fb..9f917db 100644 --- a/tun/netstack/examples/ping_client.go +++ b/tun/netstack/examples/ping_client.go @@ -17,9 +17,9 @@ import ( "golang.org/x/net/icmp" "golang.org/x/net/ipv4" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/device" + "github.com/amnezia-vpn/amnezia-wg/tun/netstack" ) func main() { diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 596cfcd..f5a40f5 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -22,7 +22,7 @@ import ( "syscall" "time" - "golang.zx2c4.com/wireguard/tun" + "github.com/amnezia-vpn/amnezia-wg/tun" "golang.org/x/net/dns/dnsmessage" "gvisor.dev/gvisor/pkg/bufferv2" diff --git a/tun/tcp_offload_linux.go b/tun/tcp_offload_linux.go index 39a7180..a43f0df 100644 --- a/tun/tcp_offload_linux.go +++ b/tun/tcp_offload_linux.go @@ -12,8 +12,8 @@ import ( "io" "unsafe" + "github.com/amnezia-vpn/amnezia-wg/conn" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" ) const tcpFlagsOffset = 13 diff --git a/tun/tcp_offload_linux_test.go b/tun/tcp_offload_linux_test.go index 9160e18..57c6a09 100644 --- a/tun/tcp_offload_linux_test.go +++ b/tun/tcp_offload_linux_test.go @@ -9,8 +9,8 @@ import ( "net/netip" "testing" + "github.com/amnezia-vpn/amnezia-wg/conn" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" ) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 12cd49f..31c1513 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -17,9 +17,9 @@ import ( "time" "unsafe" + "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amnezia-wg/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" ) const ( diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go index d07e860..7068d9b 100644 --- a/tun/tuntest/tuntest.go +++ b/tun/tuntest/tuntest.go @@ -11,7 +11,7 @@ import ( "net/netip" "os" - "golang.zx2c4.com/wireguard/tun" + "github.com/amnezia-vpn/amnezia-wg/tun" ) func Ping(dst, src netip.Addr) []byte { From f30419e0d14ba692e0974a65e0514ca4571feee4 Mon Sep 17 00:00:00 2001 From: Mazay B Date: Mon, 9 Oct 2023 13:22:49 +0100 Subject: [PATCH 014/173] Manage advanced sec via uapi --- device/device.go | 61 +++++++++++++++++++++--------------------------- device/send.go | 20 ++++++++++------ device/uapi.go | 14 +++++++++-- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/device/device.go b/device/device.go index 10365d1..a10187b 100644 --- a/device/device.go +++ b/device/device.go @@ -98,6 +98,7 @@ type Device struct { } type aSecCfgType struct { + isSet bool junkPacketCount int junkPacketMinSize int junkPacketMaxSize int @@ -545,7 +546,7 @@ func (device *Device) BindUpdate() error { // start receiving routines device.net.stopping.Add(len(recvFns)) device.queue.decryption.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.decryption - device.queue.handshake.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.handshake + device.queue.handshake.wg.Add(len(recvFns)) // each RoutineReceiveIncoming goroutine writes to device.queue.handshake batchSize := netc.bind.BatchSize() for _, fn := range recvFns { go device.RoutineReceiveIncoming(batchSize, fn) @@ -565,25 +566,17 @@ func (device *Device) isAdvancedSecurityOn() bool { return device.isASecOn.IsSet() } -func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { - - if tempASecCfg.junkPacketCount == 0 && - tempASecCfg.junkPacketMaxSize == 0 && - tempASecCfg.junkPacketMinSize == 0 && - tempASecCfg.initPacketJunkSize == 0 && - tempASecCfg.responsePacketJunkSize == 0 && - tempASecCfg.initPacketMagicHeader == 0 && - tempASecCfg.responsePacketMagicHeader == 0 && - tempASecCfg.underloadPacketMagicHeader == 0 && - tempASecCfg.transportPacketMagicHeader == 0 { +func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { + + if !tempASecCfg.isSet { return err } - + isASecOn := false device.aSecMux.Lock() if tempASecCfg.junkPacketCount < 0 { err = ipcErrorf( - ipc.IpcErrorInvalid, + ipc.IpcErrorInvalid, "JunkPacketCount should be non negative", ) } @@ -591,24 +584,24 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { if tempASecCfg.junkPacketCount != 0 { isASecOn = true } - + device.aSecCfg.junkPacketMinSize = tempASecCfg.junkPacketMinSize if tempASecCfg.junkPacketMinSize != 0 { isASecOn = true } - if device.aSecCfg.junkPacketCount > 0 && + if device.aSecCfg.junkPacketCount > 0 && tempASecCfg.junkPacketMaxSize == tempASecCfg.junkPacketMinSize { - + tempASecCfg.junkPacketMaxSize++ // to make rand gen work } - if tempASecCfg.junkPacketMaxSize >= MaxSegmentSize{ + if tempASecCfg.junkPacketMaxSize >= MaxSegmentSize { device.aSecCfg.junkPacketMinSize = 0 device.aSecCfg.junkPacketMaxSize = 1 if err != nil { err = ipcErrorf( - ipc.IpcErrorInvalid, + ipc.IpcErrorInvalid, "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d; %w", tempASecCfg.junkPacketMaxSize, MaxSegmentSize, @@ -616,7 +609,7 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { ) } else { err = ipcErrorf( - ipc.IpcErrorInvalid, + ipc.IpcErrorInvalid, "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", tempASecCfg.junkPacketMaxSize, MaxSegmentSize, @@ -625,18 +618,18 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { } else if tempASecCfg.junkPacketMaxSize < tempASecCfg.junkPacketMinSize { if err != nil { err = ipcErrorf( - ipc.IpcErrorInvalid, + ipc.IpcErrorInvalid, "maxSize: %d; should be greater than minSize: %d; %w", tempASecCfg.junkPacketMaxSize, - tempASecCfg.junkPacketMinSize, + tempASecCfg.junkPacketMinSize, err, ) } else { err = ipcErrorf( - ipc.IpcErrorInvalid, + ipc.IpcErrorInvalid, "maxSize: %d; should be greater than minSize: %d", tempASecCfg.junkPacketMaxSize, - tempASecCfg.junkPacketMinSize, + tempASecCfg.junkPacketMinSize, ) } } else { @@ -664,10 +657,10 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { MaxSegmentSize, ) } - } else { + } else { device.aSecCfg.initPacketJunkSize = tempASecCfg.initPacketJunkSize } - + if tempASecCfg.initPacketJunkSize != 0 { isASecOn = true } @@ -689,7 +682,7 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { MaxSegmentSize, ) } - } else { + } else { device.aSecCfg.responsePacketJunkSize = tempASecCfg.responsePacketJunkSize } @@ -706,7 +699,7 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { device.log.Verbosef("UAPI: Using default init type") MessageInitiationType = 1 } - + if tempASecCfg.responsePacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating response_packet_magic_header") @@ -716,7 +709,7 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { device.log.Verbosef("UAPI: Using default response type") MessageResponseType = 2 } - + if tempASecCfg.underloadPacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating underload_packet_magic_header") @@ -787,14 +780,14 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { newResponseSize, ) } - } else { + } else { packetSizeToMsgType = map[int]uint32{ - newInitSize: MessageInitiationType, - newResponseSize: MessageResponseType, + newInitSize: MessageInitiationType, + newResponseSize: MessageResponseType, MessageCookieReplySize: MessageCookieReplyType, MessageTransportSize: MessageTransportType, } - + msgTypeToJunkSize = map[uint32]int{ MessageInitiationType: device.aSecCfg.initPacketJunkSize, MessageResponseType: device.aSecCfg.responsePacketJunkSize, @@ -805,6 +798,6 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { device.isASecOn.SetTo(isASecOn) device.aSecMux.Unlock() - + return err } diff --git a/device/send.go b/device/send.go index 6f70d54..b5c8e10 100644 --- a/device/send.go +++ b/device/send.go @@ -126,25 +126,31 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { if peer.device.isAdvancedSecurityOn() { peer.device.aSecMux.RLock() junks, err := peer.createJunkPackets() + peer.device.aSecMux.RUnlock() + if err != nil { - peer.device.aSecMux.RUnlock() peer.device.log.Errorf("%v - %v", peer, err) return err } - sendBuffer = append(sendBuffer, junks...) + + err = peer.SendBuffers(junks) + if err != nil { + peer.device.log.Errorf("%v - Failed to send junk packets: %v", peer, err) + return err + } + if peer.device.aSecCfg.initPacketJunkSize != 0 { buf := make([]byte, 0, peer.device.aSecCfg.initPacketJunkSize) writer := bytes.NewBuffer(buf[:0]) err = appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) if err != nil { - peer.device.aSecMux.RUnlock() peer.device.log.Errorf("%v - %v", peer, err) return err } junkedHeader = writer.Bytes() } - peer.device.aSecMux.RUnlock() } + var buf [MessageInitiationSize]byte writer := bytes.NewBuffer(buf[:0]) binary.Write(writer, binary.LittleEndian, msg) @@ -154,9 +160,9 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - + sendBuffer = append(sendBuffer, junkedHeader) - + err = peer.SendBuffers(sendBuffer) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) @@ -191,7 +197,7 @@ func (peer *Peer) SendHandshakeResponse() error { return err } junkedHeader = writer.Bytes() - } + } peer.device.aSecMux.RUnlock() } var buf [MessageResponseSize]byte diff --git a/device/uapi.go b/device/uapi.go index bfd005a..653803c 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -295,6 +295,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy } device.log.Verbosef("UAPI: Updating junk_packet_count") tempASecCfg.junkPacketCount = junkPacketCount + tempASecCfg.isSet = true case "jmin": junkPacketMinSize, err := strconv.Atoi(value) @@ -303,6 +304,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy } device.log.Verbosef("UAPI: Updating junk_packet_min_size") tempASecCfg.junkPacketMinSize = junkPacketMinSize + tempASecCfg.isSet = true case "jmax": junkPacketMaxSize, err := strconv.Atoi(value) @@ -311,6 +313,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy } device.log.Verbosef("UAPI: Updating junk_packet_max_size") tempASecCfg.junkPacketMaxSize = junkPacketMaxSize + tempASecCfg.isSet = true case "s1": initPacketJunkSize, err := strconv.Atoi(value) @@ -319,6 +322,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy } device.log.Verbosef("UAPI: Updating init_packet_junk_size") tempASecCfg.initPacketJunkSize = initPacketJunkSize + tempASecCfg.isSet = true case "s2": responsePacketJunkSize, err := strconv.Atoi(value) @@ -327,6 +331,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy } device.log.Verbosef("UAPI: Updating response_packet_junk_size") tempASecCfg.responsePacketJunkSize = responsePacketJunkSize + tempASecCfg.isSet = true case "h1": initPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) @@ -334,6 +339,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse init_packet_magic_header %w", err) } tempASecCfg.initPacketMagicHeader = uint32(initPacketMagicHeader) + tempASecCfg.isSet = true case "h2": responsePacketMagicHeader, err := strconv.ParseUint(value, 10, 32) @@ -341,6 +347,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse response_packet_magic_header %w", err) } tempASecCfg.responsePacketMagicHeader = uint32(responsePacketMagicHeader) + tempASecCfg.isSet = true case "h3": underloadPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) @@ -348,6 +355,7 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse underload_packet_magic_header %w", err) } tempASecCfg.underloadPacketMagicHeader = uint32(underloadPacketMagicHeader) + tempASecCfg.isSet = true case "h4": transportPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) @@ -355,8 +363,10 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse transport_packet_magic_header %w", err) } tempASecCfg.transportPacketMagicHeader = uint32(transportPacketMagicHeader) + tempASecCfg.isSet = true + default: - return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v",key) + return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) } return nil @@ -463,7 +473,7 @@ func (device *Device) handlePeerLine( device.log.Verbosef("%v - UAPI: Updating endpoint", peer.Peer) endpoint, err := device.net.bind.ParseEndpoint(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "failed to set endpoint %v: %w", value, err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to set endpoint %v: %w", value, err) } peer.Lock() defer peer.Unlock() From b81ca925dbeb9ba775dcf2ad38b54f24e256a6e2 Mon Sep 17 00:00:00 2001 From: Mazay B Date: Sat, 14 Oct 2023 11:42:30 +0100 Subject: [PATCH 015/173] peer.device.aSecMux.RLock added --- device/send.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/device/send.go b/device/send.go index b5c8e10..c60342e 100644 --- a/device/send.go +++ b/device/send.go @@ -139,16 +139,19 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } + peer.device.aSecMux.RLock() if peer.device.aSecCfg.initPacketJunkSize != 0 { buf := make([]byte, 0, peer.device.aSecCfg.initPacketJunkSize) writer := bytes.NewBuffer(buf[:0]) err = appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) if err != nil { peer.device.log.Errorf("%v - %v", peer, err) + peer.device.aSecMux.RUnlock() return err } junkedHeader = writer.Bytes() } + peer.device.aSecMux.RUnlock() } var buf [MessageInitiationSize]byte From c493b95f66b9ddad97fa782d49a74eaa185ed4a3 Mon Sep 17 00:00:00 2001 From: pokamest Date: Wed, 25 Oct 2023 22:41:33 +0100 Subject: [PATCH 016/173] Update README.md Signed-off-by: pokamest --- README.md | 57 ++++++++++++++++--------------------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 074f7ec..717c4c5 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,27 @@ -# Go Implementation of [WireGuard](https://www.wireguard.com/) +# Go Implementation of AmneziaWG -This is an implementation of WireGuard in Go. +AmneziaWG is a contemporary version of the WireGuard protocol. It's a fork of WireGuard-Go and offers protection against detection by Deep Packet Inspection (DPI) systems. At the same time, it retains the simplified architecture and high performance of the original. + +The precursor, WireGuard, is known for its efficiency but had issues with detection due to its distinctive packet signatures. +AmneziaWG addresses this problem by employing advanced obfuscation methods, allowing its traffic to blend seamlessly with regular internet traffic. +As a result, AmneziaWG maintains high performance while adding an extra layer of stealth, making it a superb choice for those seeking a fast and discreet VPN connection. ## 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: +Simply run: ``` -$ wireguard-go wg0 +$ amnezia-wg 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`: +To run amnezia-wg without forking to the background, pass `-f` or `--foreground`: ``` -$ wireguard-go -f wg0 +$ amnezia-wg -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. +When an interface is running, you may use [`amnezia-wg-tools `](https://github.com/amnezia-vpn/amnezia-wg-tools) 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`. @@ -26,52 +29,24 @@ To run with more logging you may set the environment variable `LOG_LEVEL=debug`. ### 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. +This will run on Linux; you should run amnezia-wg instead of using default linux kernel module. ### 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. +This runs on MacOS, you should use it from [awg-apple](https://github.com/amnezia-vpn/awg-apple) ### 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. +This runs on Windows, you should use it from [awg-windows](https://github.com/amnezia-vpn/awg-windows), 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 +$ git clone https://github.com/amnezia-vpn/amnezia-wg +$ cd amnezia-wg $ 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. From 24f8d7c9e7312590e1f2d8e0ca02c30b9d88e91a Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Fri, 6 Oct 2023 17:16:49 -0700 Subject: [PATCH 017/173] tun: implement UDP GSO/GRO for Linux Signed-off-by: Jordan Whited --- ...{tcp_offload_linux.go => offload_linux.go} | 595 ++++++++++++++---- ...ad_linux_test.go => offload_linux_test.go} | 405 ++++++++++-- ...65e4830d6dc087cab24cd1e154c2e790589a309b77 | 8 - ...6784411a8ce2e8e03aa3384105e581f2c67494700d | 8 - tun/tun_linux.go | 71 ++- 5 files changed, 849 insertions(+), 238 deletions(-) rename tun/{tcp_offload_linux.go => offload_linux.go} (50%) rename tun/{tcp_offload_linux_test.go => offload_linux_test.go} (52%) delete mode 100644 tun/testdata/fuzz/Fuzz_handleGRO/032aec0105f26f709c118365e4830d6dc087cab24cd1e154c2e790589a309b77 delete mode 100644 tun/testdata/fuzz/Fuzz_handleGRO/0da283f9a2098dec30d1c86784411a8ce2e8e03aa3384105e581f2c67494700d diff --git a/tun/tcp_offload_linux.go b/tun/offload_linux.go similarity index 50% rename from tun/tcp_offload_linux.go rename to tun/offload_linux.go index b023bbd..54461cc 100644 --- a/tun/tcp_offload_linux.go +++ b/tun/offload_linux.go @@ -57,22 +57,23 @@ const ( virtioNetHdrLen = int(unsafe.Sizeof(virtioNetHdr{})) ) -// flowKey represents the key for a flow. -type flowKey struct { +// tcpFlowKey represents the key for a TCP flow. +type tcpFlowKey struct { srcAddr, dstAddr [16]byte srcPort, dstPort uint16 rxAck uint32 // varying ack values should not be coalesced. Treat them as separate flows. + isV6 bool } -// tcpGROTable holds flow and coalescing information for the purposes of GRO. +// tcpGROTable holds flow and coalescing information for the purposes of TCP GRO. type tcpGROTable struct { - itemsByFlow map[flowKey][]tcpGROItem + itemsByFlow map[tcpFlowKey][]tcpGROItem itemsPool [][]tcpGROItem } func newTCPGROTable() *tcpGROTable { t := &tcpGROTable{ - itemsByFlow: make(map[flowKey][]tcpGROItem, conn.IdealBatchSize), + itemsByFlow: make(map[tcpFlowKey][]tcpGROItem, conn.IdealBatchSize), itemsPool: make([][]tcpGROItem, conn.IdealBatchSize), } for i := range t.itemsPool { @@ -81,14 +82,15 @@ func newTCPGROTable() *tcpGROTable { return t } -func newFlowKey(pkt []byte, srcAddr, dstAddr, tcphOffset int) flowKey { - key := flowKey{} - addrSize := dstAddr - srcAddr - copy(key.srcAddr[:], pkt[srcAddr:dstAddr]) - copy(key.dstAddr[:], pkt[dstAddr:dstAddr+addrSize]) +func newTCPFlowKey(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffset int) tcpFlowKey { + key := tcpFlowKey{} + addrSize := dstAddrOffset - srcAddrOffset + copy(key.srcAddr[:], pkt[srcAddrOffset:dstAddrOffset]) + copy(key.dstAddr[:], pkt[dstAddrOffset:dstAddrOffset+addrSize]) key.srcPort = binary.BigEndian.Uint16(pkt[tcphOffset:]) key.dstPort = binary.BigEndian.Uint16(pkt[tcphOffset+2:]) key.rxAck = binary.BigEndian.Uint32(pkt[tcphOffset+8:]) + key.isV6 = addrSize == 16 return key } @@ -96,7 +98,7 @@ func newFlowKey(pkt []byte, srcAddr, dstAddr, tcphOffset int) flowKey { // returning the packets found for the flow, or inserting a new one if none // is found. func (t *tcpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex int) ([]tcpGROItem, bool) { - key := newFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) + key := newTCPFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) items, ok := t.itemsByFlow[key] if ok { return items, ok @@ -108,7 +110,7 @@ func (t *tcpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, t // insert an item in the table for the provided packet and packet metadata. func (t *tcpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex int) { - key := newFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) + key := newTCPFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) item := tcpGROItem{ key: key, bufsIndex: uint16(bufsIndex), @@ -131,7 +133,7 @@ func (t *tcpGROTable) updateAt(item tcpGROItem, i int) { items[i] = item } -func (t *tcpGROTable) deleteAt(key flowKey, i int) { +func (t *tcpGROTable) deleteAt(key tcpFlowKey, i int) { items, _ := t.itemsByFlow[key] items = append(items[:i], items[i+1:]...) t.itemsByFlow[key] = items @@ -140,7 +142,7 @@ func (t *tcpGROTable) deleteAt(key flowKey, i int) { // tcpGROItem represents bookkeeping data for a TCP packet during the lifetime // of a GRO evaluation across a vector of packets. type tcpGROItem struct { - key flowKey + key tcpFlowKey sentSeq uint32 // the sequence number bufsIndex uint16 // the index into the original bufs slice numMerged uint16 // the number of packets merged into this item @@ -164,6 +166,103 @@ func (t *tcpGROTable) reset() { } } +// udpFlowKey represents the key for a UDP flow. +type udpFlowKey struct { + srcAddr, dstAddr [16]byte + srcPort, dstPort uint16 + isV6 bool +} + +// udpGROTable holds flow and coalescing information for the purposes of UDP GRO. +type udpGROTable struct { + itemsByFlow map[udpFlowKey][]udpGROItem + itemsPool [][]udpGROItem +} + +func newUDPGROTable() *udpGROTable { + u := &udpGROTable{ + itemsByFlow: make(map[udpFlowKey][]udpGROItem, conn.IdealBatchSize), + itemsPool: make([][]udpGROItem, conn.IdealBatchSize), + } + for i := range u.itemsPool { + u.itemsPool[i] = make([]udpGROItem, 0, conn.IdealBatchSize) + } + return u +} + +func newUDPFlowKey(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffset int) udpFlowKey { + key := udpFlowKey{} + addrSize := dstAddrOffset - srcAddrOffset + copy(key.srcAddr[:], pkt[srcAddrOffset:dstAddrOffset]) + copy(key.dstAddr[:], pkt[dstAddrOffset:dstAddrOffset+addrSize]) + key.srcPort = binary.BigEndian.Uint16(pkt[udphOffset:]) + key.dstPort = binary.BigEndian.Uint16(pkt[udphOffset+2:]) + key.isV6 = addrSize == 16 + return key +} + +// lookupOrInsert looks up a flow for the provided packet and metadata, +// returning the packets found for the flow, or inserting a new one if none +// is found. +func (u *udpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex int) ([]udpGROItem, bool) { + key := newUDPFlowKey(pkt, srcAddrOffset, dstAddrOffset, udphOffset) + items, ok := u.itemsByFlow[key] + if ok { + return items, ok + } + // TODO: insert() performs another map lookup. This could be rearranged to avoid. + u.insert(pkt, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex, false) + return nil, false +} + +// insert an item in the table for the provided packet and packet metadata. +func (u *udpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex int, cSumKnownInvalid bool) { + key := newUDPFlowKey(pkt, srcAddrOffset, dstAddrOffset, udphOffset) + item := udpGROItem{ + key: key, + bufsIndex: uint16(bufsIndex), + gsoSize: uint16(len(pkt[udphOffset+udphLen:])), + iphLen: uint8(udphOffset), + cSumKnownInvalid: cSumKnownInvalid, + } + items, ok := u.itemsByFlow[key] + if !ok { + items = u.newItems() + } + items = append(items, item) + u.itemsByFlow[key] = items +} + +func (u *udpGROTable) updateAt(item udpGROItem, i int) { + items, _ := u.itemsByFlow[item.key] + items[i] = item +} + +// udpGROItem represents bookkeeping data for a UDP packet during the lifetime +// of a GRO evaluation across a vector of packets. +type udpGROItem struct { + key udpFlowKey + bufsIndex uint16 // the index into the original bufs slice + numMerged uint16 // the number of packets merged into this item + gsoSize uint16 // payload size + iphLen uint8 // ip header len + cSumKnownInvalid bool // UDP header checksum validity; a false value DOES NOT imply valid, just unknown. +} + +func (u *udpGROTable) newItems() []udpGROItem { + var items []udpGROItem + items, u.itemsPool = u.itemsPool[len(u.itemsPool)-1], u.itemsPool[:len(u.itemsPool)-1] + return items +} + +func (u *udpGROTable) reset() { + for k, items := range u.itemsByFlow { + items = items[:0] + u.itemsPool = append(u.itemsPool, items) + delete(u.itemsByFlow, k) + } +} + // canCoalesce represents the outcome of checking if two TCP packets are // candidates for coalescing. type canCoalesce int @@ -174,6 +273,61 @@ const ( coalesceAppend canCoalesce = 1 ) +// ipHeadersCanCoalesce returns true if the IP headers found in pktA and pktB +// meet all requirements to be merged as part of a GRO operation, otherwise it +// returns false. +func ipHeadersCanCoalesce(pktA, pktB []byte) bool { + if len(pktA) < 9 || len(pktB) < 9 { + return false + } + if pktA[0]>>4 == 6 { + if pktA[0] != pktB[0] || pktA[1]>>4 != pktB[1]>>4 { + // cannot coalesce with unequal Traffic class values + return false + } + if pktA[7] != pktB[7] { + // cannot coalesce with unequal Hop limit values + return false + } + } else { + if pktA[1] != pktB[1] { + // cannot coalesce with unequal ToS values + return false + } + if pktA[6]>>5 != pktB[6]>>5 { + // cannot coalesce with unequal DF or reserved bits. MF is checked + // further up the stack. + return false + } + if pktA[8] != pktB[8] { + // cannot coalesce with unequal TTL values + return false + } + } + return true +} + +// udpPacketsCanCoalesce evaluates if pkt can be coalesced with the packet +// described by item. iphLen and gsoSize describe pkt. bufs is the vector of +// packets involved in the current GRO evaluation. bufsOffset is the offset at +// which packet data begins within bufs. +func udpPacketsCanCoalesce(pkt []byte, iphLen uint8, gsoSize uint16, item udpGROItem, bufs [][]byte, bufsOffset int) canCoalesce { + pktTarget := bufs[item.bufsIndex][bufsOffset:] + if !ipHeadersCanCoalesce(pkt, pktTarget) { + return coalesceUnavailable + } + if len(pktTarget[iphLen+udphLen:])%int(item.gsoSize) != 0 { + // A smaller than gsoSize packet has been appended previously. + // Nothing can come after a smaller packet on the end. + return coalesceUnavailable + } + if gsoSize > item.gsoSize { + // We cannot have a larger packet following a smaller one. + return coalesceUnavailable + } + return coalesceAppend +} + // tcpPacketsCanCoalesce evaluates if pkt can be coalesced with the packet // described by item. This function makes considerations that match the kernel's // GRO self tests, which can be found in tools/testing/selftests/net/gro.c. @@ -189,29 +343,8 @@ func tcpPacketsCanCoalesce(pkt []byte, iphLen, tcphLen uint8, seq uint32, pshSet return coalesceUnavailable } } - if pkt[0]>>4 == 6 { - if pkt[0] != pktTarget[0] || pkt[1]>>4 != pktTarget[1]>>4 { - // cannot coalesce with unequal Traffic class values - return coalesceUnavailable - } - if pkt[7] != pktTarget[7] { - // cannot coalesce with unequal Hop limit values - return coalesceUnavailable - } - } else { - if pkt[1] != pktTarget[1] { - // cannot coalesce with unequal ToS values - return coalesceUnavailable - } - if pkt[6]>>5 != pktTarget[6]>>5 { - // cannot coalesce with unequal DF or reserved bits. MF is checked - // further up the stack. - return coalesceUnavailable - } - if pkt[8] != pktTarget[8] { - // cannot coalesce with unequal TTL values - return coalesceUnavailable - } + if !ipHeadersCanCoalesce(pkt, pktTarget) { + return coalesceUnavailable } // seq adjacency lhsLen := item.gsoSize @@ -252,16 +385,16 @@ func tcpPacketsCanCoalesce(pkt []byte, iphLen, tcphLen uint8, seq uint32, pshSet return coalesceUnavailable } -func tcpChecksumValid(pkt []byte, iphLen uint8, isV6 bool) bool { +func checksumValid(pkt []byte, iphLen, proto uint8, isV6 bool) bool { srcAddrAt := ipv4SrcAddrOffset addrSize := 4 if isV6 { srcAddrAt = ipv6SrcAddrOffset addrSize = 16 } - tcpTotalLen := uint16(len(pkt) - int(iphLen)) - tcpCSum := pseudoHeaderChecksum(unix.IPPROTO_TCP, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], tcpTotalLen) - return ^checksum(pkt[iphLen:], tcpCSum) == 0 + lenForPseudo := uint16(len(pkt) - int(iphLen)) + cSum := pseudoHeaderChecksum(proto, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], lenForPseudo) + return ^checksum(pkt[iphLen:], cSum) == 0 } // coalesceResult represents the result of attempting to coalesce two TCP @@ -276,8 +409,36 @@ const ( coalesceSuccess ) +// coalesceUDPPackets attempts to coalesce pkt with the packet described by +// item, and returns the outcome. +func coalesceUDPPackets(pkt []byte, item *udpGROItem, bufs [][]byte, bufsOffset int, isV6 bool) coalesceResult { + pktHead := bufs[item.bufsIndex][bufsOffset:] // the packet that will end up at the front + headersLen := item.iphLen + udphLen + coalescedLen := len(bufs[item.bufsIndex][bufsOffset:]) + len(pkt) - int(headersLen) + + if cap(pktHead)-bufsOffset < coalescedLen { + // We don't want to allocate a new underlying array if capacity is + // too small. + return coalesceInsufficientCap + } + if item.numMerged == 0 { + if item.cSumKnownInvalid || !checksumValid(bufs[item.bufsIndex][bufsOffset:], item.iphLen, unix.IPPROTO_UDP, isV6) { + return coalesceItemInvalidCSum + } + } + if !checksumValid(pkt, item.iphLen, unix.IPPROTO_UDP, isV6) { + return coalescePktInvalidCSum + } + extendBy := len(pkt) - int(headersLen) + bufs[item.bufsIndex] = append(bufs[item.bufsIndex], make([]byte, extendBy)...) + copy(bufs[item.bufsIndex][bufsOffset+len(pktHead):], pkt[headersLen:]) + + item.numMerged++ + return coalesceSuccess +} + // coalesceTCPPackets attempts to coalesce pkt with the packet described by -// item, returning the outcome. This function may swap bufs elements in the +// item, and returns the outcome. This function may swap bufs elements in the // event of a prepend as item's bufs index is already being tracked for writing // to a Device. func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize uint16, seq uint32, pshSet bool, item *tcpGROItem, bufs [][]byte, bufsOffset int, isV6 bool) coalesceResult { @@ -297,11 +458,11 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize return coalescePSHEnding } if item.numMerged == 0 { - if !tcpChecksumValid(bufs[item.bufsIndex][bufsOffset:], item.iphLen, isV6) { + if !checksumValid(bufs[item.bufsIndex][bufsOffset:], item.iphLen, unix.IPPROTO_TCP, isV6) { return coalesceItemInvalidCSum } } - if !tcpChecksumValid(pkt, item.iphLen, isV6) { + if !checksumValid(pkt, item.iphLen, unix.IPPROTO_TCP, isV6) { return coalescePktInvalidCSum } item.sentSeq = seq @@ -319,11 +480,11 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize return coalesceInsufficientCap } if item.numMerged == 0 { - if !tcpChecksumValid(bufs[item.bufsIndex][bufsOffset:], item.iphLen, isV6) { + if !checksumValid(bufs[item.bufsIndex][bufsOffset:], item.iphLen, unix.IPPROTO_TCP, isV6) { return coalesceItemInvalidCSum } } - if !tcpChecksumValid(pkt, item.iphLen, isV6) { + if !checksumValid(pkt, item.iphLen, unix.IPPROTO_TCP, isV6) { return coalescePktInvalidCSum } if pshSet { @@ -354,52 +515,52 @@ const ( maxUint16 = 1<<16 - 1 ) -type tcpGROResult int +type groResult int const ( - tcpGROResultNoop tcpGROResult = iota - tcpGROResultTableInsert - tcpGROResultCoalesced + groResultNoop groResult = iota + groResultTableInsert + groResultCoalesced ) // tcpGRO evaluates the TCP packet at pktI in bufs for coalescing with -// existing packets tracked in table. It returns a tcpGROResultNoop when no -// action was taken, tcpGROResultTableInsert when the evaluated packet was -// inserted into table, and tcpGROResultCoalesced when the evaluated packet was +// existing packets tracked in table. It returns a groResultNoop when no +// action was taken, groResultTableInsert when the evaluated packet was +// inserted into table, and groResultCoalesced when the evaluated packet was // coalesced with another packet in table. -func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) tcpGROResult { +func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) groResult { pkt := bufs[pktI][offset:] if len(pkt) > maxUint16 { // A valid IPv4 or IPv6 packet will never exceed this. - return tcpGROResultNoop + return groResultNoop } iphLen := int((pkt[0] & 0x0F) * 4) if isV6 { iphLen = 40 ipv6HPayloadLen := int(binary.BigEndian.Uint16(pkt[4:])) if ipv6HPayloadLen != len(pkt)-iphLen { - return tcpGROResultNoop + return groResultNoop } } else { totalLen := int(binary.BigEndian.Uint16(pkt[2:])) if totalLen != len(pkt) { - return tcpGROResultNoop + return groResultNoop } } if len(pkt) < iphLen { - return tcpGROResultNoop + return groResultNoop } tcphLen := int((pkt[iphLen+12] >> 4) * 4) if tcphLen < 20 || tcphLen > 60 { - return tcpGROResultNoop + return groResultNoop } if len(pkt) < iphLen+tcphLen { - return tcpGROResultNoop + return groResultNoop } if !isV6 { if pkt[6]&ipv4FlagMoreFragments != 0 || pkt[6]<<3 != 0 || pkt[7] != 0 { // no GRO support for fragmented segments for now - return tcpGROResultNoop + return groResultNoop } } tcpFlags := pkt[iphLen+tcpFlagsOffset] @@ -407,14 +568,14 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) // not a candidate if any non-ACK flags (except PSH+ACK) are set if tcpFlags != tcpFlagACK { if pkt[iphLen+tcpFlagsOffset] != tcpFlagACK|tcpFlagPSH { - return tcpGROResultNoop + return groResultNoop } pshSet = true } gsoSize := uint16(len(pkt) - tcphLen - iphLen) // not a candidate if payload len is 0 if gsoSize < 1 { - return tcpGROResultNoop + return groResultNoop } seq := binary.BigEndian.Uint32(pkt[iphLen+4:]) srcAddrOffset := ipv4SrcAddrOffset @@ -425,7 +586,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) } items, existing := table.lookupOrInsert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) if !existing { - return tcpGROResultNoop + return groResultTableInsert } for i := len(items) - 1; i >= 0; i-- { // In the best case of packets arriving in order iterating in reverse is @@ -443,54 +604,25 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) switch result { case coalesceSuccess: table.updateAt(item, i) - return tcpGROResultCoalesced + return groResultCoalesced case coalesceItemInvalidCSum: // delete the item with an invalid csum table.deleteAt(item.key, i) case coalescePktInvalidCSum: // no point in inserting an item that we can't coalesce - return tcpGROResultNoop + return groResultNoop default: } } } // failed to coalesce with any other packets; store the item in the flow table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) - return tcpGROResultTableInsert + return groResultTableInsert } -func isTCP4NoIPOptions(b []byte) bool { - if len(b) < 40 { - return false - } - if b[0]>>4 != 4 { - return false - } - if b[0]&0x0F != 5 { - return false - } - if b[9] != unix.IPPROTO_TCP { - return false - } - return true -} - -func isTCP6NoEH(b []byte) bool { - if len(b) < 60 { - return false - } - if b[0]>>4 != 6 { - return false - } - if b[6] != unix.IPPROTO_TCP { - return false - } - return true -} - -// applyCoalesceAccounting updates bufs to account for coalescing based on the +// applyTCPCoalesceAccounting updates bufs to account for coalescing based on the // metadata found in table. -func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 bool) error { +func applyTCPCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable) error { for _, items := range table.itemsByFlow { for _, item := range items { if item.numMerged > 0 { @@ -505,7 +637,7 @@ func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 // Recalculate the total len (IPv4) or payload len (IPv6). // Recalculate the (IPv4) header checksum. - if isV6 { + if item.key.isV6 { hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV6 binary.BigEndian.PutUint16(pkt[4:], uint16(len(pkt))-uint16(item.iphLen)) // set new IPv6 header payload len } else { @@ -525,7 +657,7 @@ func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 // this with computation of the tcp header and payload checksum. addrLen := 4 addrOffset := ipv4SrcAddrOffset - if isV6 { + if item.key.isV6 { addrLen = 16 addrOffset = ipv6SrcAddrOffset } @@ -546,54 +678,244 @@ func applyCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable, isV6 return nil } +// applyUDPCoalesceAccounting updates bufs to account for coalescing based on the +// metadata found in table. +func applyUDPCoalesceAccounting(bufs [][]byte, offset int, table *udpGROTable) error { + for _, items := range table.itemsByFlow { + for _, item := range items { + if item.numMerged > 0 { + hdr := virtioNetHdr{ + flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, // this turns into CHECKSUM_PARTIAL in the skb + hdrLen: uint16(item.iphLen + udphLen), + gsoSize: item.gsoSize, + csumStart: uint16(item.iphLen), + csumOffset: 6, + } + pkt := bufs[item.bufsIndex][offset:] + + // Recalculate the total len (IPv4) or payload len (IPv6). + // Recalculate the (IPv4) header checksum. + hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_UDP_L4 + if item.key.isV6 { + binary.BigEndian.PutUint16(pkt[4:], uint16(len(pkt))-uint16(item.iphLen)) // set new IPv6 header payload len + } else { + pkt[10], pkt[11] = 0, 0 + binary.BigEndian.PutUint16(pkt[2:], uint16(len(pkt))) // set new total length + iphCSum := ^checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum + binary.BigEndian.PutUint16(pkt[10:], iphCSum) // set IPv4 header checksum field + } + err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) + if err != nil { + return err + } + + // Recalculate the UDP len field value + binary.BigEndian.PutUint16(pkt[item.iphLen+4:], uint16(len(pkt[item.iphLen:]))) + + // Calculate the pseudo header checksum and place it at the UDP + // checksum offset. Downstream checksum offloading will combine + // this with computation of the udp header and payload checksum. + addrLen := 4 + addrOffset := ipv4SrcAddrOffset + if item.key.isV6 { + addrLen = 16 + addrOffset = ipv6SrcAddrOffset + } + srcAddrAt := offset + addrOffset + srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] + dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] + psum := pseudoHeaderChecksum(unix.IPPROTO_UDP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) + binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) + } else { + hdr := virtioNetHdr{} + err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) + if err != nil { + return err + } + } + } + } + return nil +} + +type groCandidateType uint8 + +const ( + notGROCandidate groCandidateType = iota + tcp4GROCandidate + tcp6GROCandidate + udp4GROCandidate + udp6GROCandidate +) + +func packetIsGROCandidate(b []byte) groCandidateType { + if len(b) < 28 { + return notGROCandidate + } + if b[0]>>4 == 4 { + if b[0]&0x0F != 5 { + // IPv4 packets w/IP options do not coalesce + return notGROCandidate + } + if b[9] == unix.IPPROTO_TCP && len(b) >= 40 { + return tcp4GROCandidate + } + if b[9] == unix.IPPROTO_UDP { + return udp4GROCandidate + } + } else if b[0]>>4 == 6 { + if b[6] == unix.IPPROTO_TCP && len(b) >= 60 { + return tcp6GROCandidate + } + if b[6] == unix.IPPROTO_UDP && len(b) >= 48 { + return udp6GROCandidate + } + } + return notGROCandidate +} + +const ( + udphLen = 8 +) + +// udpGRO evaluates the UDP packet at pktI in bufs for coalescing with +// existing packets tracked in table. It returns a groResultNoop when no +// action was taken, groResultTableInsert when the evaluated packet was +// inserted into table, and groResultCoalesced when the evaluated packet was +// coalesced with another packet in table. +func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) groResult { + pkt := bufs[pktI][offset:] + if len(pkt) > maxUint16 { + // A valid IPv4 or IPv6 packet will never exceed this. + return groResultNoop + } + iphLen := int((pkt[0] & 0x0F) * 4) + if isV6 { + iphLen = 40 + ipv6HPayloadLen := int(binary.BigEndian.Uint16(pkt[4:])) + if ipv6HPayloadLen != len(pkt)-iphLen { + return groResultNoop + } + } else { + totalLen := int(binary.BigEndian.Uint16(pkt[2:])) + if totalLen != len(pkt) { + return groResultNoop + } + } + if len(pkt) < iphLen { + return groResultNoop + } + if len(pkt) < iphLen+udphLen { + return groResultNoop + } + if !isV6 { + if pkt[6]&ipv4FlagMoreFragments != 0 || pkt[6]<<3 != 0 || pkt[7] != 0 { + // no GRO support for fragmented segments for now + return groResultNoop + } + } + gsoSize := uint16(len(pkt) - udphLen - iphLen) + // not a candidate if payload len is 0 + if gsoSize < 1 { + return groResultNoop + } + srcAddrOffset := ipv4SrcAddrOffset + addrLen := 4 + if isV6 { + srcAddrOffset = ipv6SrcAddrOffset + addrLen = 16 + } + items, existing := table.lookupOrInsert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, pktI) + if !existing { + return groResultTableInsert + } + // With UDP we only check the last item, otherwise we could reorder packets + // for a given flow. We must also always insert a new item, or successfully + // coalesce with an existing item, for the same reason. + item := items[len(items)-1] + can := udpPacketsCanCoalesce(pkt, uint8(iphLen), gsoSize, item, bufs, offset) + var pktCSumKnownInvalid bool + if can == coalesceAppend { + result := coalesceUDPPackets(pkt, &item, bufs, offset, isV6) + switch result { + case coalesceSuccess: + table.updateAt(item, len(items)-1) + return groResultCoalesced + case coalesceItemInvalidCSum: + // If the existing item has an invalid csum we take no action. A new + // item will be stored after it, and the existing item will never be + // revisited as part of future coalescing candidacy checks. + case coalescePktInvalidCSum: + // We must insert a new item, but we also mark it as invalid csum + // to prevent a repeat checksum validation. + pktCSumKnownInvalid = true + default: + } + } + // failed to coalesce with any other packets; store the item in the flow + table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, pktI, pktCSumKnownInvalid) + return groResultTableInsert +} + // handleGRO evaluates bufs for GRO, and writes the indices of the resulting -// packets into toWrite. toWrite, tcp4Table, and tcp6Table should initially be +// packets into toWrite. toWrite, tcpTable, and udpTable should initially be // empty (but non-nil), and are passed in to save allocs as the caller may reset // and recycle them across vectors of packets. -func handleGRO(bufs [][]byte, offset int, tcp4Table, tcp6Table *tcpGROTable, toWrite *[]int) error { +func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, toWrite *[]int) error { for i := range bufs { if offset < virtioNetHdrLen || offset > len(bufs[i])-1 { return errors.New("invalid offset") } - var result tcpGROResult - switch { - case isTCP4NoIPOptions(bufs[i][offset:]): // ipv4 packets w/IP options do not coalesce - result = tcpGRO(bufs, offset, i, tcp4Table, false) - case isTCP6NoEH(bufs[i][offset:]): // ipv6 packets w/extension headers do not coalesce - result = tcpGRO(bufs, offset, i, tcp6Table, true) + var result groResult + switch packetIsGROCandidate(bufs[i][offset:]) { + case tcp4GROCandidate: + result = tcpGRO(bufs, offset, i, tcpTable, false) + case tcp6GROCandidate: + result = tcpGRO(bufs, offset, i, tcpTable, true) + case udp4GROCandidate: + result = udpGRO(bufs, offset, i, udpTable, false) + case udp6GROCandidate: + result = udpGRO(bufs, offset, i, udpTable, true) } switch result { - case tcpGROResultNoop: + case groResultNoop: hdr := virtioNetHdr{} err := hdr.encode(bufs[i][offset-virtioNetHdrLen:]) if err != nil { return err } fallthrough - case tcpGROResultTableInsert: + case groResultTableInsert: *toWrite = append(*toWrite, i) } } - err4 := applyCoalesceAccounting(bufs, offset, tcp4Table, false) - err6 := applyCoalesceAccounting(bufs, offset, tcp6Table, true) - return errors.Join(err4, err6) + errTCP := applyTCPCoalesceAccounting(bufs, offset, tcpTable) + errUDP := applyUDPCoalesceAccounting(bufs, offset, udpTable) + return errors.Join(errTCP, errUDP) } -// tcpTSO splits packets from in into outBuffs, writing the size of each +// gsoSplit splits packets from in into outBuffs, writing the size of each // element into sizes. It returns the number of buffers populated, and/or an // error. -func tcpTSO(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffset int) (int, error) { +func gsoSplit(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffset int, isV6 bool) (int, error) { iphLen := int(hdr.csumStart) srcAddrOffset := ipv6SrcAddrOffset addrLen := 16 - if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 { + if !isV6 { in[10], in[11] = 0, 0 // clear ipv4 header checksum srcAddrOffset = ipv4SrcAddrOffset addrLen = 4 } - tcpCSumAt := int(hdr.csumStart + hdr.csumOffset) - in[tcpCSumAt], in[tcpCSumAt+1] = 0, 0 // clear tcp checksum - firstTCPSeqNum := binary.BigEndian.Uint32(in[hdr.csumStart+4:]) + transportCsumAt := int(hdr.csumStart + hdr.csumOffset) + in[transportCsumAt], in[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum + var firstTCPSeqNum uint32 + var protocol uint8 + if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 || hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV6 { + protocol = unix.IPPROTO_TCP + firstTCPSeqNum = binary.BigEndian.Uint32(in[hdr.csumStart+4:]) + } else { + protocol = unix.IPPROTO_UDP + } nextSegmentDataAt := int(hdr.hdrLen) i := 0 for ; nextSegmentDataAt < len(in); i++ { @@ -610,7 +932,7 @@ func tcpTSO(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffs out := outBuffs[i][outOffset:] copy(out, in[:iphLen]) - if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 { + if !isV6 { // For IPv4 we are responsible for incrementing the ID field, // updating the total len field, and recalculating the header // checksum. @@ -627,25 +949,32 @@ func tcpTSO(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffs binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) } - // TCP header + // copy transport header copy(out[hdr.csumStart:hdr.hdrLen], in[hdr.csumStart:hdr.hdrLen]) - tcpSeq := firstTCPSeqNum + uint32(hdr.gsoSize*uint16(i)) - binary.BigEndian.PutUint32(out[hdr.csumStart+4:], tcpSeq) - if nextSegmentEnd != len(in) { - // FIN and PSH should only be set on last segment - clearFlags := tcpFlagFIN | tcpFlagPSH - out[hdr.csumStart+tcpFlagsOffset] &^= clearFlags + + if protocol == unix.IPPROTO_TCP { + // set TCP seq and adjust TCP flags + tcpSeq := firstTCPSeqNum + uint32(hdr.gsoSize*uint16(i)) + binary.BigEndian.PutUint32(out[hdr.csumStart+4:], tcpSeq) + if nextSegmentEnd != len(in) { + // FIN and PSH should only be set on last segment + clearFlags := tcpFlagFIN | tcpFlagPSH + out[hdr.csumStart+tcpFlagsOffset] &^= clearFlags + } + } else { + // set UDP header len + binary.BigEndian.PutUint16(out[hdr.csumStart+4:], uint16(segmentDataLen)+(hdr.hdrLen-hdr.csumStart)) } // payload copy(out[hdr.hdrLen:], in[nextSegmentDataAt:nextSegmentEnd]) - // TCP checksum - tcpHLen := int(hdr.hdrLen - hdr.csumStart) - tcpLenForPseudo := uint16(tcpHLen + segmentDataLen) - tcpCSum := pseudoHeaderChecksum(unix.IPPROTO_TCP, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], tcpLenForPseudo) - tcpCSum = ^checksum(out[hdr.csumStart:totalLen], tcpCSum) - binary.BigEndian.PutUint16(out[hdr.csumStart+hdr.csumOffset:], tcpCSum) + // transport checksum + transportHeaderLen := int(hdr.hdrLen - hdr.csumStart) + lenForPseudo := uint16(transportHeaderLen + segmentDataLen) + transportCSum := pseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum = ^checksum(out[hdr.csumStart:totalLen], transportCSum) + binary.BigEndian.PutUint16(out[hdr.csumStart+hdr.csumOffset:], transportCSum) nextSegmentDataAt += int(hdr.gsoSize) } diff --git a/tun/tcp_offload_linux_test.go b/tun/offload_linux_test.go similarity index 52% rename from tun/tcp_offload_linux_test.go rename to tun/offload_linux_test.go index 41fba70..192232c 100644 --- a/tun/tcp_offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -28,6 +28,71 @@ var ( ip6PortC = netip.MustParseAddrPort("[2001:db8::3]:1") ) +func udp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv4Fields)) []byte { + totalLen := 28 + payloadLen + b := make([]byte, offset+int(totalLen), 65535) + ipv4H := header.IPv4(b[offset:]) + srcAs4 := srcIPPort.Addr().As4() + dstAs4 := dstIPPort.Addr().As4() + ipFields := &header.IPv4Fields{ + SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), + DstAddr: tcpip.AddrFromSlice(dstAs4[:]), + Protocol: unix.IPPROTO_UDP, + TTL: 64, + TotalLength: uint16(totalLen), + } + if ipFn != nil { + ipFn(ipFields) + } + ipv4H.Encode(ipFields) + udpH := header.UDP(b[offset+20:]) + udpH.Encode(&header.UDPFields{ + SrcPort: srcIPPort.Port(), + DstPort: dstIPPort.Port(), + Length: uint16(payloadLen + udphLen), + }) + ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) + pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(udphLen+payloadLen)) + udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) + return b +} + +func udp6Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { + return udp6PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) +} + +func udp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv6Fields)) []byte { + totalLen := 48 + payloadLen + b := make([]byte, offset+int(totalLen), 65535) + ipv6H := header.IPv6(b[offset:]) + srcAs16 := srcIPPort.Addr().As16() + dstAs16 := dstIPPort.Addr().As16() + ipFields := &header.IPv6Fields{ + SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), + DstAddr: tcpip.AddrFromSlice(dstAs16[:]), + TransportProtocol: unix.IPPROTO_UDP, + HopLimit: 64, + PayloadLength: uint16(payloadLen + udphLen), + } + if ipFn != nil { + ipFn(ipFields) + } + ipv6H.Encode(ipFields) + udpH := header.UDP(b[offset+40:]) + udpH.Encode(&header.UDPFields{ + SrcPort: srcIPPort.Port(), + DstPort: dstIPPort.Port(), + Length: uint16(payloadLen + udphLen), + }) + pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(udphLen+payloadLen)) + udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) + return b +} + +func udp4Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { + return udp4PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) +} + func tcp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32, ipFn func(*header.IPv4Fields)) []byte { totalLen := 40 + segmentSize b := make([]byte, offset+int(totalLen), 65535) @@ -137,6 +202,34 @@ func Test_handleVirtioRead(t *testing.T) { []int{160, 160}, false, }, + { + "udp4", + virtioNetHdr{ + flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, + gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, + gsoSize: 100, + hdrLen: 28, + csumStart: 20, + csumOffset: 6, + }, + udp4Packet(ip4PortA, ip4PortB, 200), + []int{128, 128}, + false, + }, + { + "udp6", + virtioNetHdr{ + flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, + gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, + gsoSize: 100, + hdrLen: 48, + csumStart: 40, + csumOffset: 6, + }, + udp6Packet(ip6PortA, ip6PortB, 200), + []int{148, 148}, + false, + }, } for _, tt := range tests { @@ -173,6 +266,13 @@ func flipTCP4Checksum(b []byte) []byte { return b } +func flipUDP4Checksum(b []byte) []byte { + at := virtioNetHdrLen + 20 + 6 // 20 byte ipv4 header; udp csum offset is 6 + b[at] ^= 0xFF + b[at+1] ^= 0xFF + return b +} + func Fuzz_handleGRO(f *testing.F) { pkt0 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1) pkt1 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101) @@ -180,11 +280,17 @@ func Fuzz_handleGRO(f *testing.F) { pkt3 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1) pkt4 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101) pkt5 := tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201) - f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, offset) - f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5 []byte, offset int) { - pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5} + pkt6 := udp4Packet(ip4PortA, ip4PortB, 100) + pkt7 := udp4Packet(ip4PortA, ip4PortB, 100) + pkt8 := udp4Packet(ip4PortA, ip4PortC, 100) + pkt9 := udp6Packet(ip6PortA, ip6PortB, 100) + pkt10 := udp6Packet(ip6PortA, ip6PortB, 100) + pkt11 := udp6Packet(ip6PortA, ip6PortC, 100) + f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, offset) + f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, offset int) { + pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11} toWrite := make([]int, 0, len(pkts)) - handleGRO(pkts, offset, newTCPGROTable(), newTCPGROTable(), &toWrite) + handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), &toWrite) if len(toWrite) > len(pkts) { t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts)) } @@ -210,17 +316,22 @@ func Test_handleGRO(t *testing.T) { wantErr bool }{ { - "multiple flows", + "multiple protocols and flows", [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // v4 flow 2 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // v6 flow 2 + tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 + udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 + udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 + tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 + tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 + tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 + tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 + tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 + udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 + udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 + udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 }, - []int{0, 2, 3, 5}, - []int{240, 140, 260, 160}, + []int{0, 1, 2, 4, 5, 7, 9}, + []int{240, 228, 128, 140, 260, 160, 248}, false, }, { @@ -245,9 +356,12 @@ func Test_handleGRO(t *testing.T) { flipTCP4Checksum(tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)), // v4 flow 1 seq 1 len 100 tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 seq 101 len 100 tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 + flipUDP4Checksum(udp4Packet(ip4PortA, ip4PortB, 100)), + udp4Packet(ip4PortA, ip4PortB, 100), + udp4Packet(ip4PortA, ip4PortB, 100), }, - []int{0, 1}, - []int{140, 240}, + []int{0, 1, 3, 4}, + []int{140, 240, 128, 228}, false, }, { @@ -262,75 +376,99 @@ func Test_handleGRO(t *testing.T) { false, }, { - "tcp4 unequal TTL", + "unequal TTL", [][]byte{ tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { fields.TTL++ }), + udp4Packet(ip4PortA, ip4PortB, 100), + udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { + fields.TTL++ + }), }, - []int{0, 1}, - []int{140, 140}, + []int{0, 1, 2, 3}, + []int{140, 140, 128, 128}, false, }, { - "tcp4 unequal ToS", + "unequal ToS", [][]byte{ tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { fields.TOS++ }), + udp4Packet(ip4PortA, ip4PortB, 100), + udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { + fields.TOS++ + }), }, - []int{0, 1}, - []int{140, 140}, + []int{0, 1, 2, 3}, + []int{140, 140, 128, 128}, false, }, { - "tcp4 unequal flags more fragments set", + "unequal flags more fragments set", [][]byte{ tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { fields.Flags = 1 }), + udp4Packet(ip4PortA, ip4PortB, 100), + udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { + fields.Flags = 1 + }), }, - []int{0, 1}, - []int{140, 140}, + []int{0, 1, 2, 3}, + []int{140, 140, 128, 128}, false, }, { - "tcp4 unequal flags DF set", + "unequal flags DF set", [][]byte{ tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { fields.Flags = 2 }), + udp4Packet(ip4PortA, ip4PortB, 100), + udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { + fields.Flags = 2 + }), }, - []int{0, 1}, - []int{140, 140}, + []int{0, 1, 2, 3}, + []int{140, 140, 128, 128}, false, }, { - "tcp6 unequal hop limit", + "ipv6 unequal hop limit", [][]byte{ tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { fields.HopLimit++ }), + udp6Packet(ip6PortA, ip6PortB, 100), + udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { + fields.HopLimit++ + }), }, - []int{0, 1}, - []int{160, 160}, + []int{0, 1, 2, 3}, + []int{160, 160, 148, 148}, false, }, { - "tcp6 unequal traffic class", + "ipv6 unequal traffic class", [][]byte{ tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { fields.TrafficClass++ }), + udp6Packet(ip6PortA, ip6PortB, 100), + udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { + fields.TrafficClass++ + }), }, - []int{0, 1}, - []int{160, 160}, + []int{0, 1, 2, 3}, + []int{160, 160, 148, 148}, false, }, } @@ -338,7 +476,7 @@ func Test_handleGRO(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { toWrite := make([]int, 0, len(tt.pktsIn)) - err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newTCPGROTable(), &toWrite) + err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), &toWrite) if err != nil { if tt.wantErr { return @@ -360,51 +498,198 @@ func Test_handleGRO(t *testing.T) { } } -func Test_isTCP4NoIPOptions(t *testing.T) { - valid := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] - invalidLen := valid[:39] - invalidHeaderLen := make([]byte, len(valid)) - copy(invalidHeaderLen, valid) - invalidHeaderLen[0] = 0x46 - invalidProtocol := make([]byte, len(valid)) - copy(invalidProtocol, valid) - invalidProtocol[9] = unix.IPPROTO_TCP + 1 +func Test_packetIsGROCandidate(t *testing.T) { + tcp4 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] + tcp4TooShort := tcp4[:39] + ip4InvalidHeaderLen := make([]byte, len(tcp4)) + copy(ip4InvalidHeaderLen, tcp4) + ip4InvalidHeaderLen[0] = 0x46 + ip4InvalidProtocol := make([]byte, len(tcp4)) + copy(ip4InvalidProtocol, tcp4) + ip4InvalidProtocol[9] = unix.IPPROTO_GRE + + tcp6 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] + tcp6TooShort := tcp6[:59] + ip6InvalidProtocol := make([]byte, len(tcp6)) + copy(ip6InvalidProtocol, tcp6) + ip6InvalidProtocol[6] = unix.IPPROTO_GRE + + udp4 := udp4Packet(ip4PortA, ip4PortB, 100)[virtioNetHdrLen:] + udp4TooShort := udp4[:27] + + udp6 := udp6Packet(ip6PortA, ip6PortB, 100)[virtioNetHdrLen:] + udp6TooShort := udp6[:47] tests := []struct { name string b []byte - want bool + want groCandidateType }{ { - "valid", - valid, - true, + "tcp4", + tcp4, + tcp4GROCandidate, }, { - "invalid length", - invalidLen, - false, + "tcp6", + tcp6, + tcp6GROCandidate, }, { - "invalid version", + "udp4", + udp4, + udp4GROCandidate, + }, + { + "udp6", + udp6, + udp6GROCandidate, + }, + { + "udp4 too short", + udp4TooShort, + notGROCandidate, + }, + { + "udp6 too short", + udp6TooShort, + notGROCandidate, + }, + { + "tcp4 too short", + tcp4TooShort, + notGROCandidate, + }, + { + "tcp6 too short", + tcp6TooShort, + notGROCandidate, + }, + { + "invalid IP version", []byte{0x00}, - false, + notGROCandidate, }, { - "invalid header len", - invalidHeaderLen, - false, + "invalid IP header len", + ip4InvalidHeaderLen, + notGROCandidate, }, { - "invalid protocol", - invalidProtocol, - false, + "ip4 invalid protocol", + ip4InvalidProtocol, + notGROCandidate, + }, + { + "ip6 invalid protocol", + ip6InvalidProtocol, + notGROCandidate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isTCP4NoIPOptions(tt.b); got != tt.want { - t.Errorf("isTCP4NoIPOptions() = %v, want %v", got, tt.want) + if got := packetIsGROCandidate(tt.b); got != tt.want { + t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_udpPacketsCanCoalesce(t *testing.T) { + udp4a := udp4Packet(ip4PortA, ip4PortB, 100) + udp4b := udp4Packet(ip4PortA, ip4PortB, 100) + udp4c := udp4Packet(ip4PortA, ip4PortB, 110) + + type args struct { + pkt []byte + iphLen uint8 + gsoSize uint16 + item udpGROItem + bufs [][]byte + bufsOffset int + } + tests := []struct { + name string + args args + want canCoalesce + }{ + { + "coalesceAppend equal gso", + args{ + pkt: udp4a[offset:], + iphLen: 20, + gsoSize: 100, + item: udpGROItem{ + gsoSize: 100, + iphLen: 20, + }, + bufs: [][]byte{ + udp4a, + udp4b, + }, + bufsOffset: offset, + }, + coalesceAppend, + }, + { + "coalesceAppend smaller gso", + args{ + pkt: udp4a[offset : len(udp4a)-90], + iphLen: 20, + gsoSize: 10, + item: udpGROItem{ + gsoSize: 100, + iphLen: 20, + }, + bufs: [][]byte{ + udp4a, + udp4b, + }, + bufsOffset: offset, + }, + coalesceAppend, + }, + { + "coalesceUnavailable smaller gso previously appended", + args{ + pkt: udp4a[offset:], + iphLen: 20, + gsoSize: 100, + item: udpGROItem{ + gsoSize: 100, + iphLen: 20, + }, + bufs: [][]byte{ + udp4c, + udp4b, + }, + bufsOffset: offset, + }, + coalesceUnavailable, + }, + { + "coalesceUnavailable larger following smaller", + args{ + pkt: udp4c[offset:], + iphLen: 20, + gsoSize: 110, + item: udpGROItem{ + gsoSize: 100, + iphLen: 20, + }, + bufs: [][]byte{ + udp4a, + udp4c, + }, + bufsOffset: offset, + }, + coalesceUnavailable, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := udpPacketsCanCoalesce(tt.args.pkt, tt.args.iphLen, tt.args.gsoSize, tt.args.item, tt.args.bufs, tt.args.bufsOffset); got != tt.want { + t.Errorf("udpPacketsCanCoalesce() = %v, want %v", got, tt.want) } }) } diff --git a/tun/testdata/fuzz/Fuzz_handleGRO/032aec0105f26f709c118365e4830d6dc087cab24cd1e154c2e790589a309b77 b/tun/testdata/fuzz/Fuzz_handleGRO/032aec0105f26f709c118365e4830d6dc087cab24cd1e154c2e790589a309b77 deleted file mode 100644 index 5461e79..0000000 --- a/tun/testdata/fuzz/Fuzz_handleGRO/032aec0105f26f709c118365e4830d6dc087cab24cd1e154c2e790589a309b77 +++ /dev/null @@ -1,8 +0,0 @@ -go test fuzz v1 -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -int(34) diff --git a/tun/testdata/fuzz/Fuzz_handleGRO/0da283f9a2098dec30d1c86784411a8ce2e8e03aa3384105e581f2c67494700d b/tun/testdata/fuzz/Fuzz_handleGRO/0da283f9a2098dec30d1c86784411a8ce2e8e03aa3384105e581f2c67494700d deleted file mode 100644 index b441819..0000000 --- a/tun/testdata/fuzz/Fuzz_handleGRO/0da283f9a2098dec30d1c86784411a8ce2e8e03aa3384105e581f2c67494700d +++ /dev/null @@ -1,8 +0,0 @@ -go test fuzz v1 -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -[]byte("0") -int(-48) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index eb5051e..94bffcc 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -38,6 +38,7 @@ type NativeTun struct { statusListenersShutdown chan struct{} batchSize int vnetHdr bool + udpGSO bool closeOnce sync.Once @@ -48,9 +49,10 @@ type NativeTun struct { readOpMu sync.Mutex // readOpMu guards readBuff readBuff [virtioNetHdrLen + 65535]byte // if vnetHdr every read() is prefixed by virtioNetHdr - writeOpMu sync.Mutex // writeOpMu guards toWrite, tcp4GROTable, tcp6GROTable - toWrite []int - tcp4GROTable, tcp6GROTable *tcpGROTable + writeOpMu sync.Mutex // writeOpMu guards toWrite, tcpGROTable + toWrite []int + tcpGROTable *tcpGROTable + udpGROTable *udpGROTable } func (tun *NativeTun) File() *os.File { @@ -333,8 +335,8 @@ func (tun *NativeTun) nameSlow() (string, error) { func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { tun.writeOpMu.Lock() defer func() { - tun.tcp4GROTable.reset() - tun.tcp6GROTable.reset() + tun.tcpGROTable.reset() + tun.udpGROTable.reset() tun.writeOpMu.Unlock() }() var ( @@ -343,7 +345,7 @@ func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { ) tun.toWrite = tun.toWrite[:0] if tun.vnetHdr { - err := handleGRO(bufs, offset, tun.tcp4GROTable, tun.tcp6GROTable, &tun.toWrite) + err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, &tun.toWrite) if err != nil { return 0, err } @@ -394,37 +396,42 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e sizes[0] = n return 1, nil } - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 { + if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { return 0, fmt.Errorf("unsupported virtio GSO type: %d", hdr.gsoType) } ipVersion := in[0] >> 4 switch ipVersion { case 4: - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 { + if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { return 0, fmt.Errorf("ip header version: %d, GSO type: %d", ipVersion, hdr.gsoType) } case 6: - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 { + if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { return 0, fmt.Errorf("ip header version: %d, GSO type: %d", ipVersion, hdr.gsoType) } default: return 0, fmt.Errorf("invalid ip header version: %d", ipVersion) } - if len(in) <= int(hdr.csumStart+12) { - return 0, errors.New("packet is too short") - } // Don't trust hdr.hdrLen from the kernel as it can be equal to the length // of the entire first packet when the kernel is handling it as part of a - // FORWARD path. Instead, parse the TCP header length and add it onto + // FORWARD path. Instead, parse the transport header length and add it onto // csumStart, which is synonymous for IP header length. - tcpHLen := uint16(in[hdr.csumStart+12] >> 4 * 4) - if tcpHLen < 20 || tcpHLen > 60 { - // A TCP header must be between 20 and 60 bytes in length. - return 0, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) + if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 { + hdr.hdrLen = hdr.csumStart + 8 + } else { + if len(in) <= int(hdr.csumStart+12) { + return 0, errors.New("packet is too short") + } + + tcpHLen := uint16(in[hdr.csumStart+12] >> 4 * 4) + if tcpHLen < 20 || tcpHLen > 60 { + // A TCP header must be between 20 and 60 bytes in length. + return 0, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) + } + hdr.hdrLen = hdr.csumStart + tcpHLen } - hdr.hdrLen = hdr.csumStart + tcpHLen if len(in) < int(hdr.hdrLen) { return 0, fmt.Errorf("length of packet (%d) < virtioNetHdr.hdrLen (%d)", len(in), hdr.hdrLen) @@ -438,7 +445,7 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e return 0, fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(in)) } - return tcpTSO(in, hdr, bufs, sizes, offset) + return gsoSplit(in, hdr, bufs, sizes, offset, ipVersion == 6) } func (tun *NativeTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) { @@ -497,7 +504,8 @@ func (tun *NativeTun) BatchSize() int { const ( // TODO: support TSO with ECN bits - tunOffloads = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6 + tunTCPOffloads = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6 + tunUDPOffloads = unix.TUN_F_USO4 | unix.TUN_F_USO6 ) func (tun *NativeTun) initFromFlags(name string) error { @@ -519,12 +527,17 @@ func (tun *NativeTun) initFromFlags(name string) error { } got := ifr.Uint16() if got&unix.IFF_VNET_HDR != 0 { - err = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunOffloads) + // tunTCPOffloads were added in Linux v2.6. We require their support + // if IFF_VNET_HDR is set. + err = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads) if err != nil { return } tun.vnetHdr = true tun.batchSize = conn.IdealBatchSize + // tunUDPOffloads were added in Linux v6.2. We do not return an + // error if they are unsupported at runtime. + tun.udpGSO = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) == nil } else { tun.batchSize = 1 } @@ -575,8 +588,8 @@ func CreateTUNFromFile(file *os.File, mtu int) (Device, error) { events: make(chan Event, 5), errors: make(chan error, 5), statusListenersShutdown: make(chan struct{}), - tcp4GROTable: newTCPGROTable(), - tcp6GROTable: newTCPGROTable(), + tcpGROTable: newTCPGROTable(), + udpGROTable: newUDPGROTable(), toWrite: make([]int, 0, conn.IdealBatchSize), } @@ -628,12 +641,12 @@ func CreateUnmonitoredTUNFromFD(fd int) (Device, string, error) { } file := os.NewFile(uintptr(fd), "/dev/tun") tun := &NativeTun{ - tunFile: file, - events: make(chan Event, 5), - errors: make(chan error, 5), - tcp4GROTable: newTCPGROTable(), - tcp6GROTable: newTCPGROTable(), - toWrite: make([]int, 0, conn.IdealBatchSize), + tunFile: file, + events: make(chan Event, 5), + errors: make(chan error, 5), + tcpGROTable: newTCPGROTable(), + udpGROTable: newUDPGROTable(), + toWrite: make([]int, 0, conn.IdealBatchSize), } name, err := tun.Name() if err != nil { From db7604d1aa907a37c91164982d2a881fe6edc3c1 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 31 Oct 2023 18:08:04 -0700 Subject: [PATCH 018/173] tun: don't assume UDP GRO is supported Signed-off-by: Jordan Whited --- tun/offload_linux.go | 13 +++---- tun/offload_linux_test.go | 72 ++++++++++++++++++++++++++++++++++----- tun/tun_linux.go | 2 +- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 54461cc..9a9d38e 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -748,7 +748,7 @@ const ( udp6GROCandidate ) -func packetIsGROCandidate(b []byte) groCandidateType { +func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType { if len(b) < 28 { return notGROCandidate } @@ -760,14 +760,14 @@ func packetIsGROCandidate(b []byte) groCandidateType { if b[9] == unix.IPPROTO_TCP && len(b) >= 40 { return tcp4GROCandidate } - if b[9] == unix.IPPROTO_UDP { + if b[9] == unix.IPPROTO_UDP && canUDPGRO { return udp4GROCandidate } } else if b[0]>>4 == 6 { if b[6] == unix.IPPROTO_TCP && len(b) >= 60 { return tcp6GROCandidate } - if b[6] == unix.IPPROTO_UDP && len(b) >= 48 { + if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && canUDPGRO { return udp6GROCandidate } } @@ -860,14 +860,15 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) // handleGRO evaluates bufs for GRO, and writes the indices of the resulting // packets into toWrite. toWrite, tcpTable, and udpTable should initially be // empty (but non-nil), and are passed in to save allocs as the caller may reset -// and recycle them across vectors of packets. -func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, toWrite *[]int) error { +// and recycle them across vectors of packets. canUDPGRO indicates if UDP GRO is +// supported. +func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, canUDPGRO bool, toWrite *[]int) error { for i := range bufs { if offset < virtioNetHdrLen || offset > len(bufs[i])-1 { return errors.New("invalid offset") } var result groResult - switch packetIsGROCandidate(bufs[i][offset:]) { + switch packetIsGROCandidate(bufs[i][offset:], canUDPGRO) { case tcp4GROCandidate: result = tcpGRO(bufs, offset, i, tcpTable, false) case tcp6GROCandidate: diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go index 192232c..91f3941 100644 --- a/tun/offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -286,11 +286,11 @@ func Fuzz_handleGRO(f *testing.F) { pkt9 := udp6Packet(ip6PortA, ip6PortB, 100) pkt10 := udp6Packet(ip6PortA, ip6PortB, 100) pkt11 := udp6Packet(ip6PortA, ip6PortC, 100) - f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, offset) - f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, offset int) { + f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, true, offset) + f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, canUDPGRO bool, offset int) { pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11} toWrite := make([]int, 0, len(pkts)) - handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), &toWrite) + handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), canUDPGRO, &toWrite) if len(toWrite) > len(pkts) { t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts)) } @@ -311,6 +311,7 @@ func Test_handleGRO(t *testing.T) { tests := []struct { name string pktsIn [][]byte + canUDPGRO bool wantToWrite []int wantLens []int wantErr bool @@ -330,10 +331,31 @@ func Test_handleGRO(t *testing.T) { udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 }, + true, []int{0, 1, 2, 4, 5, 7, 9}, []int{240, 228, 128, 140, 260, 160, 248}, false, }, + { + "multiple protocols and flows no UDP GRO", + [][]byte{ + tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 + udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 + udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 + tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 + tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 + tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 + tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 + tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 + udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 + udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 + udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 + }, + false, + []int{0, 1, 2, 4, 5, 7, 8, 9, 10}, + []int{240, 128, 128, 140, 260, 160, 128, 148, 148}, + false, + }, { "PSH interleaved", [][]byte{ @@ -346,6 +368,7 @@ func Test_handleGRO(t *testing.T) { tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 201), // v6 flow 1 tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 301), // v6 flow 1 }, + true, []int{0, 2, 4, 6}, []int{240, 240, 260, 260}, false, @@ -360,6 +383,7 @@ func Test_handleGRO(t *testing.T) { udp4Packet(ip4PortA, ip4PortB, 100), udp4Packet(ip4PortA, ip4PortB, 100), }, + true, []int{0, 1, 3, 4}, []int{140, 240, 128, 228}, false, @@ -371,6 +395,7 @@ func Test_handleGRO(t *testing.T) { tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 seq 1 len 100 tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 }, + true, []int{0}, []int{340}, false, @@ -387,6 +412,7 @@ func Test_handleGRO(t *testing.T) { fields.TTL++ }), }, + true, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -403,6 +429,7 @@ func Test_handleGRO(t *testing.T) { fields.TOS++ }), }, + true, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -419,6 +446,7 @@ func Test_handleGRO(t *testing.T) { fields.Flags = 1 }), }, + true, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -435,6 +463,7 @@ func Test_handleGRO(t *testing.T) { fields.Flags = 2 }), }, + true, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -451,6 +480,7 @@ func Test_handleGRO(t *testing.T) { fields.HopLimit++ }), }, + true, []int{0, 1, 2, 3}, []int{160, 160, 148, 148}, false, @@ -467,6 +497,7 @@ func Test_handleGRO(t *testing.T) { fields.TrafficClass++ }), }, + true, []int{0, 1, 2, 3}, []int{160, 160, 148, 148}, false, @@ -476,7 +507,7 @@ func Test_handleGRO(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { toWrite := make([]int, 0, len(tt.pktsIn)) - err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), &toWrite) + err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.canUDPGRO, &toWrite) if err != nil { if tt.wantErr { return @@ -521,74 +552,99 @@ func Test_packetIsGROCandidate(t *testing.T) { udp6TooShort := udp6[:47] tests := []struct { - name string - b []byte - want groCandidateType + name string + b []byte + canUDPGRO bool + want groCandidateType }{ { "tcp4", tcp4, + true, tcp4GROCandidate, }, { "tcp6", tcp6, + true, tcp6GROCandidate, }, { "udp4", udp4, + true, udp4GROCandidate, }, + { + "udp4 no support", + udp4, + false, + notGROCandidate, + }, { "udp6", udp6, + true, udp6GROCandidate, }, + { + "udp6 no support", + udp6, + false, + notGROCandidate, + }, { "udp4 too short", udp4TooShort, + true, notGROCandidate, }, { "udp6 too short", udp6TooShort, + true, notGROCandidate, }, { "tcp4 too short", tcp4TooShort, + true, notGROCandidate, }, { "tcp6 too short", tcp6TooShort, + true, notGROCandidate, }, { "invalid IP version", []byte{0x00}, + true, notGROCandidate, }, { "invalid IP header len", ip4InvalidHeaderLen, + true, notGROCandidate, }, { "ip4 invalid protocol", ip4InvalidProtocol, + true, notGROCandidate, }, { "ip6 invalid protocol", ip6InvalidProtocol, + true, notGROCandidate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := packetIsGROCandidate(tt.b); got != tt.want { + if got := packetIsGROCandidate(tt.b, tt.canUDPGRO); got != tt.want { t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want) } }) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 94bffcc..9313ebf 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -345,7 +345,7 @@ func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { ) tun.toWrite = tun.toWrite[:0] if tun.vnetHdr { - err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, &tun.toWrite) + err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.udpGSO, &tun.toWrite) if err != nil { return 0, err } From 8cc8b8b11b1f7189f3e19616d2e233fce6ee7eda Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Mon, 20 Nov 2023 16:49:06 -0800 Subject: [PATCH 019/173] device: change Peer.endpoint locking to reduce contention Access to Peer.endpoint was previously synchronized by Peer.RWMutex. This has now moved to Peer.endpoint.Mutex. Peer.SendBuffers() is now the sole caller of Endpoint.ClearSrc(), which is signaled via a new bool, Peer.endpoint.clearSrcOnTx. Previous Callers of Endpoint.ClearSrc() now set this bool, primarily via peer.markEndpointSrcForClearing(). Peer.SetEndpointFromPacket() clears Peer.endpoint.clearSrcOnTx when an updated conn.Endpoint is stored. This maintains the same event order as before, i.e. a conn.Endpoint received after peer.endpoint.clearSrcOnTx is set, but before the next Peer.SendBuffers() call results in the latest conn.Endpoint source being used for the next packet transmission. These changes result in throughput improvements for single flow, parallel (-P n) flow, and bidirectional (--bidir) flow iperf3 TCP/UDP tests as measured on both Linux and Windows. Latency under load improves especially for high throughput Linux scenarios. These improvements are likely realized on all platforms to some degree, as the changes are not platform-specific. Co-authored-by: James Tucker Signed-off-by: Jordan Whited --- device/device.go | 12 ++-------- device/mobilequirks.go | 6 ++--- device/peer.go | 50 ++++++++++++++++++++++++++------------ device/sticky_linux.go | 30 +++++++++++------------ device/timers.go | 12 ++-------- device/uapi.go | 54 ++++++++++++++++++++---------------------- 6 files changed, 83 insertions(+), 81 deletions(-) diff --git a/device/device.go b/device/device.go index 5c666ac..86dff0d 100644 --- a/device/device.go +++ b/device/device.go @@ -461,11 +461,7 @@ func (device *Device) BindSetMark(mark uint32) error { // clear cached source addresses device.peers.RLock() for _, peer := range device.peers.keyMap { - peer.Lock() - defer peer.Unlock() - if peer.endpoint != nil { - peer.endpoint.ClearSrc() - } + peer.markEndpointSrcForClearing() } device.peers.RUnlock() @@ -515,11 +511,7 @@ func (device *Device) BindUpdate() error { // clear cached source addresses device.peers.RLock() for _, peer := range device.peers.keyMap { - peer.Lock() - defer peer.Unlock() - if peer.endpoint != nil { - peer.endpoint.ClearSrc() - } + peer.markEndpointSrcForClearing() } device.peers.RUnlock() diff --git a/device/mobilequirks.go b/device/mobilequirks.go index 4e5051d..0a0080e 100644 --- a/device/mobilequirks.go +++ b/device/mobilequirks.go @@ -11,9 +11,9 @@ func (device *Device) DisableSomeRoamingForBrokenMobileSemantics() { device.net.brokenRoaming = true device.peers.RLock() for _, peer := range device.peers.keyMap { - peer.Lock() - peer.disableRoaming = peer.endpoint != nil - peer.Unlock() + peer.endpoint.Lock() + peer.endpoint.disableRoaming = peer.endpoint.val != nil + peer.endpoint.Unlock() } device.peers.RUnlock() } diff --git a/device/peer.go b/device/peer.go index 22757d4..89b719b 100644 --- a/device/peer.go +++ b/device/peer.go @@ -17,17 +17,20 @@ import ( type Peer struct { isRunning atomic.Bool - sync.RWMutex // Mostly protects endpoint, but is generally taken whenever we modify peer keypairs Keypairs handshake Handshake device *Device - endpoint conn.Endpoint stopping sync.WaitGroup // routines pending stop txBytes atomic.Uint64 // bytes send to peer (endpoint) rxBytes atomic.Uint64 // bytes received from peer lastHandshakeNano atomic.Int64 // nano seconds since epoch - disableRoaming bool + endpoint struct { + sync.Mutex + val conn.Endpoint + clearSrcOnTx bool // signal to val.ClearSrc() prior to next packet transmission + disableRoaming bool + } timers struct { retransmitHandshake *Timer @@ -74,8 +77,6 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { // create peer peer := new(Peer) - peer.Lock() - defer peer.Unlock() peer.cookieGenerator.Init(pk) peer.device = device @@ -97,7 +98,11 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { handshake.mutex.Unlock() // reset endpoint - peer.endpoint = nil + peer.endpoint.Lock() + peer.endpoint.val = nil + peer.endpoint.disableRoaming = false + peer.endpoint.clearSrcOnTx = false + peer.endpoint.Unlock() // init timers peer.timersInit() @@ -116,14 +121,19 @@ func (peer *Peer) SendBuffers(buffers [][]byte) error { return nil } - peer.RLock() - defer peer.RUnlock() - - if peer.endpoint == nil { + peer.endpoint.Lock() + endpoint := peer.endpoint.val + if endpoint == nil { + peer.endpoint.Unlock() return errors.New("no known endpoint for peer") } + if peer.endpoint.clearSrcOnTx { + endpoint.ClearSrc() + peer.endpoint.clearSrcOnTx = false + } + peer.endpoint.Unlock() - err := peer.device.net.bind.Send(buffers, peer.endpoint) + err := peer.device.net.bind.Send(buffers, endpoint) if err == nil { var totalLen uint64 for _, b := range buffers { @@ -267,10 +277,20 @@ func (peer *Peer) Stop() { } func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) { - if peer.disableRoaming { + peer.endpoint.Lock() + defer peer.endpoint.Unlock() + if peer.endpoint.disableRoaming { return } - peer.Lock() - peer.endpoint = endpoint - peer.Unlock() + peer.endpoint.clearSrcOnTx = false + peer.endpoint.val = endpoint +} + +func (peer *Peer) markEndpointSrcForClearing() { + peer.endpoint.Lock() + defer peer.endpoint.Unlock() + if peer.endpoint.val == nil { + return + } + peer.endpoint.clearSrcOnTx = true } diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 7a519c1..6eeced2 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -110,17 +110,17 @@ func (device *Device) routineRouteListener(bind conn.Bind, netlinkSock int, netl if !ok { break } - pePtr.peer.Lock() - if &pePtr.peer.endpoint != pePtr.endpoint { - pePtr.peer.Unlock() + pePtr.peer.endpoint.Lock() + if &pePtr.peer.endpoint.val != pePtr.endpoint { + pePtr.peer.endpoint.Unlock() break } - if uint32(pePtr.peer.endpoint.(*conn.StdNetEndpoint).SrcIfidx()) == ifidx { - pePtr.peer.Unlock() + if uint32(pePtr.peer.endpoint.val.(*conn.StdNetEndpoint).SrcIfidx()) == ifidx { + pePtr.peer.endpoint.Unlock() break } - pePtr.peer.endpoint.(*conn.StdNetEndpoint).ClearSrc() - pePtr.peer.Unlock() + pePtr.peer.endpoint.clearSrcOnTx = true + pePtr.peer.endpoint.Unlock() } attr = attr[attrhdr.Len:] } @@ -134,18 +134,18 @@ func (device *Device) routineRouteListener(bind conn.Bind, netlinkSock int, netl device.peers.RLock() i := uint32(1) for _, peer := range device.peers.keyMap { - peer.RLock() - if peer.endpoint == nil { - peer.RUnlock() + peer.endpoint.Lock() + if peer.endpoint.val == nil { + peer.endpoint.Unlock() continue } - nativeEP, _ := peer.endpoint.(*conn.StdNetEndpoint) + nativeEP, _ := peer.endpoint.val.(*conn.StdNetEndpoint) if nativeEP == nil { - peer.RUnlock() + peer.endpoint.Unlock() continue } if nativeEP.DstIP().Is6() || nativeEP.SrcIfidx() == 0 { - peer.RUnlock() + peer.endpoint.Unlock() break } nlmsg := struct { @@ -188,10 +188,10 @@ func (device *Device) routineRouteListener(bind conn.Bind, netlinkSock int, netl reqPeerLock.Lock() reqPeer[i] = peerEndpointPtr{ peer: peer, - endpoint: &peer.endpoint, + endpoint: &peer.endpoint.val, } reqPeerLock.Unlock() - peer.RUnlock() + peer.endpoint.Unlock() i++ _, err := netlinkCancel.Write((*[unsafe.Sizeof(nlmsg)]byte)(unsafe.Pointer(&nlmsg))[:]) if err != nil { diff --git a/device/timers.go b/device/timers.go index e28732c..d4a4ed4 100644 --- a/device/timers.go +++ b/device/timers.go @@ -100,11 +100,7 @@ func expiredRetransmitHandshake(peer *Peer) { peer.device.log.Verbosef("%s - Handshake did not complete after %d seconds, retrying (try %d)", peer, int(RekeyTimeout.Seconds()), peer.timers.handshakeAttempts.Load()+1) /* We clear the endpoint address src address, in case this is the cause of trouble. */ - peer.Lock() - if peer.endpoint != nil { - peer.endpoint.ClearSrc() - } - peer.Unlock() + peer.markEndpointSrcForClearing() peer.SendHandshakeInitiation(true) } @@ -123,11 +119,7 @@ func expiredSendKeepalive(peer *Peer) { func expiredNewHandshake(peer *Peer) { peer.device.log.Verbosef("%s - Retrying handshake because we stopped hearing back after %d seconds", peer, int((KeepaliveTimeout + RekeyTimeout).Seconds())) /* We clear the endpoint address src address, in case this is the cause of trouble. */ - peer.Lock() - if peer.endpoint != nil { - peer.endpoint.ClearSrc() - } - peer.Unlock() + peer.markEndpointSrcForClearing() peer.SendHandshakeInitiation(false) } diff --git a/device/uapi.go b/device/uapi.go index 2a91a93..4987cda 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -99,33 +99,31 @@ func (device *Device) IpcGetOperation(w io.Writer) error { for _, peer := range device.peers.keyMap { // Serialize peer state. - // Do the work in an anonymous function so that we can use defer. - func() { - peer.RLock() - defer peer.RUnlock() + peer.handshake.mutex.RLock() + keyf("public_key", (*[32]byte)(&peer.handshake.remoteStatic)) + keyf("preshared_key", (*[32]byte)(&peer.handshake.presharedKey)) + peer.handshake.mutex.RUnlock() + sendf("protocol_version=1") + peer.endpoint.Lock() + if peer.endpoint.val != nil { + sendf("endpoint=%s", peer.endpoint.val.DstToString()) + } + peer.endpoint.Unlock() - keyf("public_key", (*[32]byte)(&peer.handshake.remoteStatic)) - keyf("preshared_key", (*[32]byte)(&peer.handshake.presharedKey)) - sendf("protocol_version=1") - if peer.endpoint != nil { - sendf("endpoint=%s", peer.endpoint.DstToString()) - } + nano := peer.lastHandshakeNano.Load() + secs := nano / time.Second.Nanoseconds() + nano %= time.Second.Nanoseconds() - nano := peer.lastHandshakeNano.Load() - secs := nano / time.Second.Nanoseconds() - nano %= time.Second.Nanoseconds() + sendf("last_handshake_time_sec=%d", secs) + sendf("last_handshake_time_nsec=%d", nano) + sendf("tx_bytes=%d", peer.txBytes.Load()) + sendf("rx_bytes=%d", peer.rxBytes.Load()) + sendf("persistent_keepalive_interval=%d", peer.persistentKeepaliveInterval.Load()) - sendf("last_handshake_time_sec=%d", secs) - sendf("last_handshake_time_nsec=%d", nano) - sendf("tx_bytes=%d", peer.txBytes.Load()) - sendf("rx_bytes=%d", peer.rxBytes.Load()) - sendf("persistent_keepalive_interval=%d", peer.persistentKeepaliveInterval.Load()) - - device.allowedips.EntriesForPeer(peer, func(prefix netip.Prefix) bool { - sendf("allowed_ip=%s", prefix.String()) - return true - }) - }() + device.allowedips.EntriesForPeer(peer, func(prefix netip.Prefix) bool { + sendf("allowed_ip=%s", prefix.String()) + return true + }) } }() @@ -262,7 +260,7 @@ func (peer *ipcSetPeer) handlePostConfig() { return } if peer.created { - peer.disableRoaming = peer.device.net.brokenRoaming && peer.endpoint != nil + peer.endpoint.disableRoaming = peer.device.net.brokenRoaming && peer.endpoint.val != nil } if peer.device.isUp() { peer.Start() @@ -345,9 +343,9 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error if err != nil { return ipcErrorf(ipc.IpcErrorInvalid, "failed to set endpoint %v: %w", value, err) } - peer.Lock() - defer peer.Unlock() - peer.endpoint = endpoint + peer.endpoint.Lock() + defer peer.endpoint.Unlock() + peer.endpoint.val = endpoint case "persistent_keepalive_interval": device.log.Verbosef("%v - UAPI: Updating persistent keepalive interval", peer.Peer) From cc193a0b327276d902b3b688d3a06a857b17fcb7 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 7 Nov 2023 15:24:21 -0800 Subject: [PATCH 020/173] device: reduce redundant per-packet overhead in RX path Peer.RoutineSequentialReceiver() deals with packet vectors and does not need to perform timer and endpoint operations for every packet in a given vector. Changing these per-packet operations to per-vector improves throughput by as much as 10% in some environments. Signed-off-by: Jordan Whited --- device/receive.go | 21 +++++++++++++++------ device/send.go | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/device/receive.go b/device/receive.go index da663e9..af2db44 100644 --- a/device/receive.go +++ b/device/receive.go @@ -445,7 +445,9 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { return } elemsContainer.Lock() - for _, elem := range elemsContainer.elems { + validTailPacket := -1 + dataPacketReceived := false + for i, elem := range elemsContainer.elems { if elem.packet == nil { // decryption failed continue @@ -455,21 +457,19 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { continue } - peer.SetEndpointFromPacket(elem.endpoint) + validTailPacket = i if peer.ReceivedWithKeypair(elem.keypair) { + peer.SetEndpointFromPacket(elem.endpoint) peer.timersHandshakeComplete() peer.SendStagedPackets() } - peer.keepKeyFreshReceiving() - peer.timersAnyAuthenticatedPacketTraversal() - peer.timersAnyAuthenticatedPacketReceived() peer.rxBytes.Add(uint64(len(elem.packet) + MinMessageSize)) if len(elem.packet) == 0 { device.log.Verbosef("%v - Receiving keepalive packet", peer) continue } - peer.timersDataReceived() + dataPacketReceived = true switch elem.packet[0] >> 4 { case 4: @@ -512,6 +512,15 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)]) } + 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() { diff --git a/device/send.go b/device/send.go index 95a5cbe..2701f4e 100644 --- a/device/send.go +++ b/device/send.go @@ -436,7 +436,7 @@ func calculatePaddingSize(packetSize, mtu int) int { return paddedSize - lastUnit } -/* Encrypts the elems in the queue +/* Encrypts the elements in the queue * and marks them for sequential consumption (by releasing the mutex) * * Obs. One instance per core @@ -495,7 +495,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { return } if !peer.isRunning.Load() { - // peer has been stopped; return re-usable elemsContainer to the shared pool. + // 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. From 015e11875d52955de1d627142159535a6001fe1c Mon Sep 17 00:00:00 2001 From: tiaga Date: Tue, 19 Dec 2023 18:46:04 +0700 Subject: [PATCH 021/173] Add Dockerfile Build Docker image with the corresponding wg-tools version. --- Dockerfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a0d63da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.20 as awg +COPY . /awg +WORKDIR /awg +RUN go mod download && \ + go mod verify && \ + go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin + +FROM alpine:3.15 as awg-tools +ARG AWGTOOLS_RELEASE="1.0.20231215" +RUN apk --no-cache add linux-headers build-base bash && \ + wget https://github.com/amnezia-vpn/amnezia-wg-tools/archive/refs/tags/v${AWGTOOLS_RELEASE}.zip && \ + unzip v${AWGTOOLS_RELEASE}.zip && \ + cd amnezia-wg-tools-${AWGTOOLS_RELEASE}/src && \ + make -e LDFLAGS=-static && \ + make install + +FROM alpine:3.15 +RUN apk --no-cache add iproute2 bash +COPY --from=awg /usr/bin/amnezia-wg /usr/bin/wireguard-go +COPY --from=awg-tools /usr/bin/wg /usr/bin/wg-quick /usr/bin/ From e5f355e843a71a0492b9201884f028a01197473b Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Sun, 14 Jan 2024 18:22:02 +0300 Subject: [PATCH 022/173] Fix incorrect configuration handling for zero-valued Jc --- device/send.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/device/send.go b/device/send.go index 8191f07..db4e1a1 100644 --- a/device/send.go +++ b/device/send.go @@ -137,10 +137,13 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - err = peer.SendBuffers(junks) - if err != nil { - peer.device.log.Errorf("%v - Failed to send junk packets: %v", peer, err) - return err + if len(junks) > 0 { + err = peer.SendBuffers(junks) + + if err != nil { + peer.device.log.Errorf("%v - Failed to send junk packets: %v", peer, err) + return err + } } peer.device.aSecMux.RLock() From e3c9ec801293387e5fb761c078a11a20cd9a6a5c Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Fri, 19 Jan 2024 15:08:27 +0300 Subject: [PATCH 023/173] Naming unify --- .gitignore | 2 +- Dockerfile | 8 ++++---- Makefile | 10 +++++----- README.md | 14 +++++++------- conn/bind_windows.go | 2 +- conn/bindtest/bindtest.go | 2 +- device/bind_test.go | 2 +- device/device.go | 10 +++++----- device/device_test.go | 18 ++++++++++-------- device/keypair.go | 2 +- device/noise-protocol.go | 2 +- device/noise_test.go | 4 ++-- device/peer.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/receive.go | 4 ++-- device/send.go | 4 ++-- device/sticky_default.go | 4 ++-- device/sticky_linux.go | 4 ++-- device/tun.go | 2 +- device/uapi.go | 2 +- go.mod | 2 +- ipc/namedpipe/namedpipe_test.go | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_unix.go | 2 +- ipc/uapi_windows.go | 2 +- main.go | 8 ++++---- main_windows.go | 8 ++++---- tun/netstack/examples/http_client.go | 6 +++--- tun/netstack/examples/http_server.go | 6 +++--- tun/netstack/examples/ping_client.go | 6 +++--- tun/netstack/tun.go | 2 +- tun/offload_linux.go | 2 +- tun/offload_linux_test.go | 2 +- tun/tun_linux.go | 4 ++-- tun/tuntest/tuntest.go | 2 +- 36 files changed, 80 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 71549f4..c6bbd9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -wireguard-go \ No newline at end of file +amneziawg-go \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a0d63da..cbf05b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,13 @@ RUN go mod download && \ FROM alpine:3.15 as awg-tools ARG AWGTOOLS_RELEASE="1.0.20231215" RUN apk --no-cache add linux-headers build-base bash && \ - wget https://github.com/amnezia-vpn/amnezia-wg-tools/archive/refs/tags/v${AWGTOOLS_RELEASE}.zip && \ + wget https://github.com/amnezia-vpn/amneziawg-tools/archive/refs/tags/v${AWGTOOLS_RELEASE}.zip && \ unzip v${AWGTOOLS_RELEASE}.zip && \ - cd amnezia-wg-tools-${AWGTOOLS_RELEASE}/src && \ + cd amneziawg-tools-${AWGTOOLS_RELEASE}/src && \ make -e LDFLAGS=-static && \ make install FROM alpine:3.15 RUN apk --no-cache add iproute2 bash -COPY --from=awg /usr/bin/amnezia-wg /usr/bin/wireguard-go -COPY --from=awg-tools /usr/bin/wg /usr/bin/wg-quick /usr/bin/ +COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go +COPY --from=awg-tools /usr/bin/awg /usr/bin/awg-quick /usr/bin/ diff --git a/Makefile b/Makefile index 3f6e407..4087cba 100644 --- a/Makefile +++ b/Makefile @@ -14,18 +14,18 @@ generate-version-and-build: [ "$$(cat version.go 2>/dev/null)" != "$$ver" ] && \ echo "$$ver" > version.go && \ git update-index --assume-unchanged version.go || true - @$(MAKE) wireguard-go + @$(MAKE) amneziawg-go -wireguard-go: $(wildcard *.go) $(wildcard */*.go) +amneziawg-go: $(wildcard *.go) $(wildcard */*.go) go build -v -o "$@" -install: wireguard-go - @install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 "$<" "$(DESTDIR)$(BINDIR)/wireguard-go" +install: amneziawg-go + @install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 "$<" "$(DESTDIR)$(BINDIR)/amneziawg-go" test: go test ./... clean: - rm -f wireguard-go + rm -f amneziawg-go .PHONY: all clean test install generate-version-and-build diff --git a/README.md b/README.md index 717c4c5..ab6f62b 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,17 @@ As a result, AmneziaWG maintains high performance while adding an extra layer of Simply run: ``` -$ amnezia-wg wg0 +$ amneziawg-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. +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/amneziawg/wg0.sock`, which will result in amneziawg-go shutting down. -To run amnezia-wg without forking to the background, pass `-f` or `--foreground`: +To run amneziawg-go without forking to the background, pass `-f` or `--foreground`: ``` -$ amnezia-wg -f wg0 +$ amneziawg-go -f wg0 ``` -When an interface is running, you may use [`amnezia-wg-tools `](https://github.com/amnezia-vpn/amnezia-wg-tools) to configure it, as well as the usual `ip(8)` and `ifconfig(8)` commands. +When an interface is running, you may use [`amnezia-wg-tools `](https://github.com/amnezia-vpn/amneziawg-go-tools) 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`. @@ -46,7 +46,7 @@ This runs on Windows, you should use it from [awg-windows](https://github.com/am This requires an installation of the latest version of [Go](https://go.dev/). ``` -$ git clone https://github.com/amnezia-vpn/amnezia-wg -$ cd amnezia-wg +$ git clone https://github.com/amnezia-vpn/amneziawg-go +$ cd amneziawg-go $ make ``` diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 9bad0ee..6cfa099 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -17,7 +17,7 @@ import ( "golang.org/x/sys/windows" - "github.com/amnezia-vpn/amnezia-wg/conn/winrio" + "github.com/amnezia-vpn/amneziawg-go/conn/winrio" ) const ( diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 713c371..42b0bb7 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -12,7 +12,7 @@ import ( "net/netip" "os" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" ) type ChannelBind struct { diff --git a/device/bind_test.go b/device/bind_test.go index eae36c2..34d1c4a 100644 --- a/device/bind_test.go +++ b/device/bind_test.go @@ -8,7 +8,7 @@ package device import ( "errors" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" ) type DummyDatagram struct { diff --git a/device/device.go b/device/device.go index eded424..a9d6281 100644 --- a/device/device.go +++ b/device/device.go @@ -11,11 +11,11 @@ import ( "sync/atomic" "time" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/ipc" - "github.com/amnezia-vpn/amnezia-wg/ratelimiter" - "github.com/amnezia-vpn/amnezia-wg/rwcancel" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/ipc" + "github.com/amnezia-vpn/amneziawg-go/ratelimiter" + "github.com/amnezia-vpn/amneziawg-go/rwcancel" + "github.com/amnezia-vpn/amneziawg-go/tun" "github.com/tevino/abool/v2" ) diff --git a/device/device_test.go b/device/device_test.go index afa1dc3..e6664a6 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -20,10 +20,10 @@ import ( "testing" "time" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/conn/bindtest" - "github.com/amnezia-vpn/amnezia-wg/tun" - "github.com/amnezia-vpn/amnezia-wg/tun/tuntest" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/conn/bindtest" + "github.com/amnezia-vpn/amneziawg-go/tun" + "github.com/amnezia-vpn/amneziawg-go/tun/tuntest" ) // uapiCfg returns a string that contains cfg formatted use with IpcSet. @@ -237,7 +237,7 @@ func genTestPair( 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))) + 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() @@ -294,7 +294,7 @@ func TestUpDown(t *testing.T) { pair := genTestPair(t, false, 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[:]))) + pair[i].dev.IpcSet(fmt.Sprintf("public_key=%s\npersistent_keepalive_interval=1\n", hex.EncodeToString(k[:]))) } } var wg sync.WaitGroup @@ -513,7 +513,7 @@ func (b *fakeBindSized) Open( func (b *fakeBindSized) Close() error { return nil } -func (b *fakeBindSized) SetMark(mark uint32) error {return nil } +func (b *fakeBindSized) SetMark(mark uint32) error { return nil } func (b *fakeBindSized) Send(bufs [][]byte, ep conn.Endpoint) error { return nil } @@ -527,7 +527,9 @@ type fakeTUNDeviceSized struct { 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) 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 } diff --git a/device/keypair.go b/device/keypair.go index 73e69af..cc2941a 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -11,7 +11,7 @@ import ( "sync/atomic" "time" - "github.com/amnezia-vpn/amnezia-wg/replay" + "github.com/amnezia-vpn/amneziawg-go/replay" ) /* Due to limitations in Go and /x/crypto there is currently diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 75c1d87..1289249 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - "github.com/amnezia-vpn/amnezia-wg/tai64n" + "github.com/amnezia-vpn/amneziawg-go/tai64n" ) type handshakeState int diff --git a/device/noise_test.go b/device/noise_test.go index 2363365..075b6d3 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -10,8 +10,8 @@ import ( "encoding/binary" "testing" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/tun/tuntest" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/tun/tuntest" ) func TestCurveWrappers(t *testing.T) { diff --git a/device/peer.go b/device/peer.go index 98bc0ec..5bc8ca4 100644 --- a/device/peer.go +++ b/device/peer.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" ) type Peer struct { diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index d29dbc8..1bff95a 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -5,7 +5,7 @@ package device -import "github.com/amnezia-vpn/amnezia-wg/conn" +import "github.com/amnezia-vpn/amneziawg-go/conn" /* Reduce memory consumption for Android */ diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index 4ee2966..0061b63 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -7,7 +7,7 @@ package device -import "github.com/amnezia-vpn/amnezia-wg/conn" +import "github.com/amnezia-vpn/amneziawg-go/conn" const ( QueueStagedSize = conn.IdealBatchSize diff --git a/device/receive.go b/device/receive.go index 06d092e..66c1a32 100644 --- a/device/receive.go +++ b/device/receive.go @@ -13,7 +13,7 @@ import ( "sync" "time" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" @@ -145,7 +145,7 @@ func (device *Device) RoutineReceiveIncoming( junkSize := msgTypeToJunkSize[assumedMsgType] // transport size can align with other header types; // making sure we have the right msgType - msgType = binary.LittleEndian.Uint32(packet[junkSize:junkSize+4]) + msgType = binary.LittleEndian.Uint32(packet[junkSize : junkSize+4]) if msgType == assumedMsgType { packet = packet[junkSize:] } else { diff --git a/device/send.go b/device/send.go index db4e1a1..1b4406d 100644 --- a/device/send.go +++ b/device/send.go @@ -15,8 +15,8 @@ import ( "sync" "time" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" diff --git a/device/sticky_default.go b/device/sticky_default.go index 940702c..da776e8 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -3,8 +3,8 @@ package device import ( - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/rwcancel" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 070986c..63164a7 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -20,8 +20,8 @@ import ( "golang.org/x/sys/unix" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/rwcancel" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/tun.go b/device/tun.go index efc543d..600a5e5 100644 --- a/device/tun.go +++ b/device/tun.go @@ -8,7 +8,7 @@ package device import ( "fmt" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/tun" ) const DefaultMTU = 1420 diff --git a/device/uapi.go b/device/uapi.go index 02a9fb7..777bdda 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "github.com/amnezia-vpn/amnezia-wg/ipc" + "github.com/amnezia-vpn/amneziawg-go/ipc" ) type IPCError struct { diff --git a/go.mod b/go.mod index 97cba1c..2df4282 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/amnezia-vpn/amnezia-wg +module github.com/amnezia-vpn/amneziawg-go go 1.20 diff --git a/ipc/namedpipe/namedpipe_test.go b/ipc/namedpipe/namedpipe_test.go index d4799e1..9f9cd6a 100644 --- a/ipc/namedpipe/namedpipe_test.go +++ b/ipc/namedpipe/namedpipe_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "github.com/amnezia-vpn/amnezia-wg/ipc/namedpipe" + "github.com/amnezia-vpn/amneziawg-go/ipc/namedpipe" "golang.org/x/sys/windows" ) diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 721c404..9738aea 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -9,7 +9,7 @@ import ( "net" "os" - "github.com/amnezia-vpn/amnezia-wg/rwcancel" + "github.com/amnezia-vpn/amneziawg-go/rwcancel" "golang.org/x/sys/unix" ) diff --git a/ipc/uapi_unix.go b/ipc/uapi_unix.go index e67be26..0da452a 100644 --- a/ipc/uapi_unix.go +++ b/ipc/uapi_unix.go @@ -26,7 +26,7 @@ const ( // socketDirectory is variable because it is modified by a linker // flag in wireguard-android. -var socketDirectory = "/var/run/wireguard" +var socketDirectory = "/var/run/amneziawg" func sockPath(iface string) string { return fmt.Sprintf("%s/%s.sock", socketDirectory, iface) diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index 97a4123..bfe7965 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -8,7 +8,7 @@ package ipc import ( "net" - "github.com/amnezia-vpn/amnezia-wg/ipc/namedpipe" + "github.com/amnezia-vpn/amneziawg-go/ipc/namedpipe" "golang.org/x/sys/windows" ) diff --git a/main.go b/main.go index ea7ef4e..775372c 100644 --- a/main.go +++ b/main.go @@ -14,10 +14,10 @@ import ( "runtime" "strconv" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/device" - "github.com/amnezia-vpn/amnezia-wg/ipc" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/ipc" + "github.com/amnezia-vpn/amneziawg-go/tun" "golang.org/x/sys/unix" ) diff --git a/main_windows.go b/main_windows.go index d00b146..807f6e2 100644 --- a/main_windows.go +++ b/main_windows.go @@ -12,11 +12,11 @@ import ( "golang.org/x/sys/windows" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/device" - "github.com/amnezia-vpn/amnezia-wg/ipc" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/ipc" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/tun" ) const ( diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go index ed40904..4c4ea12 100644 --- a/tun/netstack/examples/http_client.go +++ b/tun/netstack/examples/http_client.go @@ -13,9 +13,9 @@ import ( "net/http" "net/netip" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/device" - "github.com/amnezia-vpn/amnezia-wg/tun/netstack" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go index d5e7094..09929e0 100644 --- a/tun/netstack/examples/http_server.go +++ b/tun/netstack/examples/http_server.go @@ -14,9 +14,9 @@ import ( "net/http" "net/netip" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/device" - "github.com/amnezia-vpn/amnezia-wg/tun/netstack" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" ) func main() { diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go index 9f917db..d7897b2 100644 --- a/tun/netstack/examples/ping_client.go +++ b/tun/netstack/examples/ping_client.go @@ -17,9 +17,9 @@ import ( "golang.org/x/net/icmp" "golang.org/x/net/ipv4" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/device" - "github.com/amnezia-vpn/amnezia-wg/tun/netstack" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" ) func main() { diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index b5e6145..2275173 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -22,7 +22,7 @@ import ( "syscall" "time" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/tun" "golang.org/x/net/dns/dnsmessage" "gvisor.dev/gvisor/pkg/buffer" diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 551f14d..89cf024 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -12,7 +12,7 @@ import ( "io" "unsafe" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" "golang.org/x/sys/unix" ) diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go index 71dfba3..a68cd98 100644 --- a/tun/offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -9,7 +9,7 @@ import ( "net/netip" "testing" - "github.com/amnezia-vpn/amnezia-wg/conn" + "github.com/amnezia-vpn/amneziawg-go/conn" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" diff --git a/tun/tun_linux.go b/tun/tun_linux.go index d57b167..011e56a 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -17,8 +17,8 @@ import ( "time" "unsafe" - "github.com/amnezia-vpn/amnezia-wg/conn" - "github.com/amnezia-vpn/amnezia-wg/rwcancel" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/rwcancel" "golang.org/x/sys/unix" ) diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go index 7068d9b..f620e0a 100644 --- a/tun/tuntest/tuntest.go +++ b/tun/tuntest/tuntest.go @@ -11,7 +11,7 @@ import ( "net/netip" "os" - "github.com/amnezia-vpn/amnezia-wg/tun" + "github.com/amnezia-vpn/amneziawg-go/tun" ) func Ping(dst, src netip.Addr) []byte { From bfeb3954f693dc045db4ac8232ee5176152f0d98 Mon Sep 17 00:00:00 2001 From: tiaga Date: Fri, 2 Feb 2024 22:56:00 +0700 Subject: [PATCH 024/173] Update Dockerfile - update Alpine version - improve `Dockerfile` to use pre-built AmneziaWG tools --- Dockerfile | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index cbf05b4..136ebc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,16 +5,10 @@ RUN go mod download && \ go mod verify && \ go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin -FROM alpine:3.15 as awg-tools -ARG AWGTOOLS_RELEASE="1.0.20231215" -RUN apk --no-cache add linux-headers build-base bash && \ - wget https://github.com/amnezia-vpn/amneziawg-tools/archive/refs/tags/v${AWGTOOLS_RELEASE}.zip && \ - unzip v${AWGTOOLS_RELEASE}.zip && \ - cd amneziawg-tools-${AWGTOOLS_RELEASE}/src && \ - make -e LDFLAGS=-static && \ - make install - -FROM alpine:3.15 -RUN apk --no-cache add iproute2 bash +FROM alpine:3.19 +ARG AWGTOOLS_RELEASE="1.0.20240202" +RUN apk --no-cache add iproute2 bash && \ + wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ + unzip alpine-3.19-amneziawg-tools.zip -d /usr/bin/ && \ + chmod +x /usr/bin/wg /usr/bin/wg-quick COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go -COPY --from=awg-tools /usr/bin/awg /usr/bin/awg-quick /usr/bin/ From cbd414dfecfcd711bde99b8696aba58582580732 Mon Sep 17 00:00:00 2001 From: tiaga Date: Wed, 7 Feb 2024 18:44:59 +0700 Subject: [PATCH 025/173] Add pipeline Build and push Docker image on a tag push. --- .github/workflows/build-if-tag.yml | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/build-if-tag.yml diff --git a/ .github/workflows/build-if-tag.yml b/ .github/workflows/build-if-tag.yml new file mode 100644 index 0000000..4fa0198 --- /dev/null +++ b/ .github/workflows/build-if-tag.yml @@ -0,0 +1,41 @@ +name: build-if-tag + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +env: + APP: amneziawg-go + +jobs: + build: + runs-on: ubuntu-latest + name: build + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Setup metadata + uses: docker/metadata-action@v5 + id: metadata + with: + images: amneziavpn/${{ env.APP }} + tags: type=semver,pattern={{version}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.metadata.outputs.tags }} From f0dfb5eaccf52b021cf000a8004266b7faa3881b Mon Sep 17 00:00:00 2001 From: tiaga Date: Wed, 7 Feb 2024 18:53:55 +0700 Subject: [PATCH 026/173] Fix pipeline Fix path to GitHub Actions workflow. --- { .github => .github}/workflows/build-if-tag.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename { .github => .github}/workflows/build-if-tag.yml (100%) diff --git a/ .github/workflows/build-if-tag.yml b/.github/workflows/build-if-tag.yml similarity index 100% rename from .github/workflows/build-if-tag.yml rename to .github/workflows/build-if-tag.yml From 59101fd202067ae42362dab5494b4aaded3a3456 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 10 Feb 2024 16:02:05 +0300 Subject: [PATCH 027/173] Bump crypto, net, sys modules to the latest versions --- go.mod | 6 +++--- go.sum | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2df4282..33182ee 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.20 require ( github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/sys v0.12.0 + golang.org/x/crypto v0.19.0 + golang.org/x/net v0.21.0 + golang.org/x/sys v0.17.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 ) diff --git a/go.sum b/go.sum index 71c64b6..eb7f470 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,15 @@ github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= 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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.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= From 032e33f5776a569168a0bd02b92f2d34fb0345f4 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 10 Feb 2024 17:14:51 +0300 Subject: [PATCH 028/173] Fix Android UDP GRO check --- conn/controlfns_linux.go | 8 -------- conn/features_linux.go | 6 ++++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index f6ab1d2..a2396fe 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -57,13 +57,5 @@ func init() { } return err }, - - // Attempt to enable UDP_GRO - func(network, address string, c syscall.RawConn) error { - c.Control(func(fd uintptr) { - _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) - }) - return nil - }, ) } diff --git a/conn/features_linux.go b/conn/features_linux.go index 8959d93..a6de8c1 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -19,8 +19,10 @@ func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { err = rc.Control(func(fd uintptr) { _, errSyscall := unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT) txOffload = errSyscall == nil - opt, errSyscall := unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO) - rxOffload = errSyscall == nil && opt == 1 + // getsockopt(IPPROTO_UDP, UDP_GRO) is not supported in android + // use setsockopt workaround + errSyscall = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) + rxOffload = errSyscall == nil }) if err != nil { return false, false From 6705978fc86f48f6d882535c3180eaa7653f298f Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 10 Feb 2024 17:47:33 +0300 Subject: [PATCH 029/173] Add debug udp offload info --- conn/bind_std.go | 5 +++++ conn/bind_windows.go | 4 ++++ conn/bindtest/bindtest.go | 2 ++ conn/conn.go | 2 ++ device/device.go | 1 + 5 files changed, 14 insertions(+) diff --git a/conn/bind_std.go b/conn/bind_std.go index 46df7fd..b416e22 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -298,6 +298,11 @@ func (s *StdNetBind) BatchSize() int { return 1 } +func (s *StdNetBind) GetOffloadInfo() string { + return fmt.Sprintf("ipv4TxOffload: %v, ipv4RxOffload: %v\nipv6TxOffload: %v, ipv6RxOffload: %v", + s.ipv4TxOffload, s.ipv4RxOffload, s.ipv6TxOffload, s.ipv6RxOffload) +} + func (s *StdNetBind) Close() error { s.mu.Lock() defer s.mu.Unlock() diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 6cfa099..3481f00 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -328,6 +328,10 @@ func (bind *WinRingBind) BatchSize() int { return 1 } +func (bind *WinRingBind) GetOffloadInfo() string { + return "" +} + func (bind *WinRingBind) SetMark(mark uint32) error { return nil } diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 42b0bb7..0df1420 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -91,6 +91,8 @@ func (c *ChannelBind) Close() error { func (c *ChannelBind) BatchSize() int { return 1 } +func (c *ChannelBind) GetOffloadInfo() string { return "" } + func (c *ChannelBind) SetMark(mark uint32) error { return nil } func (c *ChannelBind) makeReceiveFunc(ch chan []byte) conn.ReceiveFunc { diff --git a/conn/conn.go b/conn/conn.go index a1f57d2..489cb35 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -55,6 +55,8 @@ type Bind interface { // BatchSize is the number of buffers expected to be passed to // the ReceiveFuncs, and the maximum expected to be passed to SendBatch. BatchSize() int + + GetOffloadInfo() string } // BindSocketToInterface is implemented by Bind objects that support being diff --git a/device/device.go b/device/device.go index a9d6281..24ae1ea 100644 --- a/device/device.go +++ b/device/device.go @@ -545,6 +545,7 @@ func (device *Device) BindUpdate() error { } device.log.Verbosef("UDP bind has been updated") + device.log.Verbosef(netc.bind.GetOffloadInfo()) return nil } From 0c347529b8f752fde34b261a510a8d3d597ae75c Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 12 Feb 2024 16:27:56 +0300 Subject: [PATCH 030/173] Fix go.sum --- go.sum | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/go.sum b/go.sum index eb7f470..0e6f733 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,11 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.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= From 9c6b3ff332ab47cfa5613bc3d0f582cc887c6348 Mon Sep 17 00:00:00 2001 From: tiaga Date: Tue, 13 Feb 2024 21:27:34 +0700 Subject: [PATCH 031/173] Update Dockerfile - rename `wg` and `wg-quick` to `awg` and `awg-quick` accordingly - add iptables - update AmneziaWG tools version --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 136ebc8..9d41002 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,9 @@ RUN go mod download && \ go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin FROM alpine:3.19 -ARG AWGTOOLS_RELEASE="1.0.20240202" -RUN apk --no-cache add iproute2 bash && \ +ARG AWGTOOLS_RELEASE="1.0.20240213" +RUN apk --no-cache add iproute2 iptables bash && \ wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ unzip alpine-3.19-amneziawg-tools.zip -d /usr/bin/ && \ - chmod +x /usr/bin/wg /usr/bin/wg-quick + chmod +x /usr/bin/awg /usr/bin/awg-quick COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go From 92e28a0d14c643f620f945147d826a55b3a082ce Mon Sep 17 00:00:00 2001 From: tiaga Date: Tue, 13 Feb 2024 21:44:41 +0700 Subject: [PATCH 032/173] Fix Dockerfile Fix AmneziaWG tools installation. --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9d41002..6586268 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,8 @@ RUN go mod download && \ FROM alpine:3.19 ARG AWGTOOLS_RELEASE="1.0.20240213" RUN apk --no-cache add iproute2 iptables bash && \ + cd /usr/bin/ && \ wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ - unzip alpine-3.19-amneziawg-tools.zip -d /usr/bin/ && \ + unzip -j alpine-3.19-amneziawg-tools.zip && \ chmod +x /usr/bin/awg /usr/bin/awg-quick COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go From 4dddf62e576b5acc703b3fd354cf5e3d2cedfbca Mon Sep 17 00:00:00 2001 From: AlexanderGalkov <143902290+AlexanderGalkov@users.noreply.github.com> Date: Tue, 20 Feb 2024 20:29:36 +0700 Subject: [PATCH 033/173] Update Dockerfile add wg and wg-quick symlinks Signed-off-by: AlexanderGalkov <143902290+AlexanderGalkov@users.noreply.github.com> --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6586268..caeb333 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,5 +11,7 @@ RUN apk --no-cache add iproute2 iptables bash && \ cd /usr/bin/ && \ wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ unzip -j alpine-3.19-amneziawg-tools.zip && \ - chmod +x /usr/bin/awg /usr/bin/awg-quick + chmod +x /usr/bin/awg /usr/bin/awg-quick && \ + ln -s /usr/bin/awg /usr/bin/wg && \ + ln -s /usr/bin/awg-quick /usr/bin/wg-quick COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go From 3f0a3bcfa0da940e6ded34e654fdc9186038dd83 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 16 Mar 2024 14:43:32 +0300 Subject: [PATCH 034/173] Fix wg reconnection problem after awg connection --- device/device.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/device/device.go b/device/device.go index 24ae1ea..21a6546 100644 --- a/device/device.go +++ b/device/device.go @@ -562,6 +562,11 @@ func (device *Device) isAdvancedSecurityOn() bool { func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { if !tempASecCfg.isSet { + // restore default values + MessageInitiationType = 1 + MessageResponseType = 2 + MessageCookieReplyType = 3 + MessageTransportType = 4 return err } From 64040e66467d89c9312cf442866185404b7ec60c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 13 Apr 2024 10:55:05 -0700 Subject: [PATCH 035/173] ipc: build on aix Updates tailscale/tailscale#11361 --- ipc/uapi_fake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/uapi_fake.go b/ipc/uapi_fake.go index e68863d..3e3dcdf 100644 --- a/ipc/uapi_fake.go +++ b/ipc/uapi_fake.go @@ -1,4 +1,4 @@ -//go:build wasm || plan9 +//go:build wasm || plan9 || aix /* SPDX-License-Identifier: MIT * From 03c5a0ccf7546055344017505460bdb9c0425b17 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Mon, 29 Apr 2024 09:15:56 -0700 Subject: [PATCH 036/173] tun: implement API for disabling UDP GRO on Linux Certain device drivers (e.g. vxlan, geneve) do not properly handle coalesced UDP packets later in the stack, resulting in packet loss. Signed-off-by: Jordan Whited --- tun/tun.go | 8 ++++++++ tun/tun_linux.go | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tun/tun.go b/tun/tun.go index 0ae53d0..d3c5012 100644 --- a/tun/tun.go +++ b/tun/tun.go @@ -51,3 +51,11 @@ type Device interface { // lifetime of a Device. BatchSize() int } + +type LinuxDevice interface { + Device + // DisableUDPGRO disables UDP GRO if it is enabled. Certain device drivers + // (e.g. vxlan, geneve) do not properly handle coalesced UDP packets later + // in the stack, resulting in packet loss. + DisableUDPGRO() +} diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 9313ebf..6aa03d4 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -49,10 +49,11 @@ type NativeTun struct { readOpMu sync.Mutex // readOpMu guards readBuff readBuff [virtioNetHdrLen + 65535]byte // if vnetHdr every read() is prefixed by virtioNetHdr - writeOpMu sync.Mutex // writeOpMu guards toWrite, tcpGROTable + writeOpMu sync.Mutex // writeOpMu guards the following fields toWrite []int tcpGROTable *tcpGROTable udpGROTable *udpGROTable + udpGRO bool } func (tun *NativeTun) File() *os.File { @@ -345,7 +346,7 @@ func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { ) tun.toWrite = tun.toWrite[:0] if tun.vnetHdr { - err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.udpGSO, &tun.toWrite) + err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.udpGRO, &tun.toWrite) if err != nil { return 0, err } @@ -502,6 +503,12 @@ func (tun *NativeTun) BatchSize() int { return tun.batchSize } +func (tun *NativeTun) DisableUDPGRO() { + tun.writeOpMu.Lock() + tun.udpGRO = false + tun.writeOpMu.Unlock() +} + const ( // TODO: support TSO with ECN bits tunTCPOffloads = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6 @@ -538,6 +545,7 @@ func (tun *NativeTun) initFromFlags(name string) error { // tunUDPOffloads were added in Linux v6.2. We do not return an // error if they are unsupported at runtime. tun.udpGSO = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) == nil + tun.udpGRO = tun.udpGSO } else { tun.batchSize = 1 } From 3ddf952973fac2e4d30f4aa61a7c6dd49bfc8fcb Mon Sep 17 00:00:00 2001 From: RomikB Date: Sat, 11 May 2024 22:16:22 +0200 Subject: [PATCH 037/173] unsafe rebranding: change pipe name --- ipc/uapi_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index bfe7965..31d2a63 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -62,7 +62,7 @@ func init() { func UAPIListen(name string) (net.Listener, error) { listener, err := (&namedpipe.ListenConfig{ SecurityDescriptor: UAPISecurityDescriptor, - }).Listen(`\\.\pipe\ProtectedPrefix\Administrators\WireGuard\` + name) + }).Listen(`\\.\pipe\ProtectedPrefix\Administrators\AmneziaWG\` + name) if err != nil { return nil, err } From e433d13df6ac99fa317b5ce27d04c7dad307cab3 Mon Sep 17 00:00:00 2001 From: albexk Date: Wed, 3 Apr 2024 18:42:37 +0300 Subject: [PATCH 038/173] Add disabling UDP GSO when an error occurs due to inconsistent peer mtu --- conn/bind_std.go | 2 +- conn/errors_linux.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index b416e22..ea06cd5 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -336,7 +336,7 @@ type ErrUDPGSODisabled struct { } func (e ErrUDPGSODisabled) Error() string { - return fmt.Sprintf("disabled UDP GSO on %s, NIC(s) may not support checksum offload", e.onLaddr) + return fmt.Sprintf("disabled UDP GSO on %s, NIC(s) may not support checksum offload or peer MTU with protocol headers is greater than path MTU", e.onLaddr) } func (e ErrUDPGSODisabled) Unwrap() error { diff --git a/conn/errors_linux.go b/conn/errors_linux.go index 8e61000..7548a8a 100644 --- a/conn/errors_linux.go +++ b/conn/errors_linux.go @@ -20,7 +20,9 @@ func errShouldDisableUDPGSO(err error) bool { // See: // https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man7/udp.7?id=806eabd74910447f21005160e90957bde4db0183#n228 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/udp.c?h=v6.2&id=c9c3395d5e3dcc6daee66c6908354d47bf98cb0c#n942 - return serr.Err == unix.EIO + // If gso_size + udp + ip headers > fragment size EINVAL is returned. + // It occurs when the peer mtu + wg headers is greater than path mtu. + return serr.Err == unix.EIO || serr.Err == unix.EINVAL } return false } From 77d39ff3b9b1144d0106bd968807c62a033be8dc Mon Sep 17 00:00:00 2001 From: albexk Date: Wed, 3 Apr 2024 18:45:26 +0300 Subject: [PATCH 039/173] Minor naming changes --- README.md | 6 +++--- main.go | 22 +++++++++++----------- main_windows.go | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ab6f62b..853d318 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ To run amneziawg-go without forking to the background, pass `-f` or `--foregroun ``` $ amneziawg-go -f wg0 ``` -When an interface is running, you may use [`amnezia-wg-tools `](https://github.com/amnezia-vpn/amneziawg-go-tools) to configure it, as well as the usual `ip(8)` and `ifconfig(8)` commands. +When an interface is running, you may use [`amneziawg-tools `](https://github.com/amnezia-vpn/amneziawg-tools) 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`. @@ -34,11 +34,11 @@ This will run on Linux; you should run amnezia-wg instead of using default linux ### 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. -This runs on MacOS, you should use it from [awg-apple](https://github.com/amnezia-vpn/awg-apple) +This runs on MacOS, you should use it from [amneziawg-apple](https://github.com/amnezia-vpn/amneziawg-apple) ### Windows -This runs on Windows, you should use it from [awg-windows](https://github.com/amnezia-vpn/awg-windows), which uses this as a module. +This runs on Windows, you should use it from [amneziawg-windows](https://github.com/amnezia-vpn/amneziawg-windows), which uses this as a module. ## Building diff --git a/main.go b/main.go index 775372c..c17c405 100644 --- a/main.go +++ b/main.go @@ -46,20 +46,20 @@ func warning() { 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, "└──────────────────────────────────────────────────────┘") + fmt.Fprintln(os.Stderr, "┌──────────────────────────────────────────────────────────────┐") + fmt.Fprintln(os.Stderr, "│ │") + fmt.Fprintln(os.Stderr, "│ Running amneziawg-go is not required because this │") + fmt.Fprintln(os.Stderr, "│ kernel has first class support for AmneziaWG. For │") + fmt.Fprintln(os.Stderr, "│ information on installing the kernel module, │") + fmt.Fprintln(os.Stderr, "│ please visit: │") + fmt.Fprintln(os.Stderr, "| https://github.com/amnezia-vpn/amneziawg-linux-kernel-module │") + 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 .\n", Version, runtime.GOOS, runtime.GOARCH) + fmt.Printf("amneziawg-go v%s\n\nUserspace AmneziaWG daemon for %s-%s.\nInformation available at https://amnezia.org\n", Version, runtime.GOOS, runtime.GOARCH) return } @@ -145,7 +145,7 @@ func main() { fmt.Sprintf("(%s) ", interfaceName), ) - logger.Verbosef("Starting wireguard-go version %s", Version) + logger.Verbosef("Starting amneziawg-go version %s", Version) if err != nil { logger.Errorf("Failed to create TUN device: %v", err) diff --git a/main_windows.go b/main_windows.go index 807f6e2..bbfa690 100644 --- a/main_windows.go +++ b/main_windows.go @@ -30,13 +30,13 @@ func main() { } 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 , which includes this code as a module.") + fmt.Fprintln(os.Stderr, "Warning: this is a test program for Windows, mainly used for debugging this Go package. For a real AmneziaWG for Windows client, please visit: https://amnezia.org") logger := device.NewLogger( device.LogLevelVerbose, fmt.Sprintf("(%s) ", interfaceName), ) - logger.Verbosef("Starting wireguard-go version %s", Version) + logger.Verbosef("Starting amneziawg-go version %s", Version) tun, err := tun.CreateTUN(interfaceName, 0) if err == nil { From d2b0fc97892bbdd9802a5152a15a52c59fe80800 Mon Sep 17 00:00:00 2001 From: albexk Date: Tue, 9 Apr 2024 21:45:50 +0300 Subject: [PATCH 040/173] Add resetting of message types when closing the device --- device/device.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/device/device.go b/device/device.go index 21a6546..a8a9e2f 100644 --- a/device/device.go +++ b/device/device.go @@ -415,6 +415,8 @@ func (device *Device) Close() { device.rate.limiter.Close() + device.resetProtocol() + device.log.Verbosef("Device closed") close(device.closed) } @@ -559,14 +561,17 @@ func (device *Device) isAdvancedSecurityOn() bool { return device.isASecOn.IsSet() } +func (device *Device) resetProtocol() { + // restore default message type values + MessageInitiationType = 1 + MessageResponseType = 2 + MessageCookieReplyType = 3 + MessageTransportType = 4 +} + func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { if !tempASecCfg.isSet { - // restore default values - MessageInitiationType = 1 - MessageResponseType = 2 - MessageCookieReplyType = 3 - MessageTransportType = 4 return err } From c00bda9200364d05b071dde04f0157c7a72c39b8 Mon Sep 17 00:00:00 2001 From: albexk Date: Wed, 10 Apr 2024 16:05:31 +0300 Subject: [PATCH 041/173] Fix output of the version command --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index c17c405..5a3dfef 100644 --- a/main.go +++ b/main.go @@ -59,7 +59,7 @@ func warning() { func main() { if len(os.Args) == 2 && os.Args[1] == "--version" { - fmt.Printf("amneziawg-go v%s\n\nUserspace AmneziaWG daemon for %s-%s.\nInformation available at https://amnezia.org\n", Version, runtime.GOOS, runtime.GOARCH) + fmt.Printf("amneziawg-go %s\n\nUserspace AmneziaWG daemon for %s-%s.\nInformation available at https://amnezia.org\n", Version, runtime.GOOS, runtime.GOARCH) return } From 87d8c00f869645293c9ecd449488e151d33bde2f Mon Sep 17 00:00:00 2001 From: albexk Date: Tue, 21 May 2024 18:03:30 +0300 Subject: [PATCH 042/173] Up go to 1.22.3, up crypto to 0.21.0 --- go.mod | 6 +++--- go.sum | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 33182ee..115ae88 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/amnezia-vpn/amneziawg-go -go 1.20 +go 1.22.3 require ( github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.21.0 golang.org/x/net v0.21.0 - golang.org/x/sys v0.17.0 + golang.org/x/sys v0.18.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 ) diff --git a/go.sum b/go.sum index 0e6f733..7b53725 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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= From 2e7780471af8efd13345692c11dac0cdb9cd8d35 Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Fri, 24 May 2024 18:18:23 +0300 Subject: [PATCH 043/173] Remove GetOffloadInfo() (#32) * Remove GetOffloadInfo() * Remove GetOffloadInfo() from bind_windows as well * Allow lightweight tags to be used in the version --- Makefile | 2 +- conn/bind_std.go | 5 ----- conn/bind_windows.go | 4 ---- conn/bindtest/bindtest.go | 2 -- conn/conn.go | 2 -- device/device.go | 1 - 6 files changed, 1 insertion(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 4087cba..7a88647 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ MAKEFLAGS += --no-print-directory generate-version-and-build: @export GIT_CEILING_DIRECTORIES="$(realpath $(CURDIR)/..)" && \ - tag="$$(git describe --dirty 2>/dev/null)" && \ + tag="$$(git describe --tags --dirty 2>/dev/null)" && \ ver="$$(printf 'package main\n\nconst Version = "%s"\n' "$$tag")" && \ [ "$$(cat version.go 2>/dev/null)" != "$$ver" ] && \ echo "$$ver" > version.go && \ diff --git a/conn/bind_std.go b/conn/bind_std.go index ea06cd5..312a538 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -298,11 +298,6 @@ func (s *StdNetBind) BatchSize() int { return 1 } -func (s *StdNetBind) GetOffloadInfo() string { - return fmt.Sprintf("ipv4TxOffload: %v, ipv4RxOffload: %v\nipv6TxOffload: %v, ipv6RxOffload: %v", - s.ipv4TxOffload, s.ipv4RxOffload, s.ipv6TxOffload, s.ipv6RxOffload) -} - func (s *StdNetBind) Close() error { s.mu.Lock() defer s.mu.Unlock() diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 3481f00..6cfa099 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -328,10 +328,6 @@ func (bind *WinRingBind) BatchSize() int { return 1 } -func (bind *WinRingBind) GetOffloadInfo() string { - return "" -} - func (bind *WinRingBind) SetMark(mark uint32) error { return nil } diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 0df1420..42b0bb7 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -91,8 +91,6 @@ func (c *ChannelBind) Close() error { func (c *ChannelBind) BatchSize() int { return 1 } -func (c *ChannelBind) GetOffloadInfo() string { return "" } - func (c *ChannelBind) SetMark(mark uint32) error { return nil } func (c *ChannelBind) makeReceiveFunc(ch chan []byte) conn.ReceiveFunc { diff --git a/conn/conn.go b/conn/conn.go index 489cb35..a1f57d2 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -55,8 +55,6 @@ type Bind interface { // BatchSize is the number of buffers expected to be passed to // the ReceiveFuncs, and the maximum expected to be passed to SendBatch. BatchSize() int - - GetOffloadInfo() string } // BindSocketToInterface is implemented by Bind objects that support being diff --git a/device/device.go b/device/device.go index a8a9e2f..80e3793 100644 --- a/device/device.go +++ b/device/device.go @@ -547,7 +547,6 @@ func (device *Device) BindUpdate() error { } device.log.Verbosef("UDP bind has been updated") - device.log.Verbosef(netc.bind.GetOffloadInfo()) return nil } From 1e088837d114a74a2c335968e7d40537447b9b4f Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 08:43:41 -0700 Subject: [PATCH 044/173] 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") Signed-off-by: Jordan Whited --- device/pools.go | 11 ++++++----- device/pools_test.go | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/device/pools.go b/device/pools.go index 94f3dc7..55d2be7 100644 --- a/device/pools.go +++ b/device/pools.go @@ -7,14 +7,13 @@ package device import ( "sync" - "sync/atomic" ) type WaitPool struct { pool sync.Pool cond sync.Cond lock sync.Mutex - count atomic.Uint32 + count uint32 // Get calls not yet Put back max uint32 } @@ -27,10 +26,10 @@ func NewWaitPool(max uint32, new func() any) *WaitPool { func (p *WaitPool) Get() any { if p.max != 0 { p.lock.Lock() - for p.count.Load() >= p.max { + for p.count >= p.max { p.cond.Wait() } - p.count.Add(1) + p.count++ p.lock.Unlock() } return p.pool.Get() @@ -41,7 +40,9 @@ func (p *WaitPool) Put(x any) { if p.max == 0 { return } - p.count.Add(^uint32(0)) + p.lock.Lock() + defer p.lock.Unlock() + p.count-- p.cond.Signal() } diff --git a/device/pools_test.go b/device/pools_test.go index 82d7493..2b16f39 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -15,7 +15,6 @@ import ( ) func TestWaitPool(t *testing.T) { - t.Skip("Currently disabled") var wg sync.WaitGroup var trials atomic.Int32 startTrials := int32(100000) @@ -32,7 +31,9 @@ func TestWaitPool(t *testing.T) { wg.Add(workers) var max atomic.Uint32 updateMax := func() { - count := p.count.Load() + p.lock.Lock() + count := p.count + p.lock.Unlock() if count > p.max { t.Errorf("count (%d) > max (%d)", count, p.max) } From cfa45674af86ac5074d879257ca513438bb291eb Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 09:06:40 -0700 Subject: [PATCH 045/173] device: fix missed return of QueueOutboundElementsContainer to its WaitPool Fixes: 3bb8fec ("conn, device, tun: implement vectorized I/O plumbing") Signed-off-by: Jordan Whited --- device/send.go | 1 + 1 file changed, 1 insertion(+) diff --git a/device/send.go b/device/send.go index 2701f4e..8ed2e5f 100644 --- a/device/send.go +++ b/device/send.go @@ -506,6 +506,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } + device.PutOutboundElementsContainer(elemsContainer) continue } dataSent := false From 2e3f7d122ca8ef61e403fddc48a9db8fccd95dbf Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Mon, 1 Jul 2024 13:39:57 +0300 Subject: [PATCH 046/173] Update Go version in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index caeb333..590ec5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20 as awg +FROM golang:1.22.3 as awg COPY . /awg WORKDIR /awg RUN go mod download && \ From 2f5d148bcfe13a65d0f85dca8698200543c226da Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 7 Jun 2024 16:57:40 -0700 Subject: [PATCH 047/173] conn,device: enable cryptorouting via PeerAwareEndpoint Introduce an optional extension point for Endpoint that enables a path for WireGuard to inform an integration about the peer public key that is associated with an Endpoint. The API is expected to return either the same or a new Endpoint in response to this function. A future version of this patch could potentially remove the returned Endpoint, but would require larger integrator changes downstream. This adds a small per-packet cost that could later be removed with a larger refactor of the wireguard-go interface and Tailscale magicsock code, as well as introducing a generic bound for Endpoint in a device & bind instance. Updates tailscale/corp#20732 --- conn/conn.go | 14 ++++++++++++++ device/noise-protocol.go | 2 +- device/peer.go | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/conn/conn.go b/conn/conn.go index a1f57d2..8df5aaa 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -84,6 +84,20 @@ type Endpoint interface { SrcIP() netip.Addr } +// PeerAwareEndpoint is an optional Endpoint specialization for +// integrations that want to know about the outcome of cryptorouting +// 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. +// +// If GetPeerEndpoint returns nil, WireGuard will be unable to respond +// to the peer until a new endpoint is written by a later packet. +type PeerAwareEndpoint interface { + GetPeerEndpoint(peerPublicKey [32]byte) Endpoint +} + var ( ErrBindAlreadyOpen = errors.New("bind is already open") ErrWrongEndpointType = errors.New("endpoint type does not correspond with bind type") diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 9f2ba50..2d8f984 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -124,7 +124,7 @@ type Handshake struct { localEphemeral NoisePrivateKey // ephemeral secret key localIndex uint32 // used to clear hash-table 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 precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret lastTimestamp tai64n.Timestamp diff --git a/device/peer.go b/device/peer.go index 89b719b..876e5da 100644 --- a/device/peer.go +++ b/device/peer.go @@ -283,6 +283,9 @@ func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) { return } peer.endpoint.clearSrcOnTx = false + if ep, ok := endpoint.(conn.PeerAwareEndpoint); ok { + endpoint = ep.GetPeerEndpoint(peer.handshake.remoteStatic) + } peer.endpoint.val = endpoint } From 60eeedfd624bb0ee4d173c1aaac1ed0ddedf13fe Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 16 Jul 2024 15:36:20 -0700 Subject: [PATCH 048/173] tun: export GSOSplit() for external Device implementers External implementers of tun.Device may support GSO, and may also be platform-agnostic, e.g. gVisor. Signed-off-by: Jordan Whited --- tun/offload.go | 220 +++++++++++++++++++++++++++++++++++++++++++ tun/offload_linux.go | 134 +++++--------------------- tun/offload_test.go | 95 +++++++++++++++++++ tun/tun_linux.go | 67 +++---------- 4 files changed, 354 insertions(+), 162 deletions(-) create mode 100644 tun/offload.go create mode 100644 tun/offload_test.go diff --git a/tun/offload.go b/tun/offload.go new file mode 100644 index 0000000..6627e46 --- /dev/null +++ b/tun/offload.go @@ -0,0 +1,220 @@ +package tun + +import ( + "encoding/binary" + "fmt" +) + +// GSOType represents the type of segmentation offload. +type GSOType int + +const ( + GSONone GSOType = iota + GSOTCPv4 + GSOTCPv6 + GSOUDPL4 +) + +func (g GSOType) String() string { + switch g { + case GSONone: + return "GSONone" + case GSOTCPv4: + return "GSOTCPv4" + case GSOTCPv6: + return "GSOTCPv6" + case GSOUDPL4: + return "GSOUDPL4" + default: + return "unknown" + } +} + +// GSOOptions is loosely modeled after struct virtio_net_hdr from the VIRTIO +// specification. It is a common representation of GSO metadata that can be +// applied to support packet GSO across tun.Device implementations. +type GSOOptions struct { + // GSOType represents the type of segmentation offload. + GSOType GSOType + // HdrLen is the sum of the layer 3 and 4 header lengths. This field may be + // zero when GSOType == GSONone. + HdrLen uint16 + // CsumStart is the head byte index of the packet data to be checksummed, + // i.e. the start of the TCP or UDP header. + CsumStart uint16 + // CsumOffset is the offset from CsumStart where the 2-byte checksum value + // should be placed. + CsumOffset uint16 + // GSOSize is the size of each segment exclusive of HdrLen. The tail segment + // may be smaller than this value. + GSOSize uint16 + // NeedsCsum may be set where GSOType == GSONone. When set, the checksum + // at CsumStart + CsumOffset must be a partial checksum, i.e. the + // pseudo-header sum. + NeedsCsum bool +} + +const ( + ipv4SrcAddrOffset = 12 + ipv6SrcAddrOffset = 8 +) + +const tcpFlagsOffset = 13 + +const ( + tcpFlagFIN uint8 = 0x01 + tcpFlagPSH uint8 = 0x08 + tcpFlagACK uint8 = 0x10 +) + +const ( + // defined here in order to avoid importation of any platform-specific pkgs + ipProtoTCP = 6 + ipProtoUDP = 17 +) + +// GSOSplit splits packets from 'in' into outBufs[][outOffset:], writing +// the size of each element into sizes. It returns the number of buffers +// populated, and/or an error. Callers may pass an 'in' slice that overlaps with +// the first element of outBuffers, i.e. &in[0] may be equal to +// &outBufs[0][outOffset]. GSONone is a valid options.GSOType regardless of the +// value of options.NeedsCsum. Length of each outBufs element must be greater +// than or equal to the length of 'in', otherwise output may be silently +// truncated. +func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outOffset int) (int, error) { + cSumAt := int(options.CsumStart) + int(options.CsumOffset) + if cSumAt+1 >= len(in) { + return 0, fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(in)) + } + + if len(in) < int(options.HdrLen) { + return 0, fmt.Errorf("length of packet (%d) < GSO HdrLen (%d)", len(in), options.HdrLen) + } + + // Handle the conditions where we are copying a single element to outBuffs. + payloadLen := len(in) - int(options.HdrLen) + if options.GSOType == GSONone || payloadLen < int(options.GSOSize) { + if len(in) > len(outBufs[0][outOffset:]) { + return 0, fmt.Errorf("length of packet (%d) exceeds output element length (%d)", len(in), len(outBufs[0][outOffset:])) + } + if options.NeedsCsum { + // The initial value at the checksum offset should be summed with + // the checksum we compute. This is typically the pseudo-header sum. + initial := binary.BigEndian.Uint16(in[cSumAt:]) + in[cSumAt], in[cSumAt+1] = 0, 0 + binary.BigEndian.PutUint16(in[cSumAt:], ^checksum(in[options.CsumStart:], initial)) + } + sizes[0] = copy(outBufs[0][outOffset:], in) + return 1, nil + } + + if options.HdrLen < options.CsumStart { + return 0, fmt.Errorf("GSO HdrLen (%d) < GSO CsumStart (%d)", options.HdrLen, options.CsumStart) + } + + ipVersion := in[0] >> 4 + switch ipVersion { + case 4: + if options.GSOType != GSOTCPv4 && options.GSOType != GSOUDPL4 { + return 0, fmt.Errorf("ip header version: %d, GSO type: %s", ipVersion, options.GSOType) + } + if len(in) < 20 { + return 0, fmt.Errorf("length of packet (%d) < minimum ipv4 header size (%d)", len(in), 20) + } + case 6: + if options.GSOType != GSOTCPv6 && options.GSOType != GSOUDPL4 { + return 0, fmt.Errorf("ip header version: %d, GSO type: %s", ipVersion, options.GSOType) + } + if len(in) < 40 { + return 0, fmt.Errorf("length of packet (%d) < minimum ipv6 header size (%d)", len(in), 40) + } + default: + return 0, fmt.Errorf("invalid ip header version: %d", ipVersion) + } + + iphLen := int(options.CsumStart) + srcAddrOffset := ipv6SrcAddrOffset + addrLen := 16 + if ipVersion == 4 { + srcAddrOffset = ipv4SrcAddrOffset + addrLen = 4 + } + transportCsumAt := int(options.CsumStart + options.CsumOffset) + var firstTCPSeqNum uint32 + var protocol uint8 + if options.GSOType == GSOTCPv4 || options.GSOType == GSOTCPv6 { + protocol = ipProtoTCP + if len(in) < int(options.CsumStart)+20 { + return 0, fmt.Errorf("length of packet (%d) < GSO CsumStart (%d) + minimum TCP header size (%d)", + len(in), options.CsumStart, 20) + } + firstTCPSeqNum = binary.BigEndian.Uint32(in[options.CsumStart+4:]) + } else { + protocol = ipProtoUDP + } + nextSegmentDataAt := int(options.HdrLen) + i := 0 + for ; nextSegmentDataAt < len(in); i++ { + if i == len(outBufs) { + return i - 1, ErrTooManySegments + } + nextSegmentEnd := nextSegmentDataAt + int(options.GSOSize) + if nextSegmentEnd > len(in) { + nextSegmentEnd = len(in) + } + segmentDataLen := nextSegmentEnd - nextSegmentDataAt + totalLen := int(options.HdrLen) + segmentDataLen + sizes[i] = totalLen + out := outBufs[i][outOffset:] + + copy(out, in[:iphLen]) + if ipVersion == 4 { + // For IPv4 we are responsible for incrementing the ID field, + // updating the total len field, and recalculating the header + // checksum. + if i > 0 { + id := binary.BigEndian.Uint16(out[4:]) + id += uint16(i) + binary.BigEndian.PutUint16(out[4:], id) + } + out[10], out[11] = 0, 0 // clear ipv4 header checksum + binary.BigEndian.PutUint16(out[2:], uint16(totalLen)) + ipv4CSum := ^checksum(out[:iphLen], 0) + binary.BigEndian.PutUint16(out[10:], ipv4CSum) + } else { + // For IPv6 we are responsible for updating the payload length field. + binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) + } + + // copy transport header + copy(out[options.CsumStart:options.HdrLen], in[options.CsumStart:options.HdrLen]) + + if protocol == ipProtoTCP { + // set TCP seq and adjust TCP flags + tcpSeq := firstTCPSeqNum + uint32(options.GSOSize*uint16(i)) + binary.BigEndian.PutUint32(out[options.CsumStart+4:], tcpSeq) + if nextSegmentEnd != len(in) { + // FIN and PSH should only be set on last segment + clearFlags := tcpFlagFIN | tcpFlagPSH + out[options.CsumStart+tcpFlagsOffset] &^= clearFlags + } + } else { + // set UDP header len + binary.BigEndian.PutUint16(out[options.CsumStart+4:], uint16(segmentDataLen)+(options.HdrLen-options.CsumStart)) + } + + // payload + copy(out[options.HdrLen:], in[nextSegmentDataAt:nextSegmentEnd]) + + // transport checksum + out[transportCsumAt], out[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum + transportHeaderLen := int(options.HdrLen - options.CsumStart) + lenForPseudo := uint16(transportHeaderLen + segmentDataLen) + transportCSum := pseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum = ^checksum(out[options.CsumStart:totalLen], transportCSum) + binary.BigEndian.PutUint16(out[options.CsumStart+options.CsumOffset:], transportCSum) + + nextSegmentDataAt += int(options.GSOSize) + } + return i, nil +} diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 9a9d38e..3f0dc53 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -9,6 +9,7 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "io" "unsafe" @@ -16,14 +17,6 @@ import ( "golang.org/x/sys/unix" ) -const tcpFlagsOffset = 13 - -const ( - tcpFlagFIN uint8 = 0x01 - tcpFlagPSH uint8 = 0x08 - tcpFlagACK uint8 = 0x10 -) - // virtioNetHdr is defined in the kernel in include/uapi/linux/virtio_net.h. The // kernel symbol is virtio_net_hdr. type virtioNetHdr struct { @@ -35,6 +28,30 @@ type virtioNetHdr struct { csumOffset uint16 } +func (v *virtioNetHdr) toGSOOptions() (GSOOptions, error) { + var gsoType GSOType + switch v.gsoType { + case unix.VIRTIO_NET_HDR_GSO_NONE: + gsoType = GSONone + case unix.VIRTIO_NET_HDR_GSO_TCPV4: + gsoType = GSOTCPv4 + case unix.VIRTIO_NET_HDR_GSO_TCPV6: + gsoType = GSOTCPv6 + case unix.VIRTIO_NET_HDR_GSO_UDP_L4: + gsoType = GSOUDPL4 + default: + return GSOOptions{}, fmt.Errorf("unsupported virtio gsoType: %d", v.gsoType) + } + return GSOOptions{ + GSOType: gsoType, + HdrLen: v.hdrLen, + CsumStart: v.csumStart, + CsumOffset: v.csumOffset, + GSOSize: v.gsoSize, + NeedsCsum: v.flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0, + }, nil +} + func (v *virtioNetHdr) decode(b []byte) error { if len(b) < virtioNetHdrLen { return io.ErrShortBuffer @@ -510,9 +527,7 @@ const ( ) const ( - ipv4SrcAddrOffset = 12 - ipv6SrcAddrOffset = 8 - maxUint16 = 1<<16 - 1 + maxUint16 = 1<<16 - 1 ) type groResult int @@ -894,100 +909,3 @@ func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGR errUDP := applyUDPCoalesceAccounting(bufs, offset, udpTable) return errors.Join(errTCP, errUDP) } - -// gsoSplit splits packets from in into outBuffs, writing the size of each -// element into sizes. It returns the number of buffers populated, and/or an -// error. -func gsoSplit(in []byte, hdr virtioNetHdr, outBuffs [][]byte, sizes []int, outOffset int, isV6 bool) (int, error) { - iphLen := int(hdr.csumStart) - srcAddrOffset := ipv6SrcAddrOffset - addrLen := 16 - if !isV6 { - in[10], in[11] = 0, 0 // clear ipv4 header checksum - srcAddrOffset = ipv4SrcAddrOffset - addrLen = 4 - } - transportCsumAt := int(hdr.csumStart + hdr.csumOffset) - in[transportCsumAt], in[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum - var firstTCPSeqNum uint32 - var protocol uint8 - if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 || hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV6 { - protocol = unix.IPPROTO_TCP - firstTCPSeqNum = binary.BigEndian.Uint32(in[hdr.csumStart+4:]) - } else { - protocol = unix.IPPROTO_UDP - } - nextSegmentDataAt := int(hdr.hdrLen) - i := 0 - for ; nextSegmentDataAt < len(in); i++ { - if i == len(outBuffs) { - return i - 1, ErrTooManySegments - } - nextSegmentEnd := nextSegmentDataAt + int(hdr.gsoSize) - if nextSegmentEnd > len(in) { - nextSegmentEnd = len(in) - } - segmentDataLen := nextSegmentEnd - nextSegmentDataAt - totalLen := int(hdr.hdrLen) + segmentDataLen - sizes[i] = totalLen - out := outBuffs[i][outOffset:] - - copy(out, in[:iphLen]) - if !isV6 { - // For IPv4 we are responsible for incrementing the ID field, - // updating the total len field, and recalculating the header - // checksum. - if i > 0 { - id := binary.BigEndian.Uint16(out[4:]) - id += uint16(i) - binary.BigEndian.PutUint16(out[4:], id) - } - binary.BigEndian.PutUint16(out[2:], uint16(totalLen)) - ipv4CSum := ^checksum(out[:iphLen], 0) - binary.BigEndian.PutUint16(out[10:], ipv4CSum) - } else { - // For IPv6 we are responsible for updating the payload length field. - binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) - } - - // copy transport header - copy(out[hdr.csumStart:hdr.hdrLen], in[hdr.csumStart:hdr.hdrLen]) - - if protocol == unix.IPPROTO_TCP { - // set TCP seq and adjust TCP flags - tcpSeq := firstTCPSeqNum + uint32(hdr.gsoSize*uint16(i)) - binary.BigEndian.PutUint32(out[hdr.csumStart+4:], tcpSeq) - if nextSegmentEnd != len(in) { - // FIN and PSH should only be set on last segment - clearFlags := tcpFlagFIN | tcpFlagPSH - out[hdr.csumStart+tcpFlagsOffset] &^= clearFlags - } - } else { - // set UDP header len - binary.BigEndian.PutUint16(out[hdr.csumStart+4:], uint16(segmentDataLen)+(hdr.hdrLen-hdr.csumStart)) - } - - // payload - copy(out[hdr.hdrLen:], in[nextSegmentDataAt:nextSegmentEnd]) - - // transport checksum - transportHeaderLen := int(hdr.hdrLen - hdr.csumStart) - lenForPseudo := uint16(transportHeaderLen + segmentDataLen) - transportCSum := pseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) - transportCSum = ^checksum(out[hdr.csumStart:totalLen], transportCSum) - binary.BigEndian.PutUint16(out[hdr.csumStart+hdr.csumOffset:], transportCSum) - - nextSegmentDataAt += int(hdr.gsoSize) - } - return i, nil -} - -func gsoNoneChecksum(in []byte, cSumStart, cSumOffset uint16) error { - cSumAt := cSumStart + cSumOffset - // The initial value at the checksum offset should be summed with the - // checksum we compute. This is typically the pseudo-header checksum. - initial := binary.BigEndian.Uint16(in[cSumAt:]) - in[cSumAt], in[cSumAt+1] = 0, 0 - binary.BigEndian.PutUint16(in[cSumAt:], ^checksum(in[cSumStart:], initial)) - return nil -} diff --git a/tun/offload_test.go b/tun/offload_test.go new file mode 100644 index 0000000..82a37b9 --- /dev/null +++ b/tun/offload_test.go @@ -0,0 +1,95 @@ +package tun + +import ( + "net/netip" + "testing" + + "github.com/tailscale/wireguard-go/conn" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" +) + +func Fuzz_GSOSplit(f *testing.F) { + const segmentSize = 100 + + tcpFields := &header.TCPFields{ + SrcPort: 1, + DstPort: 1, + SeqNum: 1, + AckNum: 1, + DataOffset: 20, + Flags: header.TCPFlagAck | header.TCPFlagPsh, + WindowSize: 3000, + } + udpFields := &header.UDPFields{ + SrcPort: 1, + DstPort: 1, + Length: 8 + segmentSize, + } + + gsoTCPv4 := make([]byte, 20+20+segmentSize) + header.IPv4(gsoTCPv4).Encode(&header.IPv4Fields{ + SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.1").AsSlice()), + DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.2").AsSlice()), + Protocol: ipProtoTCP, + TTL: 64, + TotalLength: uint16(len(gsoTCPv4)), + }) + header.TCP(gsoTCPv4[20:]).Encode(tcpFields) + + gsoUDPv4 := make([]byte, 20+8+segmentSize) + header.IPv4(gsoUDPv4).Encode(&header.IPv4Fields{ + SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.1").AsSlice()), + DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.2").AsSlice()), + Protocol: ipProtoUDP, + TTL: 64, + TotalLength: uint16(len(gsoUDPv4)), + }) + header.UDP(gsoTCPv4[20:]).Encode(udpFields) + + gsoTCPv6 := make([]byte, 40+20+segmentSize) + header.IPv6(gsoTCPv6).Encode(&header.IPv6Fields{ + SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::1").AsSlice()), + DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::2").AsSlice()), + TransportProtocol: ipProtoTCP, + HopLimit: 64, + PayloadLength: uint16(20 + segmentSize), + }) + header.TCP(gsoTCPv6[40:]).Encode(tcpFields) + + gsoUDPv6 := make([]byte, 40+8+segmentSize) + header.IPv6(gsoUDPv6).Encode(&header.IPv6Fields{ + SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::1").AsSlice()), + DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::2").AsSlice()), + TransportProtocol: ipProtoUDP, + HopLimit: 64, + PayloadLength: uint16(8 + segmentSize), + }) + header.UDP(gsoUDPv6[20:]).Encode(udpFields) + + out := make([][]byte, conn.IdealBatchSize) + for i := range out { + out[i] = make([]byte, 65535) + } + sizes := make([]int, conn.IdealBatchSize) + + f.Add(gsoTCPv4, int(GSOTCPv4), uint16(40), uint16(20), uint16(16), uint16(100), false) + f.Add(gsoUDPv4, int(GSOUDPL4), uint16(28), uint16(20), uint16(6), uint16(100), false) + f.Add(gsoTCPv6, int(GSOTCPv6), uint16(60), uint16(40), uint16(16), uint16(100), false) + f.Add(gsoUDPv6, int(GSOUDPL4), uint16(48), uint16(40), uint16(6), uint16(100), false) + + f.Fuzz(func(t *testing.T, pkt []byte, gsoType int, hdrLen, csumStart, csumOffset, gsoSize uint16, needsCsum bool) { + options := GSOOptions{ + GSOType: GSOType(gsoType), + HdrLen: hdrLen, + CsumStart: csumStart, + CsumOffset: csumOffset, + GSOSize: gsoSize, + NeedsCsum: needsCsum, + } + n, _ := GSOSplit(pkt, options, out, sizes, 0) + if n > len(sizes) { + t.Errorf("n (%d) > len(sizes): %d", n, len(sizes)) + } + }) +} diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 6aa03d4..664eecc 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -380,73 +380,32 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e return 0, err } in = in[virtioNetHdrLen:] - if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_NONE { - if hdr.flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 { - // This means CHECKSUM_PARTIAL in skb context. We are responsible - // for computing the checksum starting at hdr.csumStart and placing - // at hdr.csumOffset. - err = gsoNoneChecksum(in, hdr.csumStart, hdr.csumOffset) - if err != nil { - return 0, err - } - } - if len(in) > len(bufs[0][offset:]) { - return 0, fmt.Errorf("read len %d overflows bufs element len %d", len(in), len(bufs[0][offset:])) - } - n := copy(bufs[0][offset:], in) - sizes[0] = n - return 1, nil - } - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { - return 0, fmt.Errorf("unsupported virtio GSO type: %d", hdr.gsoType) + + options, err := hdr.toGSOOptions() + if err != nil { + return 0, err } - ipVersion := in[0] >> 4 - switch ipVersion { - case 4: - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { - return 0, fmt.Errorf("ip header version: %d, GSO type: %d", ipVersion, hdr.gsoType) - } - case 6: - if hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 && hdr.gsoType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { - return 0, fmt.Errorf("ip header version: %d, GSO type: %d", ipVersion, hdr.gsoType) - } - default: - return 0, fmt.Errorf("invalid ip header version: %d", ipVersion) - } - - // Don't trust hdr.hdrLen from the kernel as it can be equal to the length + // Don't trust HdrLen from the kernel as it can be equal to the length // of the entire first packet when the kernel is handling it as part of a // FORWARD path. Instead, parse the transport header length and add it onto - // csumStart, which is synonymous for IP header length. - if hdr.gsoType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 { - hdr.hdrLen = hdr.csumStart + 8 - } else { - if len(in) <= int(hdr.csumStart+12) { + // CsumStart, which is synonymous for IP header length. + if options.GSOType == GSOUDPL4 { + options.HdrLen = options.CsumStart + 8 + } else if options.GSOType != GSONone { + if len(in) <= int(options.CsumStart+12) { return 0, errors.New("packet is too short") } - tcpHLen := uint16(in[hdr.csumStart+12] >> 4 * 4) + tcpHLen := uint16(in[options.CsumStart+12] >> 4 * 4) if tcpHLen < 20 || tcpHLen > 60 { // A TCP header must be between 20 and 60 bytes in length. return 0, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) } - hdr.hdrLen = hdr.csumStart + tcpHLen + options.HdrLen = options.CsumStart + tcpHLen } - if len(in) < int(hdr.hdrLen) { - return 0, fmt.Errorf("length of packet (%d) < virtioNetHdr.hdrLen (%d)", len(in), hdr.hdrLen) - } - - if hdr.hdrLen < hdr.csumStart { - return 0, fmt.Errorf("virtioNetHdr.hdrLen (%d) < virtioNetHdr.csumStart (%d)", hdr.hdrLen, hdr.csumStart) - } - cSumAt := int(hdr.csumStart + hdr.csumOffset) - if cSumAt+1 >= len(in) { - return 0, fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(in)) - } - - return gsoSplit(in, hdr, bufs, sizes, offset, ipVersion == 6) + return GSOSplit(in, options, bufs, sizes, offset) } func (tun *NativeTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) { From 6c039a188c2d15592023f851731cd03214af32ec Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 23 Jul 2024 13:34:34 -0700 Subject: [PATCH 049/173] tun: export optimized IP checksum funcs External implementers of tun.Device may support GRO, requiring checksum offload. Signed-off-by: Jordan Whited --- tun/checksum.go | 4 +++- tun/checksum_amd64.go | 11 +++++++---- tun/checksum_generic.go | 2 +- tun/offload.go | 8 ++++---- tun/offload_linux.go | 16 ++++++++-------- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/tun/checksum.go b/tun/checksum.go index ee3f359..6634050 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -702,7 +702,9 @@ func pseudoHeaderChecksum32(protocol uint8, srcAddr, dstAddr []byte, totalLen ui return foldedSum } -func pseudoHeaderChecksum(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { +// PseudoHeaderChecksum computes an IP pseudo-header checksum. srcAddr and +// dstAddr must be 4 or 16 bytes in length. +func PseudoHeaderChecksum(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { if strconv.IntSize < 64 { return pseudoHeaderChecksum32(protocol, srcAddr, dstAddr, totalLen) } diff --git a/tun/checksum_amd64.go b/tun/checksum_amd64.go index 5e87693..4fb684e 100644 --- a/tun/checksum_amd64.go +++ b/tun/checksum_amd64.go @@ -2,12 +2,15 @@ package tun import "golang.org/x/sys/cpu" -// checksum computes an IP checksum starting with the provided initial value. -// The length of data should be at least 128 bytes for best performance. Smaller -// buffers will still compute a correct result. For best performance with -// smaller buffers, use shortChecksum(). var checksum = checksumAMD64 +// Checksum computes an IP checksum starting with the provided initial value. +// The length of data should be at least 128 bytes for best performance. Smaller +// buffers will still compute a correct result. +func Checksum(data []byte, initial uint16) uint16 { + return checksum(data, initial) +} + func init() { if cpu.X86.HasAVX && cpu.X86.HasAVX2 && cpu.X86.HasBMI2 { checksum = checksumAVX2 diff --git a/tun/checksum_generic.go b/tun/checksum_generic.go index d0bfb69..2ef201a 100644 --- a/tun/checksum_generic.go +++ b/tun/checksum_generic.go @@ -7,7 +7,7 @@ package tun import "strconv" -func checksum(data []byte, initial uint16) uint16 { +func Checksum(data []byte, initial uint16) uint16 { if strconv.IntSize < 64 { return checksumGeneric32(data, initial) } diff --git a/tun/offload.go b/tun/offload.go index 6627e46..6db437c 100644 --- a/tun/offload.go +++ b/tun/offload.go @@ -102,7 +102,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO // the checksum we compute. This is typically the pseudo-header sum. initial := binary.BigEndian.Uint16(in[cSumAt:]) in[cSumAt], in[cSumAt+1] = 0, 0 - binary.BigEndian.PutUint16(in[cSumAt:], ^checksum(in[options.CsumStart:], initial)) + binary.BigEndian.PutUint16(in[cSumAt:], ^Checksum(in[options.CsumStart:], initial)) } sizes[0] = copy(outBufs[0][outOffset:], in) return 1, nil @@ -179,7 +179,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO } out[10], out[11] = 0, 0 // clear ipv4 header checksum binary.BigEndian.PutUint16(out[2:], uint16(totalLen)) - ipv4CSum := ^checksum(out[:iphLen], 0) + ipv4CSum := ^Checksum(out[:iphLen], 0) binary.BigEndian.PutUint16(out[10:], ipv4CSum) } else { // For IPv6 we are responsible for updating the payload length field. @@ -210,8 +210,8 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO out[transportCsumAt], out[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum transportHeaderLen := int(options.HdrLen - options.CsumStart) lenForPseudo := uint16(transportHeaderLen + segmentDataLen) - transportCSum := pseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) - transportCSum = ^checksum(out[options.CsumStart:totalLen], transportCSum) + transportCSum := PseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum = ^Checksum(out[options.CsumStart:totalLen], transportCSum) binary.BigEndian.PutUint16(out[options.CsumStart+options.CsumOffset:], transportCSum) nextSegmentDataAt += int(options.GSOSize) diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 3f0dc53..fe34401 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -410,8 +410,8 @@ func checksumValid(pkt []byte, iphLen, proto uint8, isV6 bool) bool { addrSize = 16 } lenForPseudo := uint16(len(pkt) - int(iphLen)) - cSum := pseudoHeaderChecksum(proto, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], lenForPseudo) - return ^checksum(pkt[iphLen:], cSum) == 0 + cSum := PseudoHeaderChecksum(proto, pkt[srcAddrAt:srcAddrAt+addrSize], pkt[srcAddrAt+addrSize:srcAddrAt+addrSize*2], lenForPseudo) + return ^Checksum(pkt[iphLen:], cSum) == 0 } // coalesceResult represents the result of attempting to coalesce two TCP @@ -659,7 +659,7 @@ func applyTCPCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable) e hdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV4 pkt[10], pkt[11] = 0, 0 binary.BigEndian.PutUint16(pkt[2:], uint16(len(pkt))) // set new total length - iphCSum := ^checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum + iphCSum := ^Checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum binary.BigEndian.PutUint16(pkt[10:], iphCSum) // set IPv4 header checksum field } err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) @@ -679,8 +679,8 @@ func applyTCPCoalesceAccounting(bufs [][]byte, offset int, table *tcpGROTable) e srcAddrAt := offset + addrOffset srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] - psum := pseudoHeaderChecksum(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) - binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) + psum := PseudoHeaderChecksum(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) + binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], Checksum([]byte{}, psum)) } else { hdr := virtioNetHdr{} err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) @@ -716,7 +716,7 @@ func applyUDPCoalesceAccounting(bufs [][]byte, offset int, table *udpGROTable) e } else { pkt[10], pkt[11] = 0, 0 binary.BigEndian.PutUint16(pkt[2:], uint16(len(pkt))) // set new total length - iphCSum := ^checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum + iphCSum := ^Checksum(pkt[:item.iphLen], 0) // compute IPv4 header checksum binary.BigEndian.PutUint16(pkt[10:], iphCSum) // set IPv4 header checksum field } err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) @@ -739,8 +739,8 @@ func applyUDPCoalesceAccounting(bufs [][]byte, offset int, table *udpGROTable) e srcAddrAt := offset + addrOffset srcAddr := bufs[item.bufsIndex][srcAddrAt : srcAddrAt+addrLen] dstAddr := bufs[item.bufsIndex][srcAddrAt+addrLen : srcAddrAt+addrLen*2] - psum := pseudoHeaderChecksum(unix.IPPROTO_UDP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) - binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], checksum([]byte{}, psum)) + psum := PseudoHeaderChecksum(unix.IPPROTO_UDP, srcAddr, dstAddr, uint16(len(pkt)-int(item.iphLen))) + binary.BigEndian.PutUint16(pkt[hdr.csumStart+hdr.csumOffset:], Checksum([]byte{}, psum)) } else { hdr := virtioNetHdr{} err := hdr.encode(bufs[item.bufsIndex][offset-virtioNetHdrLen:]) From 71393c576b98c1903cd8d31450b603535586f81e Mon Sep 17 00:00:00 2001 From: Adrian Dewhurst Date: Wed, 31 Jul 2024 16:08:04 -0400 Subject: [PATCH 050/173] tun: fix checksum test failures on non-4KiB page sizes When generating page-aligned random bytes, random data started at the beginning of the buffer that will be chopped off. When the page size differs, the start of the returned slice is different than expected for the expected checksums, causing the tests to fail. --- tun/checksum_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tun/checksum_test.go b/tun/checksum_test.go index c40efc9..f5b8f18 100644 --- a/tun/checksum_test.go +++ b/tun/checksum_test.go @@ -21,26 +21,33 @@ type archChecksumDetails struct { f func([]byte, uint16) uint16 } -func deterministicRandomBytes(seed int64, length int) []byte { +func fillRandomBuffer(seed int64, buf []byte) { rng := rand.New(rand.NewSource(seed)) - buf := make([]byte, length) n, err := rng.Read(buf) if err != nil { panic(err) } - if n != length { + if n != len(buf) { panic("incomplete random buffer") } +} + +func deterministicRandomBytes(seed int64, length int) []byte { + buf := make([]byte, length) + fillRandomBuffer(seed, buf) return buf } func getPageAlignedRandomBytes(seed int64, length int) []byte { alignment := syscall.Getpagesize() - buf := deterministicRandomBytes(seed, length+(alignment-1)) + buf := make([]byte, length+(alignment-1)) bufPtr := uintptr(unsafe.Pointer(&buf[0])) alignedBufPtr := (bufPtr + uintptr(alignment-1)) & ^uintptr(alignment-1) alignedStart := int(alignedBufPtr - bufPtr) - return buf[alignedStart:] + + buf = buf[alignedStart : alignedStart+length] + fillRandomBuffer(seed, buf) + return buf } func TestChecksum(t *testing.T) { From 799c1978fafc07fae8a15a42c8535b70e6c69e6e Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 4 Sep 2024 12:17:44 -0700 Subject: [PATCH 051/173] tun: add method for disabling TCP GRO on Linux torvalds/linux@e269d79c7d35aa3808b1f3c1737d63dab504ddc8 broke virtio_net TCP & UDP GRO causing GRO writes to return EINVAL. The bug was then resolved later in torvalds/linux@89add40066f9ed9abe5f7f886fe5789ff7e0c50e. The offending commit was pulled into various LTS releases. Updates tailscale/tailscale#13041 Signed-off-by: Jordan Whited --- tun/offload_linux.go | 18 ++++----- tun/offload_linux_test.go | 82 ++++++++++++++++++++++----------------- tun/tun.go | 23 +++++++++-- tun/tun_linux.go | 45 ++++++++++++++++++--- 4 files changed, 114 insertions(+), 54 deletions(-) diff --git a/tun/offload_linux.go b/tun/offload_linux.go index fe34401..fb6ac5b 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -763,7 +763,7 @@ const ( udp6GROCandidate ) -func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType { +func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if len(b) < 28 { return notGROCandidate } @@ -772,17 +772,17 @@ func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType { // IPv4 packets w/IP options do not coalesce return notGROCandidate } - if b[9] == unix.IPPROTO_TCP && len(b) >= 40 { + if b[9] == unix.IPPROTO_TCP && len(b) >= 40 && gro.canTCPGRO() { return tcp4GROCandidate } - if b[9] == unix.IPPROTO_UDP && canUDPGRO { + if b[9] == unix.IPPROTO_UDP && gro.canUDPGRO() { return udp4GROCandidate } } else if b[0]>>4 == 6 { - if b[6] == unix.IPPROTO_TCP && len(b) >= 60 { + if b[6] == unix.IPPROTO_TCP && len(b) >= 60 && gro.canTCPGRO() { return tcp6GROCandidate } - if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && canUDPGRO { + if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && gro.canUDPGRO() { return udp6GROCandidate } } @@ -875,15 +875,15 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) // handleGRO evaluates bufs for GRO, and writes the indices of the resulting // packets into toWrite. toWrite, tcpTable, and udpTable should initially be // empty (but non-nil), and are passed in to save allocs as the caller may reset -// and recycle them across vectors of packets. canUDPGRO indicates if UDP GRO is -// supported. -func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, canUDPGRO bool, toWrite *[]int) error { +// and recycle them across vectors of packets. gro indicates if TCP and UDP GRO +// are supported/enabled. +func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, gro groDisablementFlags, toWrite *[]int) error { for i := range bufs { if offset < virtioNetHdrLen || offset > len(bufs[i])-1 { return errors.New("invalid offset") } var result groResult - switch packetIsGROCandidate(bufs[i][offset:], canUDPGRO) { + switch packetIsGROCandidate(bufs[i][offset:], gro) { case tcp4GROCandidate: result = tcpGRO(bufs, offset, i, tcpTable, false) case tcp6GROCandidate: diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go index 91f3941..4070378 100644 --- a/tun/offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -286,11 +286,11 @@ func Fuzz_handleGRO(f *testing.F) { pkt9 := udp6Packet(ip6PortA, ip6PortB, 100) pkt10 := udp6Packet(ip6PortA, ip6PortB, 100) pkt11 := udp6Packet(ip6PortA, ip6PortC, 100) - f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, true, offset) - f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, canUDPGRO bool, offset int) { + f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, 0, offset) + f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, gro int, offset int) { pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11} toWrite := make([]int, 0, len(pkts)) - handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), canUDPGRO, &toWrite) + handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), groDisablementFlags(gro), &toWrite) if len(toWrite) > len(pkts) { t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts)) } @@ -311,7 +311,7 @@ func Test_handleGRO(t *testing.T) { tests := []struct { name string pktsIn [][]byte - canUDPGRO bool + gro groDisablementFlags wantToWrite []int wantLens []int wantErr bool @@ -331,7 +331,7 @@ func Test_handleGRO(t *testing.T) { udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 }, - true, + 0, []int{0, 1, 2, 4, 5, 7, 9}, []int{240, 228, 128, 140, 260, 160, 248}, false, @@ -351,7 +351,7 @@ func Test_handleGRO(t *testing.T) { udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 }, - false, + udpGRODisabled, []int{0, 1, 2, 4, 5, 7, 8, 9, 10}, []int{240, 128, 128, 140, 260, 160, 128, 148, 148}, false, @@ -368,7 +368,7 @@ func Test_handleGRO(t *testing.T) { tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 201), // v6 flow 1 tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 301), // v6 flow 1 }, - true, + 0, []int{0, 2, 4, 6}, []int{240, 240, 260, 260}, false, @@ -383,7 +383,7 @@ func Test_handleGRO(t *testing.T) { udp4Packet(ip4PortA, ip4PortB, 100), udp4Packet(ip4PortA, ip4PortB, 100), }, - true, + 0, []int{0, 1, 3, 4}, []int{140, 240, 128, 228}, false, @@ -395,7 +395,7 @@ func Test_handleGRO(t *testing.T) { tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 seq 1 len 100 tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 }, - true, + 0, []int{0}, []int{340}, false, @@ -412,7 +412,7 @@ func Test_handleGRO(t *testing.T) { fields.TTL++ }), }, - true, + 0, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -429,7 +429,7 @@ func Test_handleGRO(t *testing.T) { fields.TOS++ }), }, - true, + 0, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -446,7 +446,7 @@ func Test_handleGRO(t *testing.T) { fields.Flags = 1 }), }, - true, + 0, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -463,7 +463,7 @@ func Test_handleGRO(t *testing.T) { fields.Flags = 2 }), }, - true, + 0, []int{0, 1, 2, 3}, []int{140, 140, 128, 128}, false, @@ -480,7 +480,7 @@ func Test_handleGRO(t *testing.T) { fields.HopLimit++ }), }, - true, + 0, []int{0, 1, 2, 3}, []int{160, 160, 148, 148}, false, @@ -497,7 +497,7 @@ func Test_handleGRO(t *testing.T) { fields.TrafficClass++ }), }, - true, + 0, []int{0, 1, 2, 3}, []int{160, 160, 148, 148}, false, @@ -507,7 +507,7 @@ func Test_handleGRO(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { toWrite := make([]int, 0, len(tt.pktsIn)) - err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.canUDPGRO, &toWrite) + err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.gro, &toWrite) if err != nil { if tt.wantErr { return @@ -552,99 +552,111 @@ func Test_packetIsGROCandidate(t *testing.T) { udp6TooShort := udp6[:47] tests := []struct { - name string - b []byte - canUDPGRO bool - want groCandidateType + name string + b []byte + gro groDisablementFlags + want groCandidateType }{ { "tcp4", tcp4, - true, + 0, tcp4GROCandidate, }, + { + "tcp4 no support", + tcp4, + tcpGRODisabled, + notGROCandidate, + }, { "tcp6", tcp6, - true, + 0, tcp6GROCandidate, }, + { + "tcp6 no support", + tcp6, + tcpGRODisabled, + notGROCandidate, + }, { "udp4", udp4, - true, + 0, udp4GROCandidate, }, { "udp4 no support", udp4, - false, + udpGRODisabled, notGROCandidate, }, { "udp6", udp6, - true, + 0, udp6GROCandidate, }, { "udp6 no support", udp6, - false, + udpGRODisabled, notGROCandidate, }, { "udp4 too short", udp4TooShort, - true, + 0, notGROCandidate, }, { "udp6 too short", udp6TooShort, - true, + 0, notGROCandidate, }, { "tcp4 too short", tcp4TooShort, - true, + 0, notGROCandidate, }, { "tcp6 too short", tcp6TooShort, - true, + 0, notGROCandidate, }, { "invalid IP version", []byte{0x00}, - true, + 0, notGROCandidate, }, { "invalid IP header len", ip4InvalidHeaderLen, - true, + 0, notGROCandidate, }, { "ip4 invalid protocol", ip4InvalidProtocol, - true, + 0, notGROCandidate, }, { "ip6 invalid protocol", ip6InvalidProtocol, - true, + 0, notGROCandidate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := packetIsGROCandidate(tt.b, tt.canUDPGRO); got != tt.want { + if got := packetIsGROCandidate(tt.b, tt.gro); got != tt.want { t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want) } }) diff --git a/tun/tun.go b/tun/tun.go index d3c5012..719a606 100644 --- a/tun/tun.go +++ b/tun/tun.go @@ -52,10 +52,25 @@ type Device interface { BatchSize() int } -type LinuxDevice interface { +// GRODevice is a Device extended with methods for disabling GRO. Certain OS +// versions may have offload bugs. Where these bugs negatively impact throughput +// or break connectivity entirely we can use these methods to disable the +// related offload. +// +// Linux has the following known, GRO bugs. +// +// torvalds/linux@e269d79c7d35aa3808b1f3c1737d63dab504ddc8 broke virtio_net +// TCP & UDP GRO causing GRO writes to return EINVAL. The bug was then +// resolved later in +// torvalds/linux@89add40066f9ed9abe5f7f886fe5789ff7e0c50e. The offending +// commit was pulled into various LTS releases. +// +// UDP GRO writes end up blackholing/dropping packets destined for a +// vxlan/geneve interface on kernel versions prior to 6.8.5. +type GRODevice interface { Device - // DisableUDPGRO disables UDP GRO if it is enabled. Certain device drivers - // (e.g. vxlan, geneve) do not properly handle coalesced UDP packets later - // in the stack, resulting in packet loss. + // DisableUDPGRO disables UDP GRO if it is enabled. DisableUDPGRO() + // DisableTCPGRO disables TCP GRO if it is enabled. + DisableTCPGRO() } diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 664eecc..4a03387 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -38,7 +38,6 @@ type NativeTun struct { statusListenersShutdown chan struct{} batchSize int vnetHdr bool - udpGSO bool closeOnce sync.Once @@ -53,7 +52,30 @@ type NativeTun struct { toWrite []int tcpGROTable *tcpGROTable udpGROTable *udpGROTable - udpGRO bool + gro groDisablementFlags +} + +type groDisablementFlags int + +const ( + tcpGRODisabled groDisablementFlags = 1 << iota + udpGRODisabled +) + +func (g *groDisablementFlags) disableTCPGRO() { + *g |= tcpGRODisabled +} + +func (g *groDisablementFlags) canTCPGRO() bool { + return (*g)&tcpGRODisabled == 0 +} + +func (g *groDisablementFlags) disableUDPGRO() { + *g |= udpGRODisabled +} + +func (g *groDisablementFlags) canUDPGRO() bool { + return (*g)&udpGRODisabled == 0 } func (tun *NativeTun) File() *os.File { @@ -346,7 +368,7 @@ func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { ) tun.toWrite = tun.toWrite[:0] if tun.vnetHdr { - err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.udpGRO, &tun.toWrite) + err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.gro, &tun.toWrite) if err != nil { return 0, err } @@ -462,9 +484,19 @@ func (tun *NativeTun) BatchSize() int { return tun.batchSize } +// DisableUDPGRO disables UDP GRO if it is enabled. See the GRODevice interface +// for cases where it should be called. func (tun *NativeTun) DisableUDPGRO() { tun.writeOpMu.Lock() - tun.udpGRO = false + tun.gro.disableUDPGRO() + tun.writeOpMu.Unlock() +} + +// DisableTCPGRO disables TCP GRO if it is enabled. See the GRODevice interface +// for cases where it should be called. +func (tun *NativeTun) DisableTCPGRO() { + tun.writeOpMu.Lock() + tun.gro.disableTCPGRO() tun.writeOpMu.Unlock() } @@ -503,8 +535,9 @@ func (tun *NativeTun) initFromFlags(name string) error { tun.batchSize = conn.IdealBatchSize // tunUDPOffloads were added in Linux v6.2. We do not return an // error if they are unsupported at runtime. - tun.udpGSO = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) == nil - tun.udpGRO = tun.udpGSO + if unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) != nil { + tun.gro.disableUDPGRO() + } } else { tun.batchSize = 1 } From 4e883d38c8d363e92e02445e4c664f2c27ffdb88 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 12 Nov 2024 16:53:55 -0800 Subject: [PATCH 052/173] tun: use x/sys/unix IoctlIfreq/NewIfreq to set MTU on Linux The manual struct packing was suspect: https://github.com/tailscale/tailscale/issues/11899 And no need for doing it manually if there's API for it already. Updates tailscale/tailscale#11899 Signed-off-by: Brad Fitzpatrick --- tun/tun_linux.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 4a03387..7cdbf88 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -269,21 +269,15 @@ func (tun *NativeTun) setMTU(n int) error { defer unix.Close(fd) - // do ioctl call - var ifr [ifReqSize]byte - copy(ifr[:], name) - *(*uint32)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = uint32(n) - _, _, errno := unix.Syscall( - unix.SYS_IOCTL, - uintptr(fd), - uintptr(unix.SIOCSIFMTU), - uintptr(unsafe.Pointer(&ifr[0])), - ) - - if errno != 0 { - return fmt.Errorf("failed to set MTU of TUN device: %w", errno) + req, err := unix.NewIfreq(name) + if err != nil { + return fmt.Errorf("unix.NewIfreq(%q): %w", name, err) + } + req.SetUint32(uint32(n)) + err = unix.IoctlIfreq(fd, unix.SIOCSIFMTU, req) + if err != nil { + return fmt.Errorf("failed to set MTU of TUN device %q: %w", name, err) } - return nil } From 0b8b35511f19726b3dc1ff25e57061c326ad0e5b Mon Sep 17 00:00:00 2001 From: Nahum Shalman Date: Mon, 2 Dec 2024 14:30:52 +0000 Subject: [PATCH 053/173] ipc: build on solaris/illumos --- ipc/uapi_fake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/uapi_fake.go b/ipc/uapi_fake.go index 3e3dcdf..a2e0f85 100644 --- a/ipc/uapi_fake.go +++ b/ipc/uapi_fake.go @@ -1,4 +1,4 @@ -//go:build wasm || plan9 || aix +//go:build wasm || plan9 || aix || solaris || illumos /* SPDX-License-Identifier: MIT * From b8da08c1067a827c4537a6dc2d0655764792208b Mon Sep 17 00:00:00 2001 From: drkivi <115035277+drkivi@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:43:02 +0330 Subject: [PATCH 054/173] Update Dockerfile golang -> 1.23.6 AWGTOOLS_RELEASE -> 1.0.20241018 Signed-off-by: drkivi <115035277+drkivi@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 590ec5a..73016f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.3 as awg +FROM golang:1.23.6 as awg COPY . /awg WORKDIR /awg RUN go mod download && \ @@ -6,7 +6,7 @@ RUN go mod download && \ go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin FROM alpine:3.19 -ARG AWGTOOLS_RELEASE="1.0.20240213" +ARG AWGTOOLS_RELEASE="1.0.20241018" RUN apk --no-cache add iproute2 iptables bash && \ cd /usr/bin/ && \ wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ From 668ddfd455a55091b262325c99d85be473a751f7 Mon Sep 17 00:00:00 2001 From: drkivi <115035277+drkivi@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:44:17 +0330 Subject: [PATCH 055/173] Update go.mod Submodules Version Up Signed-off-by: drkivi <115035277+drkivi@users.noreply.github.com> --- go.mod | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 115ae88..4575bc8 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ module github.com/amnezia-vpn/amneziawg-go -go 1.22.3 +go 1.23.6 require ( github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.21.0 - golang.org/x/net v0.21.0 - golang.org/x/sys v0.18.0 + golang.org/x/crypto v0.32.0 + golang.org/x/net v0.34.0 + golang.org/x/sys v0.29.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 + gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6 ) require ( - github.com/google/btree v1.0.1 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + github.com/google/btree v1.1.3 // indirect + golang.org/x/time v0.9.0 // indirect ) From c97b5b76158fd85b1d461c9937ba5ff9186912d9 Mon Sep 17 00:00:00 2001 From: drkivi <115035277+drkivi@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:44:58 +0330 Subject: [PATCH 056/173] Update go.sum Signed-off-by: drkivi <115035277+drkivi@users.noreply.github.com> --- go.sum | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/go.sum b/go.sum index 7b53725..10f1f2a 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,20 @@ -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.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.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6 h1:6B7MdW3OEbJqOMr7cEYU9bkzvCjUBX/JlXk12xcANuQ= +gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= From 91a0587fb251a72c28724ee111fe04cf1436ca4c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 3 Mar 2025 16:01:00 -0800 Subject: [PATCH 057/173] tun: add plan9 support Reviewed-by: James Tucker --- tun/tun_plan9.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tun/tun_plan9.go diff --git a/tun/tun_plan9.go b/tun/tun_plan9.go new file mode 100644 index 0000000..7b66ead --- /dev/null +++ b/tun/tun_plan9.go @@ -0,0 +1,147 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + */ + +package tun + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" +) + +type NativeTun struct { + name string // "/net/ipifc/2" + ctlFile *os.File + dataFile *os.File + events chan Event + errors chan error + closeOnce sync.Once +} + +func CreateTUN(_ string, mtu int) (Device, error) { + ctl, err := os.OpenFile("/net/ipifc/clone", os.O_RDWR, 0) + if err != nil { + return nil, err + } + nbuf := make([]byte, 5) + n, err := ctl.Read(nbuf) + if err != nil { + ctl.Close() + return nil, fmt.Errorf("error reading from clone file: %w", err) + } + ifn, err := strconv.Atoi(strings.TrimSpace(string(nbuf[:n]))) + if err != nil { + ctl.Close() + return nil, fmt.Errorf("error converting clone result %q to int: %w", nbuf[:n], err) + } + + if _, err := fmt.Fprintf(ctl, "bind pkt\n"); err != nil { + ctl.Close() + return nil, fmt.Errorf("error binding to pkt: %w", err) + } + if mtu > 0 { + if _, err := fmt.Fprintf(ctl, "mtu %d\n", mtu); err != nil { + ctl.Close() + return nil, fmt.Errorf("error setting MTU: %w", err) + } + } + + dataFile, err := os.OpenFile(fmt.Sprintf("/net/ipifc/%d/data", ifn), os.O_RDWR, 0) + if err != nil { + ctl.Close() + return nil, err + } + + tun := &NativeTun{ + ctlFile: ctl, + dataFile: dataFile, + name: fmt.Sprintf("/net/ipifc/%d", ifn), + events: make(chan Event, 10), + errors: make(chan error, 5), + } + tun.events <- EventUp + + return tun, nil +} + +func (tun *NativeTun) Name() (string, error) { + return tun.name, nil +} + +func (tun *NativeTun) File() *os.File { + return tun.ctlFile +} + +func (tun *NativeTun) Events() <-chan Event { + return tun.events +} + +func (tun *NativeTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) { + select { + case err := <-tun.errors: + return 0, err + default: + n, err := tun.dataFile.Read(bufs[0][offset:]) + if n == 1 && bufs[0][offset] == 0 { + // EOF + err = io.EOF + n = 0 + } + sizes[0] = n + return 1, err + } +} + +func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { + for i, buf := range bufs { + if _, err := tun.dataFile.Write(buf[offset:]); err != nil { + return i, err + } + } + return len(bufs), nil +} + +func (tun *NativeTun) Close() error { + var err1, err2 error + tun.closeOnce.Do(func() { + _, err1 := fmt.Fprintf(tun.ctlFile, "unbind\n") + if err := tun.ctlFile.Close(); err != nil && err1 == nil { + err1 = err + } + err2 = tun.dataFile.Close() + }) + if err1 != nil { + return err1 + } + return err2 +} + +func (tun *NativeTun) MTU() (int, error) { + var buf [100]byte + f, err := os.Open(tun.name + "/status") + if err != nil { + return 0, err + } + defer f.Close() + n, err := f.Read(buf[:]) + _, res, ok := strings.Cut(string(buf[:n]), " maxtu ") + if ok { + if mtus, _, ok := strings.Cut(res, " "); ok { + mtu, err := strconv.Atoi(mtus) + if err != nil { + return 0, fmt.Errorf("error converting mtu %q to int: %w", mtus, err) + } + return mtu, nil + } + } + return 0, fmt.Errorf("no 'maxtu' field found in %s/status", tun.name) +} + +func (tun *NativeTun) BatchSize() int { + return 1 +} From 71be0eb3a6547f172d17ce8b831b89e48052dc27 Mon Sep 17 00:00:00 2001 From: Mark Puha Date: Tue, 18 Mar 2025 08:34:23 +0100 Subject: [PATCH 058/173] faster and more secure junk creation --- Dockerfile | 2 +- device/device.go | 2 + device/device_test.go | 6 +- device/junk_creator.go | 69 ++++++++++++++++++++ device/junk_creator_test.go | 124 ++++++++++++++++++++++++++++++++++++ device/send.go | 32 +--------- device/util.go | 25 -------- device/util_test.go | 27 -------- go.mod | 8 +-- go.sum | 12 ++-- 10 files changed, 212 insertions(+), 95 deletions(-) create mode 100644 device/junk_creator.go create mode 100644 device/junk_creator_test.go delete mode 100644 device/util.go delete mode 100644 device/util_test.go diff --git a/Dockerfile b/Dockerfile index 73016f7..12159be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.6 as awg +FROM golang:1.24 as awg COPY . /awg WORKDIR /awg RUN go mod download && \ diff --git a/device/device.go b/device/device.go index 80e3793..1be15d0 100644 --- a/device/device.go +++ b/device/device.go @@ -95,6 +95,7 @@ type Device struct { isASecOn abool.AtomicBool aSecMux sync.RWMutex aSecCfg aSecCfgType + junkCreator junkCreator } type aSecCfgType struct { @@ -799,6 +800,7 @@ func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { } device.isASecOn.SetTo(isASecOn) + device.junkCreator, err = NewJunkCreator(device) device.aSecMux.Unlock() return err diff --git a/device/device_test.go b/device/device_test.go index e6664a6..d03610f 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -109,7 +109,7 @@ func genASecurityConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { "replace_peers", "true", "jc", "5", "jmin", "500", - "jmax", "501", + "jmax", "1000", "s1", "30", "s2", "40", "h1", "123456", @@ -131,7 +131,7 @@ func genASecurityConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { "replace_peers", "true", "jc", "5", "jmin", "500", - "jmax", "501", + "jmax", "1000", "s1", "30", "s2", "40", "h1", "123456", @@ -274,7 +274,7 @@ func TestTwoDevicePing(t *testing.T) { }) } -func TestTwoDevicePingASecurity(t *testing.T) { +func TestASecurityTwoDevicePing(t *testing.T) { goroutineLeakCheck(t) pair := genTestPair(t, true, true) t.Run("ping 1.0.0.1", func(t *testing.T) { diff --git a/device/junk_creator.go b/device/junk_creator.go new file mode 100644 index 0000000..3a2d3b4 --- /dev/null +++ b/device/junk_creator.go @@ -0,0 +1,69 @@ +package device + +import ( + "bytes" + crand "crypto/rand" + "fmt" + v2 "math/rand/v2" +) + +type junkCreator struct { + device *Device + cha8Rand *v2.ChaCha8 +} + +func NewJunkCreator(d *Device) (junkCreator, error) { + buf := make([]byte, 32) + _, err := crand.Read(buf) + if err != nil { + return junkCreator{}, err + } + return junkCreator{device: d, cha8Rand: v2.NewChaCha8([32]byte(buf))}, nil +} + +// Should be called with aSecMux RLocked +func (jc *junkCreator) createJunkPackets() ([][]byte, error) { + if jc.device.aSecCfg.junkPacketCount == 0 { + return nil, nil + } + + junks := make([][]byte, 0, jc.device.aSecCfg.junkPacketCount) + for i := 0; i < jc.device.aSecCfg.junkPacketCount; i++ { + packetSize := jc.randomPacketSize() + junk, err := jc.randomJunkWithSize(packetSize) + if err != nil { + return nil, fmt.Errorf("Failed to create junk packet: %v", err) + } + junks = append(junks, junk) + } + return junks, nil +} + +// Should be called with aSecMux RLocked +func (jc *junkCreator) randomPacketSize() int { + return int( + jc.cha8Rand.Uint64()%uint64( + jc.device.aSecCfg.junkPacketMaxSize-jc.device.aSecCfg.junkPacketMinSize, + ), + ) + jc.device.aSecCfg.junkPacketMinSize +} + +// Should be called with aSecMux RLocked +func (jc *junkCreator) appendJunk(writer *bytes.Buffer, size int) error { + headerJunk, err := jc.randomJunkWithSize(size) + if err != nil { + return fmt.Errorf("failed to create header junk: %v", err) + } + _, err = writer.Write(headerJunk) + if err != nil { + return fmt.Errorf("failed to write header junk: %v", err) + } + return nil +} + +// Should be called with aSecMux RLocked +func (jc *junkCreator) randomJunkWithSize(size int) ([]byte, error) { + junk := make([]byte, size) + _, err := jc.cha8Rand.Read(junk) + return junk, err +} diff --git a/device/junk_creator_test.go b/device/junk_creator_test.go new file mode 100644 index 0000000..d3cf2b3 --- /dev/null +++ b/device/junk_creator_test.go @@ -0,0 +1,124 @@ +package device + +import ( + "bytes" + "fmt" + "testing" + + "github.com/amnezia-vpn/amneziawg-go/conn/bindtest" + "github.com/amnezia-vpn/amneziawg-go/tun/tuntest" +) + +func setUpJunkCreator(t *testing.T) (junkCreator, error) { + cfg, _ := genASecurityConfigs(t) + tun := tuntest.NewChannelTUN() + binds := bindtest.NewChannelBinds() + level := LogLevelVerbose + dev := NewDevice( + tun.TUN(), + binds[0], + NewLogger(level, ""), + ) + + if err := dev.IpcSet(cfg[0]); err != nil { + t.Errorf("failed to configure device %v", err) + dev.Close() + return junkCreator{}, err + } + + jc, err := NewJunkCreator(dev) + + if err != nil { + t.Errorf("failed to create junk creator %v", err) + dev.Close() + return junkCreator{}, err + } + + return jc, nil +} + +func Test_junkCreator_createJunkPackets(t *testing.T) { + jc, err := setUpJunkCreator(t) + if err != nil { + return + } + t.Run("", func(t *testing.T) { + got, err := jc.createJunkPackets() + if err != nil { + t.Errorf( + "junkCreator.createJunkPackets() = %v; failed", + err, + ) + return + } + seen := make(map[string]bool) + for _, junk := range got { + key := string(junk) + if seen[key] { + t.Errorf( + "junkCreator.createJunkPackets() = %v, duplicate key: %v", + got, + junk, + ) + return + } + seen[key] = true + } + }) +} + +func Test_junkCreator_randomJunkWithSize(t *testing.T) { + t.Run("", func(t *testing.T) { + jc, err := setUpJunkCreator(t) + if err != nil { + return + } + r1, _ := jc.randomJunkWithSize(10) + r2, _ := jc.randomJunkWithSize(10) + fmt.Printf("%v\n%v\n", r1, r2) + if bytes.Equal(r1, r2) { + t.Errorf("same junks %v", err) + jc.device.Close() + return + } + }) +} + +func Test_junkCreator_randomPacketSize(t *testing.T) { + jc, err := setUpJunkCreator(t) + if err != nil { + return + } + for range [30]struct{}{} { + t.Run("", func(t *testing.T) { + if got := jc.randomPacketSize(); jc.device.aSecCfg.junkPacketMinSize > got || + got > jc.device.aSecCfg.junkPacketMaxSize { + t.Errorf( + "junkCreator.randomPacketSize() = %v, not between range [%v,%v]", + got, + jc.device.aSecCfg.junkPacketMinSize, + jc.device.aSecCfg.junkPacketMaxSize, + ) + } + }) + } +} + +func Test_junkCreator_appendJunk(t *testing.T) { + jc, err := setUpJunkCreator(t) + if err != nil { + return + } + t.Run("", func(t *testing.T) { + s := "apple" + buffer := bytes.NewBuffer([]byte(s)) + err := jc.appendJunk(buffer, 30) + if err != nil && + buffer.Len() != len(s)+30 { + t.Errorf("appendWithJunk() size don't match") + } + read := make([]byte, 50) + buffer.Read(read) + fmt.Println(string(read)) + }) +} diff --git a/device/send.go b/device/send.go index 1b4406d..7eca099 100644 --- a/device/send.go +++ b/device/send.go @@ -9,7 +9,6 @@ import ( "bytes" "encoding/binary" "errors" - "math/rand" "net" "os" "sync" @@ -129,7 +128,7 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { var junkedHeader []byte if peer.device.isAdvancedSecurityOn() { peer.device.aSecMux.RLock() - junks, err := peer.createJunkPackets() + junks, err := peer.device.junkCreator.createJunkPackets() peer.device.aSecMux.RUnlock() if err != nil { @@ -150,7 +149,7 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { if peer.device.aSecCfg.initPacketJunkSize != 0 { buf := make([]byte, 0, peer.device.aSecCfg.initPacketJunkSize) writer := bytes.NewBuffer(buf[:0]) - err = appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) + err = peer.device.junkCreator.appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) if err != nil { peer.device.log.Errorf("%v - %v", peer, err) peer.device.aSecMux.RUnlock() @@ -200,7 +199,7 @@ func (peer *Peer) SendHandshakeResponse() error { if peer.device.aSecCfg.responsePacketJunkSize != 0 { buf := make([]byte, 0, peer.device.aSecCfg.responsePacketJunkSize) writer := bytes.NewBuffer(buf[:0]) - err = appendJunk(writer, peer.device.aSecCfg.responsePacketJunkSize) + err = peer.device.junkCreator.appendJunk(writer, peer.device.aSecCfg.responsePacketJunkSize) if err != nil { peer.device.aSecMux.RUnlock() peer.device.log.Errorf("%v - %v", peer, err) @@ -469,31 +468,6 @@ top: } } -func (peer *Peer) createJunkPackets() ([][]byte, error) { - if peer.device.aSecCfg.junkPacketCount == 0 { - return nil, nil - } - - junks := make([][]byte, 0, peer.device.aSecCfg.junkPacketCount) - for i := 0; i < peer.device.aSecCfg.junkPacketCount; i++ { - packetSize := rand.Intn( - peer.device.aSecCfg.junkPacketMaxSize-peer.device.aSecCfg.junkPacketMinSize, - ) + peer.device.aSecCfg.junkPacketMinSize - - junk, err := randomJunkWithSize(packetSize) - if err != nil { - peer.device.log.Errorf( - "%v - Failed to create junk packet: %v", - peer, - err, - ) - return nil, err - } - junks = append(junks, junk) - } - return junks, nil -} - func (peer *Peer) FlushStagedPackets() { for { select { diff --git a/device/util.go b/device/util.go deleted file mode 100644 index aab8ab7..0000000 --- a/device/util.go +++ /dev/null @@ -1,25 +0,0 @@ -package device - -import ( - "bytes" - crand "crypto/rand" - "fmt" -) - -func appendJunk(writer *bytes.Buffer, size int) error { - headerJunk, err := randomJunkWithSize(size) - if err != nil { - return fmt.Errorf("failed to create header junk: %v", err) - } - _, err = writer.Write(headerJunk) - if err != nil { - return fmt.Errorf("failed to write header junk: %v", err) - } - return nil -} - -func randomJunkWithSize(size int) ([]byte, error) { - junk := make([]byte, size) - _, err := crand.Read(junk) - return junk, err -} diff --git a/device/util_test.go b/device/util_test.go deleted file mode 100644 index c061eef..0000000 --- a/device/util_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package device - -import ( - "bytes" - "fmt" - "testing" -) - -func Test_randomJunktWithSize(t *testing.T) { - junk, err := randomJunkWithSize(30) - fmt.Println(string(junk), len(junk), err) -} - -func Test_appendJunk(t *testing.T) { - t.Run("", func(t *testing.T) { - s := "apple" - buffer := bytes.NewBuffer([]byte(s)) - err := appendJunk(buffer, 30) - if err != nil && - buffer.Len() != len(s)+30 { - t.Errorf("appendWithJunk() size don't match") - } - read := make([]byte, 50) - buffer.Read(read) - fmt.Println(string(read)) - }) -} diff --git a/go.mod b/go.mod index 4575bc8..608969f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/amnezia-vpn/amneziawg-go -go 1.23.6 +go 1.24 require ( github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.32.0 - golang.org/x/net v0.34.0 - golang.org/x/sys v0.29.0 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.37.0 + golang.org/x/sys v0.31.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6 ) diff --git a/go.sum b/go.sum index 10f1f2a..497f949 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,14 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= From 113c8f1340ed8748da68528112d746469de11ae0 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 08:43:41 -0700 Subject: [PATCH 059/173] 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 Signed-off-by: Jordan Whited Signed-off-by: Jason A. Donenfeld --- device/pools.go | 11 ++++++----- device/pools_test.go | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/device/pools.go b/device/pools.go index 94f3dc7..55d2be7 100644 --- a/device/pools.go +++ b/device/pools.go @@ -7,14 +7,13 @@ package device import ( "sync" - "sync/atomic" ) type WaitPool struct { pool sync.Pool cond sync.Cond lock sync.Mutex - count atomic.Uint32 + count uint32 // Get calls not yet Put back max uint32 } @@ -27,10 +26,10 @@ func NewWaitPool(max uint32, new func() any) *WaitPool { func (p *WaitPool) Get() any { if p.max != 0 { p.lock.Lock() - for p.count.Load() >= p.max { + for p.count >= p.max { p.cond.Wait() } - p.count.Add(1) + p.count++ p.lock.Unlock() } return p.pool.Get() @@ -41,7 +40,9 @@ func (p *WaitPool) Put(x any) { if p.max == 0 { return } - p.count.Add(^uint32(0)) + p.lock.Lock() + defer p.lock.Unlock() + p.count-- p.cond.Signal() } diff --git a/device/pools_test.go b/device/pools_test.go index 82d7493..538230b 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -32,7 +32,9 @@ func TestWaitPool(t *testing.T) { wg.Add(workers) var max atomic.Uint32 updateMax := func() { - count := p.count.Load() + p.lock.Lock() + count := p.count + p.lock.Unlock() if count > p.max { t.Errorf("count (%d) > max (%d)", count, p.max) } From 867a4c4a3f3a1a8fc4934553c8091c094ed6bdf8 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 09:06:40 -0700 Subject: [PATCH 060/173] device: fix missed return of QueueOutboundElementsContainer to its WaitPool Fixes: 3bb8fec ("conn, device, tun: implement vectorized I/O plumbing") Reviewed-by: Brad Fitzpatrick Signed-off-by: Jordan Whited Signed-off-by: Jason A. Donenfeld --- device/send.go | 1 + 1 file changed, 1 insertion(+) diff --git a/device/send.go b/device/send.go index 769720a..b20b3c5 100644 --- a/device/send.go +++ b/device/send.go @@ -506,6 +506,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } + device.PutOutboundElementsContainer(elemsContainer) continue } dataSent := false From 9eb3221f1de589e5dd6a1721fdd7dc0fde0eb10b Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:48:53 +0200 Subject: [PATCH 061/173] global: bump copyright notice Signed-off-by: Jason A. Donenfeld --- README.md | 2 +- conn/bind_std.go | 2 +- conn/bind_windows.go | 2 +- conn/bindtest/bindtest.go | 2 +- conn/boundif_android.go | 2 +- conn/conn.go | 2 +- conn/conn_test.go | 2 +- conn/controlfns.go | 2 +- conn/controlfns_linux.go | 2 +- conn/controlfns_unix.go | 2 +- conn/controlfns_windows.go | 2 +- conn/default.go | 2 +- conn/errors_default.go | 2 +- conn/errors_linux.go | 2 +- conn/features_default.go | 2 +- conn/features_linux.go | 2 +- conn/gso_default.go | 2 +- conn/gso_linux.go | 2 +- conn/mark_default.go | 2 +- conn/mark_unix.go | 2 +- conn/sticky_default.go | 2 +- conn/sticky_linux.go | 2 +- conn/sticky_linux_test.go | 2 +- conn/winrio/rio_windows.go | 2 +- device/allowedips.go | 2 +- device/allowedips_rand_test.go | 2 +- device/allowedips_test.go | 2 +- device/bind_test.go | 2 +- device/channels.go | 2 +- device/constants.go | 2 +- device/cookie.go | 2 +- device/cookie_test.go | 2 +- device/device.go | 2 +- device/device_test.go | 2 +- device/endpoint_test.go | 2 +- device/indextable.go | 2 +- device/ip.go | 2 +- device/kdf_test.go | 2 +- device/keypair.go | 2 +- device/logger.go | 2 +- device/mobilequirks.go | 2 +- device/noise-helpers.go | 2 +- device/noise-protocol.go | 2 +- device/noise-types.go | 2 +- device/noise_test.go | 2 +- device/peer.go | 2 +- device/pools.go | 2 +- device/pools_test.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/queueconstants_ios.go | 2 +- device/queueconstants_windows.go | 2 +- device/race_disabled_test.go | 2 +- device/race_enabled_test.go | 2 +- device/receive.go | 2 +- device/send.go | 2 +- device/sticky_linux.go | 2 +- device/timers.go | 2 +- device/tun.go | 2 +- device/uapi.go | 2 +- format_test.go | 2 +- ipc/uapi_bsd.go | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_unix.go | 2 +- ipc/uapi_wasm.go | 2 +- ipc/uapi_windows.go | 2 +- main.go | 2 +- main_windows.go | 2 +- ratelimiter/ratelimiter.go | 2 +- ratelimiter/ratelimiter_test.go | 2 +- replay/replay.go | 2 +- replay/replay_test.go | 2 +- rwcancel/rwcancel.go | 2 +- tai64n/tai64n.go | 2 +- tai64n/tai64n_test.go | 2 +- tun/alignment_windows_test.go | 2 +- tun/netstack/examples/http_client.go | 2 +- tun/netstack/examples/http_server.go | 2 +- tun/netstack/examples/ping_client.go | 2 +- tun/netstack/tun.go | 2 +- tun/offload_linux.go | 2 +- tun/offload_linux_test.go | 2 +- tun/operateonfd.go | 2 +- tun/tun.go | 2 +- tun/tun_darwin.go | 2 +- tun/tun_freebsd.go | 2 +- tun/tun_linux.go | 2 +- tun/tun_openbsd.go | 2 +- tun/tun_windows.go | 2 +- tun/tuntest/tuntest.go | 2 +- 90 files changed, 90 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 074f7ec..709728d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ $ make ## License - Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + Copyright (C) 2017-2025 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 diff --git a/conn/bind_std.go b/conn/bind_std.go index 46df7fd..f5c8816 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/bind_windows.go b/conn/bind_windows.go index d5095e0..a3b8460 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 74e7add..46e20e6 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package bindtest diff --git a/conn/boundif_android.go b/conn/boundif_android.go index dd3ca5b..be69b2a 100644 --- a/conn/boundif_android.go +++ b/conn/boundif_android.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/conn.go b/conn/conn.go index a1f57d2..1304657 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -1,6 +1,6 @@ /* 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. diff --git a/conn/conn_test.go b/conn/conn_test.go index c6194ee..618d02b 100644 --- a/conn/conn_test.go +++ b/conn/conn_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns.go b/conn/controlfns.go index 4f7d90f..27421bd 100644 --- a/conn/controlfns.go +++ b/conn/controlfns.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index f6ab1d2..3447349 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_unix.go b/conn/controlfns_unix.go index 91692c0..b2e7570 100644 --- a/conn/controlfns_unix.go +++ b/conn/controlfns_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_windows.go b/conn/controlfns_windows.go index c3bdf7d..5e38305 100644 --- a/conn/controlfns_windows.go +++ b/conn/controlfns_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/default.go b/conn/default.go index b6f761b..2ce1579 100644 --- a/conn/default.go +++ b/conn/default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/errors_default.go b/conn/errors_default.go index f1e5b90..d967518 100644 --- a/conn/errors_default.go +++ b/conn/errors_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/errors_linux.go b/conn/errors_linux.go index 8e61000..037d820 100644 --- a/conn/errors_linux.go +++ b/conn/errors_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/features_default.go b/conn/features_default.go index d53ff5f..cae2bea 100644 --- a/conn/features_default.go +++ b/conn/features_default.go @@ -3,7 +3,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/features_linux.go b/conn/features_linux.go index 8959d93..6386023 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/gso_default.go b/conn/gso_default.go index 57780db..a9a3e80 100644 --- a/conn/gso_default.go +++ b/conn/gso_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/gso_linux.go b/conn/gso_linux.go index 8596b29..4ee31fa 100644 --- a/conn/gso_linux.go +++ b/conn/gso_linux.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/mark_default.go b/conn/mark_default.go index 3102384..72b266e 100644 --- a/conn/mark_default.go +++ b/conn/mark_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/mark_unix.go b/conn/mark_unix.go index d9e46ee..d0580d5 100644 --- a/conn/mark_unix.go +++ b/conn/mark_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_default.go b/conn/sticky_default.go index 0b21386..15b65af 100644 --- a/conn/sticky_default.go +++ b/conn/sticky_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_linux.go b/conn/sticky_linux.go index 8e206e9..adfedc1 100644 --- a/conn/sticky_linux.go +++ b/conn/sticky_linux.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_linux_test.go b/conn/sticky_linux_test.go index d2bd584..1b1ee68 100644 --- a/conn/sticky_linux_test.go +++ b/conn/sticky_linux_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/winrio/rio_windows.go b/conn/winrio/rio_windows.go index d1037bb..c396658 100644 --- a/conn/winrio/rio_windows.go +++ b/conn/winrio/rio_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package winrio diff --git a/device/allowedips.go b/device/allowedips.go index fa46f97..b40c817 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go index 07065c3..8dd9b67 100644 --- a/device/allowedips_rand_test.go +++ b/device/allowedips_rand_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/allowedips_test.go b/device/allowedips_test.go index cde068e..9ef8a76 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/bind_test.go b/device/bind_test.go index 302a521..d3fa565 100644 --- a/device/bind_test.go +++ b/device/bind_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/channels.go b/device/channels.go index e526f6b..be15d1c 100644 --- a/device/channels.go +++ b/device/channels.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/constants.go b/device/constants.go index 59854a1..41da618 100644 --- a/device/constants.go +++ b/device/constants.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/cookie.go b/device/cookie.go index 876f05d..a093c8b 100644 --- a/device/cookie.go +++ b/device/cookie.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/cookie_test.go b/device/cookie_test.go index 4f1e50a..c937290 100644 --- a/device/cookie_test.go +++ b/device/cookie_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/device.go b/device/device.go index 83c33ee..6854ed8 100644 --- a/device/device.go +++ b/device/device.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/device_test.go b/device/device_test.go index fff172b..0091e20 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/endpoint_test.go b/device/endpoint_test.go index 93a4998..85482d8 100644 --- a/device/endpoint_test.go +++ b/device/endpoint_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/indextable.go b/device/indextable.go index 00ade7d..2460fa6 100644 --- a/device/indextable.go +++ b/device/indextable.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/ip.go b/device/ip.go index eaf2363..f558744 100644 --- a/device/ip.go +++ b/device/ip.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/kdf_test.go b/device/kdf_test.go index f9c76d6..325db59 100644 --- a/device/kdf_test.go +++ b/device/kdf_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/keypair.go b/device/keypair.go index e3540d7..0b72e19 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/logger.go b/device/logger.go index 22b0df0..a2adea3 100644 --- a/device/logger.go +++ b/device/logger.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/mobilequirks.go b/device/mobilequirks.go index 0a0080e..af4be31 100644 --- a/device/mobilequirks.go +++ b/device/mobilequirks.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-helpers.go b/device/noise-helpers.go index c2f356b..35dd907 100644 --- a/device/noise-helpers.go +++ b/device/noise-helpers.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-protocol.go b/device/noise-protocol.go index e8f6145..b72acf8 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-types.go b/device/noise-types.go index e850359..41c944e 100644 --- a/device/noise-types.go +++ b/device/noise-types.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise_test.go b/device/noise_test.go index 2dd5324..f0928ac 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/peer.go b/device/peer.go index 47a2f14..ebf25f9 100644 --- a/device/peer.go +++ b/device/peer.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/pools.go b/device/pools.go index 55d2be7..2c18f41 100644 --- a/device/pools.go +++ b/device/pools.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/pools_test.go b/device/pools_test.go index 538230b..8381d5a 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index 25f700a..236dea1 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index ea763d0..b061185 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_ios.go b/device/queueconstants_ios.go index acd3cec..632e29d 100644 --- a/device/queueconstants_ios.go +++ b/device/queueconstants_ios.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_windows.go b/device/queueconstants_windows.go index 1eee32b..9a296d6 100644 --- a/device/queueconstants_windows.go +++ b/device/queueconstants_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/race_disabled_test.go b/device/race_disabled_test.go index bb5c450..14b3284 100644 --- a/device/race_disabled_test.go +++ b/device/race_disabled_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/race_enabled_test.go b/device/race_enabled_test.go index 4e9daea..f1ea5cf 100644 --- a/device/race_enabled_test.go +++ b/device/race_enabled_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/receive.go b/device/receive.go index 1ab3e29..c7b6c87 100644 --- a/device/receive.go +++ b/device/receive.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/send.go b/device/send.go index b20b3c5..38f55c2 100644 --- a/device/send.go +++ b/device/send.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 6057ff1..7307b7e 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -1,6 +1,6 @@ /* 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 * WireGuard's kernelspace implementation. This is more or less a straight port diff --git a/device/timers.go b/device/timers.go index d4a4ed4..32519aa 100644 --- a/device/timers.go +++ b/device/timers.go @@ -1,6 +1,6 @@ /* 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. */ diff --git a/device/tun.go b/device/tun.go index 2a2ace9..c85dd50 100644 --- a/device/tun.go +++ b/device/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/uapi.go b/device/uapi.go index d81dae3..521a741 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/format_test.go b/format_test.go index 6f6cab7..4d02c48 100644 --- a/format_test.go +++ b/format_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/ipc/uapi_bsd.go b/ipc/uapi_bsd.go index ddcaf27..fd433a5 100644 --- a/ipc/uapi_bsd.go +++ b/ipc/uapi_bsd.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 1562a18..fddded0 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_unix.go b/ipc/uapi_unix.go index e67be26..dcce167 100644 --- a/ipc/uapi_unix.go +++ b/ipc/uapi_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_wasm.go b/ipc/uapi_wasm.go index fa84684..50ac091 100644 --- a/ipc/uapi_wasm.go +++ b/ipc/uapi_wasm.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index aa023c9..86e60b0 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/main.go b/main.go index e016116..b6989e2 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/main_windows.go b/main_windows.go index a4dc46f..67036cf 100644 --- a/main_windows.go +++ b/main_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/ratelimiter/ratelimiter.go b/ratelimiter/ratelimiter.go index f7d05ef..ac69e3a 100644 --- a/ratelimiter/ratelimiter.go +++ b/ratelimiter/ratelimiter.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ratelimiter diff --git a/ratelimiter/ratelimiter_test.go b/ratelimiter/ratelimiter_test.go index 0bfa3af..71140da 100644 --- a/ratelimiter/ratelimiter_test.go +++ b/ratelimiter/ratelimiter_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ratelimiter diff --git a/replay/replay.go b/replay/replay.go index 8b99e23..46e224d 100644 --- a/replay/replay.go +++ b/replay/replay.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ // Package replay implements an efficient anti-replay algorithm as specified in RFC 6479. diff --git a/replay/replay_test.go b/replay/replay_test.go index 9a9e4a8..8378ec3 100644 --- a/replay/replay_test.go +++ b/replay/replay_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package replay diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index e397c0e..793e764 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ // Package rwcancel implements cancelable read/write operations on diff --git a/tai64n/tai64n.go b/tai64n/tai64n.go index 8f10b39..e1a97a5 100644 --- a/tai64n/tai64n.go +++ b/tai64n/tai64n.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tai64n diff --git a/tai64n/tai64n_test.go b/tai64n/tai64n_test.go index c70fc1a..d0b4425 100644 --- a/tai64n/tai64n_test.go +++ b/tai64n/tai64n_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tai64n diff --git a/tun/alignment_windows_test.go b/tun/alignment_windows_test.go index 67a785e..e3252b2 100644 --- a/tun/alignment_windows_test.go +++ b/tun/alignment_windows_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go index ccd32ed..d71267d 100644 --- a/tun/netstack/examples/http_client.go +++ b/tun/netstack/examples/http_client.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go index f5b7a8f..7278851 100644 --- a/tun/netstack/examples/http_server.go +++ b/tun/netstack/examples/http_server.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go index 2eef0fb..d1b562f 100644 --- a/tun/netstack/examples/ping_client.go +++ b/tun/netstack/examples/ping_client.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 2b73054..7279cd9 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package netstack diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 9ff7fea..5f0db06 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go index ae55c8c..d87e636 100644 --- a/tun/offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/operateonfd.go b/tun/operateonfd.go index f1beb6d..343f754 100644 --- a/tun/operateonfd.go +++ b/tun/operateonfd.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun.go b/tun/tun.go index 0ae53d0..336d642 100644 --- a/tun/tun.go +++ b/tun/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_darwin.go b/tun/tun_darwin.go index c9a6c0b..407b6f2 100644 --- a/tun/tun_darwin.go +++ b/tun/tun_darwin.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_freebsd.go b/tun/tun_freebsd.go index 7c65fd9..4adf3a1 100644 --- a/tun/tun_freebsd.go +++ b/tun/tun_freebsd.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_linux.go b/tun/tun_linux.go index bd69cb5..1461e06 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_openbsd.go b/tun/tun_openbsd.go index ae571b9..5aa9070 100644 --- a/tun/tun_openbsd.go +++ b/tun/tun_openbsd.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_windows.go b/tun/tun_windows.go index 2af8e3e..de65fb4 100644 --- a/tun/tun_windows.go +++ b/tun/tun_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go index d07e860..9c4564f 100644 --- a/tun/tuntest/tuntest.go +++ b/tun/tuntest/tuntest.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tuntest From 32546a15a87f253c8d03292a3be813b176a8b4e7 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:50:41 +0200 Subject: [PATCH 062/173] mod: bump deps Signed-off-by: Jason A. Donenfeld --- go.mod | 14 +++++++------- go.sum | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 919dc49..2a80e00 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module golang.zx2c4.com/wireguard -go 1.20 +go 1.23.1 require ( - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/sys v0.12.0 + golang.org/x/crypto v0.37.0 + golang.org/x/net v0.39.0 + golang.org/x/sys v0.32.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 + gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c ) require ( - github.com/google/btree v1.0.1 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + github.com/google/btree v1.1.2 // indirect + golang.org/x/time v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 6bcecea..61875c1 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -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/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= From e3c1354d27f53462801e1b86b4275699a6f9fdac Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:54:57 +0200 Subject: [PATCH 063/173] 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 --- tun/netstack/tun.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 7279cd9..04f6986 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -155,7 +155,7 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { func (tun *netTun) WriteNotify() { pkt := tun.ep.Read() - if pkt.IsNil() { + if pkt == nil { return } From 45916071ba13c8f6ec44dfc46bda0449d2d54a9f Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 5 May 2025 15:09:09 +0200 Subject: [PATCH 064/173] 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 Suggested-by: Colin Adler Signed-off-by: Jason A. Donenfeld --- tun/netstack/tun.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 04f6986..a7aec9e 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -43,6 +43,7 @@ type netTun struct { ep *channel.Endpoint stack *stack.Stack events chan tun.Event + notifyHandle *channel.NotificationHandle incomingPacket chan *buffer.View mtu int dnsServers []netip.Addr @@ -70,7 +71,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, if tcpipErr != nil { return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) } - dev.ep.AddNotify(dev) + dev.notifyHandle = dev.ep.AddNotify(dev) tcpipErr = dev.stack.CreateNIC(1, dev.ep) if tcpipErr != nil { return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr) @@ -167,13 +168,14 @@ func (tun *netTun) WriteNotify() { func (tun *netTun) Close() error { tun.stack.RemoveNIC(1) + tun.stack.Close() + tun.ep.RemoveNotify(tun.notifyHandle) + tun.ep.Close() if tun.events != nil { close(tun.events) } - tun.ep.Close() - if tun.incomingPacket != nil { close(tun.incomingPacket) } From b82c016264bf9b2089da1174795a8464f311eebf Mon Sep 17 00:00:00 2001 From: Tu Dinh Ngoc Date: Thu, 20 Jun 2024 13:28:38 +0000 Subject: [PATCH 065/173] 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 [Jason: simplified and cleaned up unit tests] Signed-off-by: Jason A. Donenfeld --- tun/checksum.go | 122 +++++++++++++++++++------------------------ tun/checksum_test.go | 63 ++++++++++++++++++++++ 2 files changed, 116 insertions(+), 69 deletions(-) diff --git a/tun/checksum.go b/tun/checksum.go index 29a8fc8..b489c56 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -1,102 +1,86 @@ package tun -import "encoding/binary" +import ( + "encoding/binary" + "math/bits" +) // TODO: Explore SIMD and/or other assembly optimizations. -// TODO: Test native endian loads. See RFC 1071 section 2 part B. func checksumNoFold(b []byte, initial uint64) uint64 { - ac := initial + tmp := make([]byte, 8) + binary.NativeEndian.PutUint64(tmp, initial) + ac := binary.BigEndian.Uint64(tmp) + var carry uint64 for len(b) >= 128 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) - ac += uint64(binary.BigEndian.Uint32(b[64:68])) - ac += uint64(binary.BigEndian.Uint32(b[68:72])) - ac += uint64(binary.BigEndian.Uint32(b[72:76])) - ac += uint64(binary.BigEndian.Uint32(b[76:80])) - ac += uint64(binary.BigEndian.Uint32(b[80:84])) - ac += uint64(binary.BigEndian.Uint32(b[84:88])) - ac += uint64(binary.BigEndian.Uint32(b[88:92])) - ac += uint64(binary.BigEndian.Uint32(b[92:96])) - ac += uint64(binary.BigEndian.Uint32(b[96:100])) - ac += uint64(binary.BigEndian.Uint32(b[100:104])) - ac += uint64(binary.BigEndian.Uint32(b[104:108])) - ac += uint64(binary.BigEndian.Uint32(b[108:112])) - ac += uint64(binary.BigEndian.Uint32(b[112:116])) - ac += uint64(binary.BigEndian.Uint32(b[116:120])) - ac += uint64(binary.BigEndian.Uint32(b[120:124])) - ac += uint64(binary.BigEndian.Uint32(b[124:128])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[56:64]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[64:72]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[72:80]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[80:88]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[88:96]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[96:104]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[104:112]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[112:120]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[120:128]), carry) + ac += carry b = b[128:] } if len(b) >= 64 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[56:64]), carry) + ac += carry b = b[64:] } if len(b) >= 32 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac += carry b = b[32:] } if len(b) >= 16 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac += carry b = b[16:] } if len(b) >= 8 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac += carry b = b[8:] } if len(b) >= 4 { - ac += uint64(binary.BigEndian.Uint32(b)) + ac, carry = bits.Add64(ac, uint64(binary.NativeEndian.Uint32(b[:4])), 0) + ac += carry b = b[4:] } if len(b) >= 2 { - ac += uint64(binary.BigEndian.Uint16(b)) + ac, carry = bits.Add64(ac, uint64(binary.NativeEndian.Uint16(b[:2])), 0) + ac += carry b = b[2:] } if len(b) == 1 { - ac += uint64(b[0]) << 8 + tmp := binary.NativeEndian.Uint16([]byte{b[0], 0}) + ac, carry = bits.Add64(ac, uint64(tmp), 0) + ac += carry } - return ac + binary.NativeEndian.PutUint64(tmp, ac) + return binary.BigEndian.Uint64(tmp) } func checksum(b []byte, initial uint64) uint16 { diff --git a/tun/checksum_test.go b/tun/checksum_test.go index c1ccff5..4ea9b8b 100644 --- a/tun/checksum_test.go +++ b/tun/checksum_test.go @@ -1,11 +1,74 @@ package tun import ( + "encoding/binary" "fmt" "math/rand" "testing" + + "golang.org/x/sys/unix" ) +func checksumRef(b []byte, initial uint16) uint16 { + ac := uint64(initial) + + for len(b) >= 2 { + ac += uint64(binary.BigEndian.Uint16(b)) + b = b[2:] + } + if len(b) == 1 { + ac += uint64(b[0]) << 8 + } + + for (ac >> 16) > 0 { + ac = (ac >> 16) + (ac & 0xffff) + } + return uint16(ac) +} + +func pseudoHeaderChecksumRefNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + sum := checksumRef(srcAddr, 0) + sum = checksumRef(dstAddr, sum) + sum = checksumRef([]byte{0, protocol}, sum) + tmp := make([]byte, 2) + binary.BigEndian.PutUint16(tmp, totalLen) + return checksumRef(tmp, sum) +} + +func TestChecksum(t *testing.T) { + for length := 0; length <= 9001; length++ { + buf := make([]byte, length) + rng := rand.New(rand.NewSource(1)) + rng.Read(buf) + csum := checksum(buf, 0x1234) + csumRef := checksumRef(buf, 0x1234) + if csum != csumRef { + t.Error("Expected checksum", csumRef, "got", csum) + } + } +} + +func TestPseudoHeaderChecksum(t *testing.T) { + for _, addrLen := range []int{4, 16} { + for length := 0; length <= 9001; length++ { + srcAddr := make([]byte, addrLen) + dstAddr := make([]byte, addrLen) + buf := make([]byte, length) + rng := rand.New(rand.NewSource(1)) + rng.Read(srcAddr) + rng.Read(dstAddr) + rng.Read(buf) + phSum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) + csum := checksum(buf, phSum) + phSumRef := pseudoHeaderChecksumRefNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) + csumRef := checksumRef(buf, phSumRef) + if csum != csumRef { + t.Error("Expected checksumRef", csumRef, "got", csum) + } + } + } +} + func BenchmarkChecksum(b *testing.B) { lengths := []int{ 64, From bc30fee374479c9a327285f77a06136d29884c07 Mon Sep 17 00:00:00 2001 From: ruokeqx Date: Thu, 2 Jan 2025 20:28:33 +0800 Subject: [PATCH 066/173] tun: darwin: fetch flags and mtu from if_msghdr directly Signed-off-by: ruokeqx Signed-off-by: Jason A. Donenfeld --- tun/tun_darwin.go | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/tun/tun_darwin.go b/tun/tun_darwin.go index 407b6f2..341afe3 100644 --- a/tun/tun_darwin.go +++ b/tun/tun_darwin.go @@ -6,14 +6,12 @@ package tun import ( - "errors" "fmt" "io" "net" "os" "sync" "syscall" - "time" "unsafe" "golang.org/x/sys/unix" @@ -30,18 +28,6 @@ type NativeTun struct { closeOnce sync.Once } -func retryInterfaceByIndex(index int) (iface *net.Interface, err error) { - for i := 0; i < 20; i++ { - iface, err = net.InterfaceByIndex(index) - if err != nil && errors.Is(err, unix.ENOMEM) { - time.Sleep(time.Duration(i) * time.Second / 3) - continue - } - return iface, err - } - return nil, err -} - func (tun *NativeTun) routineRouteListener(tunIfindex int) { var ( statusUp bool @@ -62,26 +48,22 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) { return } - if n < 14 { + if n < 28 { continue } - if data[3 /* type */] != unix.RTM_IFINFO { + if data[3 /* ifm_type */] != unix.RTM_IFINFO { continue } - ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifindex */]))) + ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifm_index */]))) if ifindex != tunIfindex { continue } - iface, err := retryInterfaceByIndex(ifindex) - if err != nil { - tun.errors <- err - return - } + flags := int(*(*uint32)(unsafe.Pointer(&data[8 /* ifm_flags */]))) // Up / Down event - up := (iface.Flags & net.FlagUp) != 0 + up := (flags & syscall.IFF_UP) != 0 if up != statusUp && up { tun.events <- EventUp } @@ -90,11 +72,13 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) { } statusUp = up + mtu := int(*(*uint32)(unsafe.Pointer(&data[24 /* ifm_data.ifi_mtu */]))) + // MTU changes - if iface.MTU != statusMTU { + if mtu != statusMTU { tun.events <- EventMTUUpdate } - statusMTU = iface.MTU + statusMTU = mtu } } From 77b6c824a8225328aebca78eba676ea7eb606a69 Mon Sep 17 00:00:00 2001 From: Tom Holford Date: Sun, 4 May 2025 18:49:03 +0200 Subject: [PATCH 067/173] global: replaced unused function params with _ Signed-off-by: Jason A. Donenfeld --- conn/errors_default.go | 2 +- conn/features_default.go | 2 +- device/allowedips_test.go | 2 +- device/sticky_default.go | 2 +- device/sticky_linux.go | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conn/errors_default.go b/conn/errors_default.go index d967518..3c9b223 100644 --- a/conn/errors_default.go +++ b/conn/errors_default.go @@ -7,6 +7,6 @@ package conn -func errShouldDisableUDPGSO(err error) bool { +func errShouldDisableUDPGSO(_ error) bool { return false } diff --git a/conn/features_default.go b/conn/features_default.go index cae2bea..9fc5088 100644 --- a/conn/features_default.go +++ b/conn/features_default.go @@ -10,6 +10,6 @@ package conn import "net" -func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { +func supportsUDPOffload(_ *net.UDPConn) (txOffload, rxOffload bool) { return } diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 9ef8a76..0ce45af 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -39,7 +39,7 @@ func TestCommonBits(t *testing.T) { } } -func benchmarkTrie(peerNumber, addressNumber, addressLength int, b *testing.B) { +func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { var trie *trieEntry var peers []*Peer root := parentIndirection{&trie, 2} diff --git a/device/sticky_default.go b/device/sticky_default.go index 1038256..22e1e15 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -7,6 +7,6 @@ import ( "golang.zx2c4.com/wireguard/rwcancel" ) -func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { +func (device *Device) startRouteListener(_ conn.Bind) (*rwcancel.RWCancel, error) { return nil, nil } diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 7307b7e..f23ff02 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -9,7 +9,7 @@ * * Currently there is no way to achieve this within the net package: * 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 @@ -47,7 +47,7 @@ func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, er 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 { peer *Peer endpoint *conn.Endpoint From 0e4482a086cb6dd9bb2baac8b538d1dbf354c136 Mon Sep 17 00:00:00 2001 From: Tom Holford Date: Sun, 4 May 2025 18:49:49 +0200 Subject: [PATCH 068/173] device: use rand.NewSource instead of rand.Seed Signed-off-by: Jason A. Donenfeld --- device/allowedips_rand_test.go | 10 +++++----- device/allowedips_test.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go index 8dd9b67..b863696 100644 --- a/device/allowedips_rand_test.go +++ b/device/allowedips_rand_test.go @@ -83,7 +83,7 @@ func TestTrieRandom(t *testing.T) { var peers []*Peer var allowedIPs AllowedIPs - rand.Seed(1) + rng := rand.New(rand.NewSource(1)) for n := 0; n < NumberOfPeers; n++ { peers = append(peers, &Peer{}) @@ -91,14 +91,14 @@ func TestTrieRandom(t *testing.T) { for n := 0; n < NumberOfAddresses; n++ { var addr4 [4]byte - rand.Read(addr4[:]) + rng.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[:]) + rng.Read(addr6[:]) cidr = uint8(rand.Intn(128) + 1) index = rand.Intn(NumberOfPeers) allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(addr6), int(cidr)), peers[index]) @@ -109,7 +109,7 @@ func TestTrieRandom(t *testing.T) { for p = 0; ; p++ { for n := 0; n < NumberOfTests; n++ { var addr4 [4]byte - rand.Read(addr4[:]) + rng.Read(addr4[:]) peer1 := slow4.Lookup(addr4[:]) peer2 := allowedIPs.Lookup(addr4[:]) if peer1 != peer2 { @@ -117,7 +117,7 @@ func TestTrieRandom(t *testing.T) { } var addr6 [16]byte - rand.Read(addr6[:]) + rng.Read(addr6[:]) peer1 = slow6.Lookup(addr6[:]) peer2 = allowedIPs.Lookup(addr6[:]) if peer1 != peer2 { diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 0ce45af..7df7da5 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -44,7 +44,7 @@ func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { var peers []*Peer root := parentIndirection{&trie, 2} - rand.Seed(1) + rng := rand.New(rand.NewSource(1)) const AddressLength = 4 @@ -54,15 +54,15 @@ func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { for n := 0; n < addressNumber; n++ { var addr [AddressLength]byte - rand.Read(addr[:]) - cidr := uint8(rand.Uint32() % (AddressLength * 8)) - index := rand.Int() % peerNumber + rng.Read(addr[:]) + cidr := uint8(rng.Uint32() % (AddressLength * 8)) + index := rng.Int() % peerNumber root.insert(addr[:], cidr, peers[index]) } for n := 0; n < b.N; n++ { var addr [AddressLength]byte - rand.Read(addr[:]) + rng.Read(addr[:]) trie.lookup(addr[:]) } } From 436f7fdc1670df26eee958de464cf5cb0385abec Mon Sep 17 00:00:00 2001 From: Kurnia D Win Date: Wed, 7 Jun 2023 12:41:02 +0700 Subject: [PATCH 069/173] rwcancel: fix wrong poll event flag on ReadyWrite It should be POLLIN because closeFd is read-only file. Signed-off-by: Kurnia D Win Signed-off-by: Jason A. Donenfeld --- rwcancel/rwcancel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index 793e764..4372453 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -64,7 +64,7 @@ func (rw *RWCancel) ReadyRead() bool { func (rw *RWCancel) ReadyWrite() bool { closeFd := int32(rw.closingReader.Fd()) - pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLOUT}} + pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLIN}} var err error for { _, err = unix.Poll(pollFds, -1) From 9e7529c3d2d0c54f4d5384c01645a9279e4740ae Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Thu, 26 Dec 2024 20:36:53 +0100 Subject: [PATCH 070/173] device: reduce RoutineHandshake allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Jason A. Donenfeld --- device/noise-protocol.go | 48 ++++++++++++++++++++++++++++++++++++++++ device/receive.go | 10 +++------ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index b72acf8..12368ec 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -6,6 +6,7 @@ package device import ( + "encoding/binary" "errors" "fmt" "sync" @@ -115,6 +116,53 @@ type MessageCookieReply struct { Cookie [blake2s.Size128 + poly1305.TagSize]byte } +var errMessageTooShort = errors.New("message too short") + +func (msg *MessageInitiation) unmarshal(b []byte) error { + if len(b) < MessageInitiationSize { + return errMessageTooShort + } + + 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 *MessageResponse) unmarshal(b []byte) error { + if len(b) < MessageResponseSize { + return errMessageTooShort + } + + 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 *MessageCookieReply) unmarshal(b []byte) error { + if len(b) < MessageCookieReplySize { + return errMessageTooShort + } + + 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 +} + type Handshake struct { state handshakeState mutex sync.RWMutex diff --git a/device/receive.go b/device/receive.go index c7b6c87..1392957 100644 --- a/device/receive.go +++ b/device/receive.go @@ -6,7 +6,6 @@ package device import ( - "bytes" "encoding/binary" "errors" "net" @@ -287,8 +286,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal packet var reply MessageCookieReply - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &reply) + err := reply.unmarshal(elem.packet) if err != nil { device.log.Verbosef("Failed to decode cookie reply") goto skip @@ -353,8 +351,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal var msg MessageInitiation - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &msg) + err := msg.unmarshal(elem.packet) if err != nil { device.log.Errorf("Failed to decode initiation message") goto skip @@ -386,8 +383,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal var msg MessageResponse - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &msg) + err := msg.unmarshal(elem.packet) if err != nil { device.log.Errorf("Failed to decode response message") goto skip From 842888ac5c93ccc5ee6344eceaadf783fcf1e243 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 15 May 2025 16:48:14 +0200 Subject: [PATCH 071/173] 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 --- device/noise-protocol.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 12368ec..5f713ee 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -116,11 +116,11 @@ type MessageCookieReply struct { Cookie [blake2s.Size128 + poly1305.TagSize]byte } -var errMessageTooShort = errors.New("message too short") +var errMessageLengthMismatch = errors.New("message length mismatch") func (msg *MessageInitiation) unmarshal(b []byte) error { - if len(b) < MessageInitiationSize { - return errMessageTooShort + if len(b) != MessageInitiationSize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) @@ -135,8 +135,8 @@ func (msg *MessageInitiation) unmarshal(b []byte) error { } func (msg *MessageResponse) unmarshal(b []byte) error { - if len(b) < MessageResponseSize { - return errMessageTooShort + if len(b) != MessageResponseSize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) @@ -151,8 +151,8 @@ func (msg *MessageResponse) unmarshal(b []byte) error { } func (msg *MessageCookieReply) unmarshal(b []byte) error { - if len(b) < MessageCookieReplySize { - return errMessageTooShort + if len(b) != MessageCookieReplySize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) From 1571e0fbae8e1d955e05dde80071bc86880d61b3 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 15 May 2025 16:54:03 +0200 Subject: [PATCH 072/173] version: bump snapshot Signed-off-by: Jason A. Donenfeld --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index db75bb9..80f2d4b 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const Version = "0.0.20230223" +const Version = "0.0.20250515" From 256bcbd70d5b4eaae2a9f21a9889498c0f89041c Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Tue, 20 May 2025 23:03:06 +0200 Subject: [PATCH 073/173] device: add support for removing allowedips individually This pairs with the recent change in wireguard-tools. Signed-off-by: Jason A. Donenfeld --- device/allowedips.go | 87 +++++++++++++++++++++++++-------------- device/allowedips_test.go | 57 +++++++++++++++++++++++++ device/uapi.go | 15 ++++++- 3 files changed, 125 insertions(+), 34 deletions(-) diff --git a/device/allowedips.go b/device/allowedips.go index b40c817..d15373c 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -223,6 +223,60 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) } } +func (node *trieEntry) remove() { + node.removeFromPeerEntries() + 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.mutex.Lock() + defer table.mutex.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() +} + func (table *AllowedIPs) RemoveByPeer(peer *Peer) { table.mutex.Lock() defer table.mutex.Unlock() @@ -230,38 +284,7 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) { var next *list.Element for elem := peer.trieEntries.Front(); elem != nil; elem = next { next = elem.Next() - node := elem.Value.(*trieEntry) - - 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() + elem.Value.(*trieEntry).remove() } } diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 7df7da5..a4b08a3 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -101,6 +101,10 @@ func TestTrieIPv4(t *testing.T) { allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer) } + remove := func(peer *Peer, a, b, c, d byte, cidr uint8) { + allowedIPs.Remove(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 { @@ -176,6 +180,21 @@ func TestTrieIPv4(t *testing.T) { allowedIPs.RemoveByPeer(a) assertNEQ(a, 192, 168, 0, 1) + + insert(a, 1, 0, 0, 0, 32) + insert(a, 192, 0, 0, 0, 24) + assertEQ(a, 1, 0, 0, 0) + assertEQ(a, 192, 0, 0, 1) + remove(a, 192, 0, 0, 0, 32) + assertEQ(a, 192, 0, 0, 1) + remove(nil, 192, 0, 0, 0, 24) + assertEQ(a, 192, 0, 0, 1) + remove(b, 192, 0, 0, 0, 24) + assertEQ(a, 192, 0, 0, 1) + remove(a, 192, 0, 0, 0, 24) + assertNEQ(a, 192, 0, 0, 1) + remove(a, 1, 0, 0, 0, 32) + assertNEQ(a, 1, 0, 0, 0) } /* Test ported from kernel implementation: @@ -211,6 +230,15 @@ func TestTrieIPv6(t *testing.T) { allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer) } + remove := 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.Remove(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)...) @@ -223,6 +251,18 @@ func TestTrieIPv6(t *testing.T) { } } + assertNEQ := 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 NEQ failed") + } + } + insert(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128) insert(c, 0x26075300, 0x60006b00, 0, 0, 64) insert(e, 0, 0, 0, 0, 0) @@ -244,4 +284,21 @@ func TestTrieIPv6(t *testing.T) { assertEQ(h, 0x24046800, 0x40040800, 0, 0) assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010) assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef) + + insert(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + insert(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) + remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 96) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(nil, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(b, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertNEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) + remove(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertNEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) } diff --git a/device/uapi.go b/device/uapi.go index 521a741..cc69488 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -371,7 +371,14 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error device.allowedips.RemoveByPeer(peer.Peer) 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) if err != nil { return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err) @@ -379,7 +386,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error if peer.dummy { 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": if value != "1" { From 264889f0bbdf9250bb8389a637dd5f38389bfe0b Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Sat, 17 May 2025 11:34:30 +0200 Subject: [PATCH 074/173] device: optimize message encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimize message encoding by eliminating binary.Write (which internally uses reflection) in favour of hand-rolled encoding. This is companion to 9e7529c3d2d0c54f4d5384c01645a9279e4740ae. 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 Signed-off-by: Jason A. Donenfeld --- device/noise-protocol.go | 45 ++++++++++++++++++++++++++++++++++++++++ device/send.go | 21 +++++++------------ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 5f713ee..5cf1702 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -134,6 +134,22 @@ func (msg *MessageInitiation) unmarshal(b []byte) error { 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 @@ -150,6 +166,22 @@ func (msg *MessageResponse) unmarshal(b []byte) error { 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 @@ -163,6 +195,19 @@ func (msg *MessageCookieReply) unmarshal(b []byte) error { 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 { state handshakeState mutex sync.RWMutex diff --git a/device/send.go b/device/send.go index 38f55c2..ff8f7da 100644 --- a/device/send.go +++ b/device/send.go @@ -6,7 +6,6 @@ package device import ( - "bytes" "encoding/binary" "errors" "net" @@ -124,10 +123,8 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - var buf [MessageInitiationSize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, msg) - packet := writer.Bytes() + packet := make([]byte, MessageInitiationSize) + _ = msg.marshal(packet) peer.cookieGenerator.AddMacs(packet) peer.timersAnyAuthenticatedPacketTraversal() @@ -155,10 +152,8 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - var buf [MessageResponseSize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, response) - packet := writer.Bytes() + packet := make([]byte, MessageResponseSize) + _ = response.marshal(packet) peer.cookieGenerator.AddMacs(packet) err = peer.BeginSymmetricSession() @@ -189,11 +184,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) return err } - var buf [MessageCookieReplySize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, reply) + packet := make([]byte, MessageCookieReplySize) + _ = reply.marshal(packet) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint) + return nil } From c92064f1ce35f82bf0c5a183b54e51fd5d58ad50 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 22 May 2025 01:33:55 +0200 Subject: [PATCH 075/173] conn: don't enable GRO on Linux < 5.12 Kernels below 5.12 are missing this: commit 98184612aca0a9ee42b8eb0262a49900ee9eef0d Author: Norman Maurer 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 Reviewed-by: David Ahern Signed-off-by: David S. Miller 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 --- conn/controlfns_linux.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index 3447349..f0deefa 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -13,6 +13,35 @@ import ( "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() { controlFns = append(controlFns, @@ -60,6 +89,17 @@ func init() { // Attempt to enable UDP_GRO 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) { _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) }) From f333402bd9cbe0f3eeb02507bd14e23d7d639280 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 22 May 2025 01:45:02 +0200 Subject: [PATCH 076/173] version: bump snapshot Signed-off-by: Jason A. Donenfeld --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 80f2d4b..d5524e8 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const Version = "0.0.20250515" +const Version = "0.0.20250522" From 6413b491d4dc3c5e5b295907b0b1242c6272427e Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 28 May 2025 15:05:06 -0700 Subject: [PATCH 077/173] .github/workflows: establish basic build and test actions jobs Upstream doesn't use GitHub actions for CI as GitHub is simply a mirror. Our workflows involve GitHub, so establish some basic CI jobs. Updates tailscale/corp#28877 Signed-off-by: Jordan Whited --- .github/workflows/test.yml | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a370995 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: ["tailscale"] + pull_request: + branches: ["tailscale"] + +jobs: + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: linux + goarch: "386" + - goos: linux + goarch: loong64 + - goos: linux + goarch: arm + goarm: "5" + - goos: linux + goarch: arm + goarm: "7" + # macOS + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + # Windows + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 + # BSDs + - goos: freebsd + goarch: amd64 + - goos: openbsd + goarch: amd64 + steps: + - name: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: setup go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + - name: build + run: go build ./... + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + + test: + runs-on: ubuntu-22.04 + steps: + - name: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: setup go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + - name: test + run: go test -race -v ./... From 2b555120c89de2b197f2cfed2787ccda28616c8c Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 11 Dec 2023 16:35:57 +0100 Subject: [PATCH 078/173] device: do atomic 64-bit add outside of vector loop Only bother updating the rxBytes counter once we've processed a whole vector, since additions are atomic. cherry picked from commit WireGuard/wireguard-go@542e565baa776ed4c5c55b73ef9aa38d33d55197 Updates tailscale/corp#28879 Signed-off-by: Jason A. Donenfeld --- device/receive.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/device/receive.go b/device/receive.go index af2db44..01804b7 100644 --- a/device/receive.go +++ b/device/receive.go @@ -447,6 +447,7 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { elemsContainer.Lock() validTailPacket := -1 dataPacketReceived := false + rxBytesLen := uint64(0) for i, elem := range elemsContainer.elems { if elem.packet == nil { // decryption failed @@ -463,7 +464,7 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { peer.timersHandshakeComplete() peer.SendStagedPackets() } - peer.rxBytes.Add(uint64(len(elem.packet) + MinMessageSize)) + rxBytesLen += uint64(len(elem.packet) + MinMessageSize) if len(elem.packet) == 0 { device.log.Verbosef("%v - Receiving keepalive packet", peer) @@ -512,6 +513,8 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)]) } + + peer.rxBytes.Add(rxBytesLen) if validTailPacket >= 0 { peer.SetEndpointFromPacket(elemsContainer.elems[validTailPacket].endpoint) peer.keepKeyFreshReceiving() From f74ff38c79b272da9b154a0a324cd51ec6a1fee8 Mon Sep 17 00:00:00 2001 From: Martin Basovnik Date: Fri, 10 Nov 2023 11:10:12 +0100 Subject: [PATCH 079/173] device: fix possible deadlock in close method There is a possible deadlock in `device.Close()` when you try to close the device very soon after its start. The problem is that two different methods acquire the same locks in different order: 1. device.Close() - device.ipcMutex.Lock() - device.state.Lock() 2. device.changeState(deviceState) - device.state.Lock() - device.ipcMutex.Lock() Reproducer: func TestDevice_deadlock(t *testing.T) { d := randDevice(t) d.Close() } Problem: $ go clean -testcache && go test -race -timeout 3s -run TestDevice_deadlock ./device | grep -A 10 sync.runtime_SemacquireMutex sync.runtime_SemacquireMutex(0xc000117d20?, 0x94?, 0x0?) /usr/local/opt/go/libexec/src/runtime/sema.go:77 +0x25 sync.(*Mutex).lockSlow(0xc000130518) /usr/local/opt/go/libexec/src/sync/mutex.go:171 +0x213 sync.(*Mutex).Lock(0xc000130518) /usr/local/opt/go/libexec/src/sync/mutex.go:90 +0x55 golang.zx2c4.com/wireguard/device.(*Device).Close(0xc000130500) /Users/martin.basovnik/git/basovnik/wireguard-go/device/device.go:373 +0xb6 golang.zx2c4.com/wireguard/device.TestDevice_deadlock(0x0?) /Users/martin.basovnik/git/basovnik/wireguard-go/device/device_test.go:480 +0x2c testing.tRunner(0xc00014c000, 0x131d7b0) -- sync.runtime_SemacquireMutex(0xc000130564?, 0x60?, 0xc000130548?) /usr/local/opt/go/libexec/src/runtime/sema.go:77 +0x25 sync.(*Mutex).lockSlow(0xc000130750) /usr/local/opt/go/libexec/src/sync/mutex.go:171 +0x213 sync.(*Mutex).Lock(0xc000130750) /usr/local/opt/go/libexec/src/sync/mutex.go:90 +0x55 sync.(*RWMutex).Lock(0xc000130750) /usr/local/opt/go/libexec/src/sync/rwmutex.go:147 +0x45 golang.zx2c4.com/wireguard/device.(*Device).upLocked(0xc000130500) /Users/martin.basovnik/git/basovnik/wireguard-go/device/device.go:179 +0x72 golang.zx2c4.com/wireguard/device.(*Device).changeState(0xc000130500, 0x1) cherry picked from commit WireGuard/wireguard-go@12269c2761734b15625017d8565745096325392f Updates tailscale/corp#28879 Signed-off-by: Martin Basovnik Signed-off-by: Jason A. Donenfeld --- device/device.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device/device.go b/device/device.go index 86dff0d..5b23485 100644 --- a/device/device.go +++ b/device/device.go @@ -368,10 +368,10 @@ func (device *Device) RemoveAllPeers() { } func (device *Device) Close() { - device.ipcMutex.Lock() - defer device.ipcMutex.Unlock() device.state.Lock() defer device.state.Unlock() + device.ipcMutex.Lock() + defer device.ipcMutex.Unlock() if device.isClosed() { return } From 19f7e298052c939c07a5f9eb2b60fb62ff0d10c1 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Thu, 26 Dec 2024 20:36:53 +0100 Subject: [PATCH 080/173] device: reduce RoutineHandshake allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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% cherry picked from commit WireGuard/wireguard-go@9e7529c3d2d0c54f4d5384c01645a9279e4740ae Updates tailscale/corp#28879 Signed-off-by: Alexander Yastrebov Signed-off-by: Jason A. Donenfeld --- device/noise-protocol.go | 48 ++++++++++++++++++++++++++++++++++++++++ device/receive.go | 10 +++------ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 2d8f984..2e7e9ae 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -6,6 +6,7 @@ package device import ( + "encoding/binary" "errors" "fmt" "sync" @@ -115,6 +116,53 @@ type MessageCookieReply struct { Cookie [blake2s.Size128 + poly1305.TagSize]byte } +var errMessageTooShort = errors.New("message too short") + +func (msg *MessageInitiation) unmarshal(b []byte) error { + if len(b) < MessageInitiationSize { + return errMessageTooShort + } + + 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 *MessageResponse) unmarshal(b []byte) error { + if len(b) < MessageResponseSize { + return errMessageTooShort + } + + 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 *MessageCookieReply) unmarshal(b []byte) error { + if len(b) < MessageCookieReplySize { + return errMessageTooShort + } + + 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 +} + type Handshake struct { state handshakeState mutex sync.RWMutex diff --git a/device/receive.go b/device/receive.go index 01804b7..bc37f91 100644 --- a/device/receive.go +++ b/device/receive.go @@ -6,7 +6,6 @@ package device import ( - "bytes" "encoding/binary" "errors" "net" @@ -287,8 +286,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal packet var reply MessageCookieReply - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &reply) + err := reply.unmarshal(elem.packet) if err != nil { device.log.Verbosef("Failed to decode cookie reply") goto skip @@ -353,8 +351,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal var msg MessageInitiation - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &msg) + err := msg.unmarshal(elem.packet) if err != nil { device.log.Errorf("Failed to decode initiation message") goto skip @@ -386,8 +383,7 @@ func (device *Device) RoutineHandshake(id int) { // unmarshal var msg MessageResponse - reader := bytes.NewReader(elem.packet) - err := binary.Read(reader, binary.LittleEndian, &msg) + err := msg.unmarshal(elem.packet) if err != nil { device.log.Errorf("Failed to decode response message") goto skip From ae0636254c5f6232d78b7d9a9908eee434639406 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 15 May 2025 16:48:14 +0200 Subject: [PATCH 081/173] 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. cherry picked from commit WireGuard/wireguard-go@842888ac5c93ccc5ee6344eceaadf783fcf1e243 Updates tailscale/corp#28879 Signed-off-by: Jason A. Donenfeld --- device/noise-protocol.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 2e7e9ae..1e99f68 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -116,11 +116,11 @@ type MessageCookieReply struct { Cookie [blake2s.Size128 + poly1305.TagSize]byte } -var errMessageTooShort = errors.New("message too short") +var errMessageLengthMismatch = errors.New("message length mismatch") func (msg *MessageInitiation) unmarshal(b []byte) error { - if len(b) < MessageInitiationSize { - return errMessageTooShort + if len(b) != MessageInitiationSize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) @@ -135,8 +135,8 @@ func (msg *MessageInitiation) unmarshal(b []byte) error { } func (msg *MessageResponse) unmarshal(b []byte) error { - if len(b) < MessageResponseSize { - return errMessageTooShort + if len(b) != MessageResponseSize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) @@ -151,8 +151,8 @@ func (msg *MessageResponse) unmarshal(b []byte) error { } func (msg *MessageCookieReply) unmarshal(b []byte) error { - if len(b) < MessageCookieReplySize { - return errMessageTooShort + if len(b) != MessageCookieReplySize { + return errMessageLengthMismatch } msg.Type = binary.LittleEndian.Uint32(b) From 022546570f38af621ea050600e3b1eb43d430a88 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Sat, 17 May 2025 11:34:30 +0200 Subject: [PATCH 082/173] device: optimize message encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimize message encoding by eliminating binary.Write (which internally uses reflection) in favour of hand-rolled encoding. This is companion to 9e7529c3d2d0c54f4d5384c01645a9279e4740ae. 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% cherry picked from commit WireGuard/wireguard-go@264889f0bbdf9250bb8389a637dd5f38389bfe0b Updates tailscale/corp#28879 Signed-off-by: Alexander Yastrebov Signed-off-by: Jason A. Donenfeld --- device/noise-protocol.go | 45 ++++++++++++++++++++++++++++++++++++++++ device/send.go | 21 +++++++------------ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 1e99f68..cb4dedb 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -134,6 +134,22 @@ func (msg *MessageInitiation) unmarshal(b []byte) error { 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 @@ -150,6 +166,22 @@ func (msg *MessageResponse) unmarshal(b []byte) error { 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 @@ -163,6 +195,19 @@ func (msg *MessageCookieReply) unmarshal(b []byte) error { 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 { state handshakeState mutex sync.RWMutex diff --git a/device/send.go b/device/send.go index 8ed2e5f..7900f57 100644 --- a/device/send.go +++ b/device/send.go @@ -6,7 +6,6 @@ package device import ( - "bytes" "encoding/binary" "errors" "net" @@ -124,10 +123,8 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - var buf [MessageInitiationSize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, msg) - packet := writer.Bytes() + packet := make([]byte, MessageInitiationSize) + _ = msg.marshal(packet) peer.cookieGenerator.AddMacs(packet) peer.timersAnyAuthenticatedPacketTraversal() @@ -155,10 +152,8 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - var buf [MessageResponseSize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, response) - packet := writer.Bytes() + packet := make([]byte, MessageResponseSize) + _ = response.marshal(packet) peer.cookieGenerator.AddMacs(packet) err = peer.BeginSymmetricSession() @@ -189,11 +184,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) return err } - var buf [MessageCookieReplySize]byte - writer := bytes.NewBuffer(buf[:0]) - binary.Write(writer, binary.LittleEndian, reply) + packet := make([]byte, MessageCookieReplySize) + _ = reply.marshal(packet) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint) + return nil } From 65cd6eed7d7f688acea0b24ba64a781a9c58248e Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Fri, 30 May 2025 13:14:43 -0700 Subject: [PATCH 083/173] conn,device: provide 8 free bytes at packet head to conn.Bind.Send() This enables a conn.Bind to bring its own encapsulating transport, e.g. VXLAN/Geneve. Updates tailscale/corp#27502 Signed-off-by: Jordan Whited --- conn/bind_std.go | 9 +++++---- conn/bind_std_test.go | 2 +- conn/bind_windows.go | 3 ++- conn/bindtest/bindtest.go | 3 ++- conn/conn.go | 8 +++++--- device/constants.go | 6 +++--- device/device_test.go | 10 +++++----- device/noise-protocol.go | 15 ++++++++------- device/peer.go | 5 ++++- device/send.go | 36 +++++++++++++++++++++++------------- 10 files changed, 58 insertions(+), 39 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 428e528..fc05634 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -341,7 +341,7 @@ func (e ErrUDPGSODisabled) Unwrap() error { return e.RetryErr } -func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { +func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { s.mu.Lock() blackhole := s.blackhole4 conn := s.ipv4 @@ -384,7 +384,7 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { ) retry: if offload { - n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, *msgs, setGSOSize) + n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, offset, *msgs, setGSOSize) err = s.send(conn, br, (*msgs)[:n]) if err != nil && offload && errShouldDisableUDPGSO(err) { offload = false @@ -401,7 +401,7 @@ retry: } else { for i := range bufs { (*msgs)[i].Addr = ua - (*msgs)[i].Buffers[0] = bufs[i] + (*msgs)[i].Buffers[0] = bufs[i][offset:] setSrcControl(&(*msgs)[i].OOB, endpoint.(*StdNetEndpoint)) } err = s.send(conn, br, (*msgs)[:len(bufs)]) @@ -450,7 +450,7 @@ const ( 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 ( base = -1 // index of msg we are currently coalescing into gsoSize int // segmentation size of msgs[base] @@ -462,6 +462,7 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs maxPayloadLen = maxIPv6PayloadLen } for i, buf := range bufs { + buf = buf[offset:] if i > 0 { msgLen := len(buf) baseLenBefore := len(msgs[base].Buffers[0]) diff --git a/conn/bind_std_test.go b/conn/bind_std_test.go index 34a3c9a..77af0d9 100644 --- a/conn/bind_std_test.go +++ b/conn/bind_std_test.go @@ -98,7 +98,7 @@ func Test_coalesceMessages(t *testing.T) { msgs[i].Buffers = make([][]byte, 1) msgs[i].OOB = make([]byte, 0, 2) } - got := coalesceMessages(addr, &StdNetEndpoint{AddrPort: addr.AddrPort()}, tt.buffs, msgs, mockSetGSOSize) + got := coalesceMessages(addr, &StdNetEndpoint{AddrPort: addr.AddrPort()}, tt.buffs, 0, msgs, mockSetGSOSize) if got != len(tt.wantLens) { t.Fatalf("got len %d want: %d", got, len(tt.wantLens)) } diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 9638b30..737b475 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -486,7 +486,7 @@ func (bind *afWinRingBind) Send(buf []byte, nend *WinRingEndpoint, isOpen *atomi 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) if !ok { return ErrWrongEndpointType @@ -494,6 +494,7 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint) error { bind.mu.RLock() defer bind.mu.RUnlock() for _, buf := range bufs { + buf = buf[offset:] switch nend.family { case windows.AF_INET: if bind.v4.blackhole { diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 836d983..741b776 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -107,8 +107,9 @@ func (c *ChannelBind) makeReceiveFunc(ch chan []byte) conn.ReceiveFunc { } } -func (c *ChannelBind) Send(bufs [][]byte, ep conn.Endpoint) error { +func (c *ChannelBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error { for _, b := range bufs { + b = b[offset:] select { case <-c.closeSignal: return net.ErrClosed diff --git a/conn/conn.go b/conn/conn.go index 8df5aaa..5083648 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -45,9 +45,11 @@ type Bind interface { // This mark is passed to the kernel as the socket option SO_MARK. SetMark(mark uint32) error - // Send writes one or more packets in bufs to address ep. The length of - // bufs must not exceed BatchSize(). - Send(bufs [][]byte, ep Endpoint) error + // Send writes one or more packets in bufs to address ep. A nonzero offset + // can be used to instruct the Bind on where packet data begins in each + // 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(s string) (Endpoint, error) diff --git a/device/constants.go b/device/constants.go index 59854a1..92c3bde 100644 --- a/device/constants.go +++ b/device/constants.go @@ -27,9 +27,9 @@ const ( ) const ( - MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive) - MaxMessageSize = MaxSegmentSize // maximum size of transport message - MaxContentSize = MaxSegmentSize - MessageTransportSize // maximum size of transport message content + MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive) + MaxMessageSize = MaxSegmentSize // maximum size of transport message + MaxContentSize = MaxSegmentSize - MessageTransportSize - MessageEncapsulatingTransportSize // maximum size of transport message content ) /* Implementation constants */ diff --git a/device/device_test.go b/device/device_test.go index 4088b9f..e443421 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -426,11 +426,11 @@ type fakeBindSized struct { 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 } +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, offset int) 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 diff --git a/device/noise-protocol.go b/device/noise-protocol.go index cb4dedb..555ce91 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -61,13 +61,14 @@ const ( ) const ( - MessageInitiationSize = 148 // size of handshake initiation message - MessageResponseSize = 92 // size of response message - MessageCookieReplySize = 64 // size of cookie reply message - MessageTransportHeaderSize = 16 // size of data preceding content in transport message - MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport - MessageKeepaliveSize = MessageTransportSize // size of keepalive - MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message + MessageInitiationSize = 148 // size of handshake initiation message + MessageResponseSize = 92 // size of response message + MessageCookieReplySize = 64 // size of cookie reply message + MessageTransportHeaderSize = 16 // size of data preceding content in transport message + MessageEncapsulatingTransportSize = 8 // size of optional, free (for use by conn.Bind.Send()) space preceding the transport header + MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport + MessageKeepaliveSize = MessageTransportSize // size of keepalive + MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message ) const ( diff --git a/device/peer.go b/device/peer.go index 876e5da..f79a0af 100644 --- a/device/peer.go +++ b/device/peer.go @@ -113,6 +113,9 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { return peer, nil } +// 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 { peer.device.net.RLock() defer peer.device.net.RUnlock() @@ -133,7 +136,7 @@ func (peer *Peer) SendBuffers(buffers [][]byte) error { } peer.endpoint.Unlock() - err := peer.device.net.bind.Send(buffers, endpoint) + err := peer.device.net.bind.Send(buffers, endpoint, MessageEncapsulatingTransportSize) if err == nil { var totalLen uint64 for _, b := range buffers { diff --git a/device/send.go b/device/send.go index 7900f57..bf854b7 100644 --- a/device/send.go +++ b/device/send.go @@ -45,11 +45,15 @@ import ( */ type QueueOutboundElement struct { - buffer *[MaxMessageSize]byte // slice holding the packet data - packet []byte // slice of "buffer" (always!) - nonce uint64 // nonce for encryption - keypair *Keypair // keypair for encryption - peer *Peer // related peer + buffer *[MaxMessageSize]byte // slice holding the packet data + // packet is always a slice of "buffer". The starting offset in buffer + // is either: + // a) MessageEncapsulatingTransportSize+MessageTransportHeaderSize (plaintext) + // b) 0 (post-encryption) + packet []byte + nonce uint64 // nonce for encryption + keypair *Keypair // keypair for encryption + peer *Peer // related peer } type QueueOutboundElementsContainer struct { @@ -123,14 +127,15 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - packet := make([]byte, MessageInitiationSize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageInitiationSize) + packet := buf[MessageEncapsulatingTransportSize:] _ = msg.marshal(packet) peer.cookieGenerator.AddMacs(packet) peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - err = peer.SendBuffers([][]byte{packet}) + err = peer.SendBuffers([][]byte{buf}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -152,7 +157,8 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - packet := make([]byte, MessageResponseSize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageResponseSize) + packet := buf[MessageEncapsulatingTransportSize:] _ = response.marshal(packet) peer.cookieGenerator.AddMacs(packet) @@ -167,7 +173,7 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketSent() // TODO: allocation could be avoided - err = peer.SendBuffers([][]byte{packet}) + err = peer.SendBuffers([][]byte{buf}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } @@ -184,10 +190,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) return err } - packet := make([]byte, MessageCookieReplySize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageCookieReplySize) + packet := buf[MessageEncapsulatingTransportSize:] _ = reply.marshal(packet) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint, MessageEncapsulatingTransportSize) return nil } @@ -220,7 +227,7 @@ func (device *Device) RoutineReadFromTUN() { elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize) count = 0 sizes = make([]int, batchSize) - offset = MessageTransportHeaderSize + offset = MessageEncapsulatingTransportSize + MessageTransportHeaderSize ) for i := range elems { @@ -446,7 +453,7 @@ func (device *Device) RoutineEncryption(id int) { for elemsContainer := range device.queue.encryption.c { for _, elem := range elemsContainer.elems { // populate header fields - header := elem.buffer[:MessageTransportHeaderSize] + header := elem.buffer[MessageEncapsulatingTransportSize : MessageEncapsulatingTransportSize+MessageTransportHeaderSize] fieldType := header[0:4] fieldReceiver := header[4:8] @@ -469,6 +476,9 @@ func (device *Device) RoutineEncryption(id int) { elem.packet, nil, ) + + // re-slice packet to include encapsulating transport space + elem.packet = elem.buffer[:MessageEncapsulatingTransportSize+len(elem.packet)] } elemsContainer.Unlock() } From deedce495a1616d4be20d8a12d78275abeb52d16 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 08:43:41 -0700 Subject: [PATCH 084/173] 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 Signed-off-by: Jordan Whited Signed-off-by: Jason A. Donenfeld --- device/pools.go | 11 ++++++----- device/pools_test.go | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/device/pools.go b/device/pools.go index 94f3dc7..55d2be7 100644 --- a/device/pools.go +++ b/device/pools.go @@ -7,14 +7,13 @@ package device import ( "sync" - "sync/atomic" ) type WaitPool struct { pool sync.Pool cond sync.Cond lock sync.Mutex - count atomic.Uint32 + count uint32 // Get calls not yet Put back max uint32 } @@ -27,10 +26,10 @@ func NewWaitPool(max uint32, new func() any) *WaitPool { func (p *WaitPool) Get() any { if p.max != 0 { p.lock.Lock() - for p.count.Load() >= p.max { + for p.count >= p.max { p.cond.Wait() } - p.count.Add(1) + p.count++ p.lock.Unlock() } return p.pool.Get() @@ -41,7 +40,9 @@ func (p *WaitPool) Put(x any) { if p.max == 0 { return } - p.count.Add(^uint32(0)) + p.lock.Lock() + defer p.lock.Unlock() + p.count-- p.cond.Signal() } diff --git a/device/pools_test.go b/device/pools_test.go index 82d7493..538230b 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -32,7 +32,9 @@ func TestWaitPool(t *testing.T) { wg.Add(workers) var max atomic.Uint32 updateMax := func() { - count := p.count.Load() + p.lock.Lock() + count := p.count + p.lock.Unlock() if count > p.max { t.Errorf("count (%d) > max (%d)", count, p.max) } From c803ce1e5bd7723274500226ee56395a50c3ab8f Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 27 Jun 2024 09:06:40 -0700 Subject: [PATCH 085/173] device: fix missed return of QueueOutboundElementsContainer to its WaitPool Fixes: 3bb8fec ("conn, device, tun: implement vectorized I/O plumbing") Reviewed-by: Brad Fitzpatrick Signed-off-by: Jordan Whited Signed-off-by: Jason A. Donenfeld --- device/send.go | 1 + 1 file changed, 1 insertion(+) diff --git a/device/send.go b/device/send.go index 7eca099..a00e2bb 100644 --- a/device/send.go +++ b/device/send.go @@ -568,6 +568,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) } + device.PutOutboundElementsContainer(elemsContainer) continue } dataSent := false From c0b6e6a2001c1ad6529e30cb4a289467a5684b3c Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:48:53 +0200 Subject: [PATCH 086/173] global: bump copyright notice Signed-off-by: Jason A. Donenfeld --- README.md | 1 + conn/bind_std.go | 2 +- conn/bind_windows.go | 2 +- conn/bindtest/bindtest.go | 2 +- conn/boundif_android.go | 2 +- conn/conn.go | 2 +- conn/conn_test.go | 2 +- conn/controlfns.go | 2 +- conn/controlfns_linux.go | 2 +- conn/controlfns_unix.go | 2 +- conn/controlfns_windows.go | 2 +- conn/default.go | 2 +- conn/errors_default.go | 2 +- conn/errors_linux.go | 2 +- conn/features_default.go | 2 +- conn/features_linux.go | 2 +- conn/gso_default.go | 2 +- conn/gso_linux.go | 2 +- conn/mark_default.go | 2 +- conn/mark_unix.go | 2 +- conn/sticky_default.go | 2 +- conn/sticky_linux.go | 2 +- conn/sticky_linux_test.go | 2 +- conn/winrio/rio_windows.go | 2 +- device/allowedips.go | 2 +- device/allowedips_rand_test.go | 2 +- device/allowedips_test.go | 2 +- device/bind_test.go | 2 +- device/channels.go | 2 +- device/constants.go | 2 +- device/cookie.go | 2 +- device/cookie_test.go | 2 +- device/device.go | 2 +- device/device_test.go | 2 +- device/endpoint_test.go | 2 +- device/indextable.go | 2 +- device/ip.go | 2 +- device/kdf_test.go | 2 +- device/keypair.go | 2 +- device/logger.go | 2 +- device/mobilequirks.go | 2 +- device/noise-helpers.go | 2 +- device/noise-protocol.go | 2 +- device/noise-types.go | 2 +- device/noise_test.go | 2 +- device/peer.go | 2 +- device/pools.go | 2 +- device/pools_test.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/queueconstants_ios.go | 2 +- device/queueconstants_windows.go | 2 +- device/race_disabled_test.go | 2 +- device/race_enabled_test.go | 2 +- device/receive.go | 2 +- device/send.go | 2 +- device/sticky_linux.go | 2 +- device/timers.go | 2 +- device/tun.go | 2 +- device/uapi.go | 2 +- format_test.go | 2 +- ipc/uapi_bsd.go | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_unix.go | 2 +- ipc/uapi_wasm.go | 2 +- ipc/uapi_windows.go | 2 +- main.go | 2 +- main_windows.go | 2 +- ratelimiter/ratelimiter.go | 2 +- ratelimiter/ratelimiter_test.go | 2 +- replay/replay.go | 2 +- replay/replay_test.go | 2 +- rwcancel/rwcancel.go | 2 +- tai64n/tai64n.go | 2 +- tai64n/tai64n_test.go | 2 +- tun/alignment_windows_test.go | 2 +- tun/netstack/examples/http_client.go | 2 +- tun/netstack/examples/http_server.go | 2 +- tun/netstack/examples/ping_client.go | 2 +- tun/netstack/tun.go | 2 +- tun/offload_linux.go | 2 +- tun/offload_linux_test.go | 2 +- tun/operateonfd.go | 2 +- tun/tun.go | 2 +- tun/tun_darwin.go | 2 +- tun/tun_freebsd.go | 2 +- tun/tun_linux.go | 2 +- tun/tun_openbsd.go | 2 +- tun/tun_windows.go | 2 +- tun/tuntest/tuntest.go | 2 +- 90 files changed, 90 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 853d318..428b752 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,4 @@ $ git clone https://github.com/amnezia-vpn/amneziawg-go $ cd amneziawg-go $ make ``` + diff --git a/conn/bind_std.go b/conn/bind_std.go index 312a538..6908ba8 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 6cfa099..1a0e021 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go index 42b0bb7..25b5eab 100644 --- a/conn/bindtest/bindtest.go +++ b/conn/bindtest/bindtest.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package bindtest diff --git a/conn/boundif_android.go b/conn/boundif_android.go index dd3ca5b..be69b2a 100644 --- a/conn/boundif_android.go +++ b/conn/boundif_android.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/conn.go b/conn/conn.go index a1f57d2..1304657 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -1,6 +1,6 @@ /* 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. diff --git a/conn/conn_test.go b/conn/conn_test.go index c6194ee..618d02b 100644 --- a/conn/conn_test.go +++ b/conn/conn_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns.go b/conn/controlfns.go index 4f7d90f..27421bd 100644 --- a/conn/controlfns.go +++ b/conn/controlfns.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index a2396fe..7bd3917 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_unix.go b/conn/controlfns_unix.go index 91692c0..b2e7570 100644 --- a/conn/controlfns_unix.go +++ b/conn/controlfns_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/controlfns_windows.go b/conn/controlfns_windows.go index c3bdf7d..5e38305 100644 --- a/conn/controlfns_windows.go +++ b/conn/controlfns_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/default.go b/conn/default.go index b6f761b..2ce1579 100644 --- a/conn/default.go +++ b/conn/default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/errors_default.go b/conn/errors_default.go index f1e5b90..d967518 100644 --- a/conn/errors_default.go +++ b/conn/errors_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/errors_linux.go b/conn/errors_linux.go index 7548a8a..9ed7d76 100644 --- a/conn/errors_linux.go +++ b/conn/errors_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/features_default.go b/conn/features_default.go index d53ff5f..cae2bea 100644 --- a/conn/features_default.go +++ b/conn/features_default.go @@ -3,7 +3,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/features_linux.go b/conn/features_linux.go index a6de8c1..936029e 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/gso_default.go b/conn/gso_default.go index 57780db..a9a3e80 100644 --- a/conn/gso_default.go +++ b/conn/gso_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/gso_linux.go b/conn/gso_linux.go index 8596b29..4ee31fa 100644 --- a/conn/gso_linux.go +++ b/conn/gso_linux.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/mark_default.go b/conn/mark_default.go index 3102384..72b266e 100644 --- a/conn/mark_default.go +++ b/conn/mark_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/mark_unix.go b/conn/mark_unix.go index d9e46ee..d0580d5 100644 --- a/conn/mark_unix.go +++ b/conn/mark_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_default.go b/conn/sticky_default.go index 0b21386..15b65af 100644 --- a/conn/sticky_default.go +++ b/conn/sticky_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_linux.go b/conn/sticky_linux.go index 8e206e9..adfedc1 100644 --- a/conn/sticky_linux.go +++ b/conn/sticky_linux.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/sticky_linux_test.go b/conn/sticky_linux_test.go index d2bd584..1b1ee68 100644 --- a/conn/sticky_linux_test.go +++ b/conn/sticky_linux_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package conn diff --git a/conn/winrio/rio_windows.go b/conn/winrio/rio_windows.go index d1037bb..c396658 100644 --- a/conn/winrio/rio_windows.go +++ b/conn/winrio/rio_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package winrio diff --git a/device/allowedips.go b/device/allowedips.go index fa46f97..b40c817 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go index 07065c3..8dd9b67 100644 --- a/device/allowedips_rand_test.go +++ b/device/allowedips_rand_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/allowedips_test.go b/device/allowedips_test.go index cde068e..9ef8a76 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/bind_test.go b/device/bind_test.go index 34d1c4a..24dec1f 100644 --- a/device/bind_test.go +++ b/device/bind_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/channels.go b/device/channels.go index e526f6b..be15d1c 100644 --- a/device/channels.go +++ b/device/channels.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/constants.go b/device/constants.go index 59854a1..41da618 100644 --- a/device/constants.go +++ b/device/constants.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/cookie.go b/device/cookie.go index 876f05d..a093c8b 100644 --- a/device/cookie.go +++ b/device/cookie.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/cookie_test.go b/device/cookie_test.go index 4f1e50a..c937290 100644 --- a/device/cookie_test.go +++ b/device/cookie_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/device.go b/device/device.go index 1be15d0..2a37321 100644 --- a/device/device.go +++ b/device/device.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/device_test.go b/device/device_test.go index d03610f..f66d326 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/endpoint_test.go b/device/endpoint_test.go index 93a4998..85482d8 100644 --- a/device/endpoint_test.go +++ b/device/endpoint_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/indextable.go b/device/indextable.go index 00ade7d..2460fa6 100644 --- a/device/indextable.go +++ b/device/indextable.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/ip.go b/device/ip.go index eaf2363..f558744 100644 --- a/device/ip.go +++ b/device/ip.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/kdf_test.go b/device/kdf_test.go index f9c76d6..325db59 100644 --- a/device/kdf_test.go +++ b/device/kdf_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/keypair.go b/device/keypair.go index cc2941a..05bce68 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/logger.go b/device/logger.go index 22b0df0..a2adea3 100644 --- a/device/logger.go +++ b/device/logger.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/mobilequirks.go b/device/mobilequirks.go index 0a0080e..af4be31 100644 --- a/device/mobilequirks.go +++ b/device/mobilequirks.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-helpers.go b/device/noise-helpers.go index c2f356b..35dd907 100644 --- a/device/noise-helpers.go +++ b/device/noise-helpers.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 1289249..789eb16 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise-types.go b/device/noise-types.go index e850359..41c944e 100644 --- a/device/noise-types.go +++ b/device/noise-types.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/noise_test.go b/device/noise_test.go index 075b6d3..8f72f29 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/peer.go b/device/peer.go index 5bc8ca4..8f88b2a 100644 --- a/device/peer.go +++ b/device/peer.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/pools.go b/device/pools.go index 55d2be7..2c18f41 100644 --- a/device/pools.go +++ b/device/pools.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/pools_test.go b/device/pools_test.go index 538230b..8381d5a 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index 1bff95a..741fcf3 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index 0061b63..f19e9b1 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_ios.go b/device/queueconstants_ios.go index acd3cec..632e29d 100644 --- a/device/queueconstants_ios.go +++ b/device/queueconstants_ios.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/queueconstants_windows.go b/device/queueconstants_windows.go index 1eee32b..9a296d6 100644 --- a/device/queueconstants_windows.go +++ b/device/queueconstants_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/race_disabled_test.go b/device/race_disabled_test.go index bb5c450..14b3284 100644 --- a/device/race_disabled_test.go +++ b/device/race_disabled_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/race_enabled_test.go b/device/race_enabled_test.go index 4e9daea..f1ea5cf 100644 --- a/device/race_enabled_test.go +++ b/device/race_enabled_test.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/receive.go b/device/receive.go index 66c1a32..0a4910a 100644 --- a/device/receive.go +++ b/device/receive.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/send.go b/device/send.go index a00e2bb..7f0faa3 100644 --- a/device/send.go +++ b/device/send.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 63164a7..5ff9dd6 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -1,6 +1,6 @@ /* 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 * WireGuard's kernelspace implementation. This is more or less a straight port diff --git a/device/timers.go b/device/timers.go index d4a4ed4..32519aa 100644 --- a/device/timers.go +++ b/device/timers.go @@ -1,6 +1,6 @@ /* 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. */ diff --git a/device/tun.go b/device/tun.go index 600a5e5..42178b2 100644 --- a/device/tun.go +++ b/device/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/device/uapi.go b/device/uapi.go index 777bdda..1b5e357 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package device diff --git a/format_test.go b/format_test.go index 6f6cab7..4d02c48 100644 --- a/format_test.go +++ b/format_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/ipc/uapi_bsd.go b/ipc/uapi_bsd.go index ddcaf27..fd433a5 100644 --- a/ipc/uapi_bsd.go +++ b/ipc/uapi_bsd.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 9738aea..058e8e7 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_unix.go b/ipc/uapi_unix.go index 0da452a..79604ee 100644 --- a/ipc/uapi_unix.go +++ b/ipc/uapi_unix.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_wasm.go b/ipc/uapi_wasm.go index fa84684..50ac091 100644 --- a/ipc/uapi_wasm.go +++ b/ipc/uapi_wasm.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index 31d2a63..321fe60 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ipc diff --git a/main.go b/main.go index 5a3dfef..f8fded9 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/main_windows.go b/main_windows.go index bbfa690..d3e2fe6 100644 --- a/main_windows.go +++ b/main_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/ratelimiter/ratelimiter.go b/ratelimiter/ratelimiter.go index f7d05ef..ac69e3a 100644 --- a/ratelimiter/ratelimiter.go +++ b/ratelimiter/ratelimiter.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ratelimiter diff --git a/ratelimiter/ratelimiter_test.go b/ratelimiter/ratelimiter_test.go index 0bfa3af..71140da 100644 --- a/ratelimiter/ratelimiter_test.go +++ b/ratelimiter/ratelimiter_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package ratelimiter diff --git a/replay/replay.go b/replay/replay.go index 8b99e23..46e224d 100644 --- a/replay/replay.go +++ b/replay/replay.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ // Package replay implements an efficient anti-replay algorithm as specified in RFC 6479. diff --git a/replay/replay_test.go b/replay/replay_test.go index 9a9e4a8..8378ec3 100644 --- a/replay/replay_test.go +++ b/replay/replay_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package replay diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index e397c0e..793e764 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ // Package rwcancel implements cancelable read/write operations on diff --git a/tai64n/tai64n.go b/tai64n/tai64n.go index 8f10b39..e1a97a5 100644 --- a/tai64n/tai64n.go +++ b/tai64n/tai64n.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tai64n diff --git a/tai64n/tai64n_test.go b/tai64n/tai64n_test.go index c70fc1a..d0b4425 100644 --- a/tai64n/tai64n_test.go +++ b/tai64n/tai64n_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tai64n diff --git a/tun/alignment_windows_test.go b/tun/alignment_windows_test.go index 67a785e..e3252b2 100644 --- a/tun/alignment_windows_test.go +++ b/tun/alignment_windows_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go index 4c4ea12..8b12ecc 100644 --- a/tun/netstack/examples/http_client.go +++ b/tun/netstack/examples/http_client.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go index 09929e0..80cd036 100644 --- a/tun/netstack/examples/http_server.go +++ b/tun/netstack/examples/http_server.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go index d7897b2..b243b5c 100644 --- a/tun/netstack/examples/ping_client.go +++ b/tun/netstack/examples/ping_client.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package main diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 2275173..13d1f11 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package netstack diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 89cf024..b61654b 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go index a68cd98..c04e003 100644 --- a/tun/offload_linux_test.go +++ b/tun/offload_linux_test.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/operateonfd.go b/tun/operateonfd.go index f1beb6d..343f754 100644 --- a/tun/operateonfd.go +++ b/tun/operateonfd.go @@ -2,7 +2,7 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun.go b/tun/tun.go index 0ae53d0..336d642 100644 --- a/tun/tun.go +++ b/tun/tun.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_darwin.go b/tun/tun_darwin.go index c9a6c0b..407b6f2 100644 --- a/tun/tun_darwin.go +++ b/tun/tun_darwin.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_freebsd.go b/tun/tun_freebsd.go index 7c65fd9..4adf3a1 100644 --- a/tun/tun_freebsd.go +++ b/tun/tun_freebsd.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 011e56a..bc6e7c1 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_openbsd.go b/tun/tun_openbsd.go index ae571b9..5aa9070 100644 --- a/tun/tun_openbsd.go +++ b/tun/tun_openbsd.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tun_windows.go b/tun/tun_windows.go index 2af8e3e..de65fb4 100644 --- a/tun/tun_windows.go +++ b/tun/tun_windows.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tun diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go index f620e0a..0fa70b0 100644 --- a/tun/tuntest/tuntest.go +++ b/tun/tuntest/tuntest.go @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: MIT * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. */ package tuntest From 704d57c27a6421df4fe5382eb9a524fc446511bd Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:50:41 +0200 Subject: [PATCH 087/173] mod: bump deps Signed-off-by: Jason A. Donenfeld --- go.mod | 8 ++++---- go.sum | 20 ++++++++------------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 608969f..99569f3 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.24 require ( github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.36.0 - golang.org/x/net v0.37.0 - golang.org/x/sys v0.31.0 + golang.org/x/crypto v0.37.0 + golang.org/x/net v0.39.0 + golang.org/x/sys v0.32.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6 + gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c ) require ( diff --git a/go.sum b/go.sum index 497f949..b8ac0bd 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,16 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6 h1:6B7MdW3OEbJqOMr7cEYU9bkzvCjUBX/JlXk12xcANuQ= -gvisor.dev/gvisor v0.0.0-20250130013005-04f9204697c6/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= From 6a7c878409f32dc39a82bc597766c81304ab9840 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 4 May 2025 17:54:57 +0200 Subject: [PATCH 088/173] 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 --- tun/netstack/tun.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 13d1f11..2c25649 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -155,7 +155,7 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { func (tun *netTun) WriteNotify() { pkt := tun.ep.Read() - if pkt.IsNil() { + if pkt == nil { return } From ac8a885a0361332602c51164aa87da2607146e27 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 5 May 2025 15:09:09 +0200 Subject: [PATCH 089/173] 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 Suggested-by: Colin Adler Signed-off-by: Jason A. Donenfeld --- tun/netstack/tun.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go index 2c25649..48a428b 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/tun.go @@ -43,6 +43,7 @@ type netTun struct { ep *channel.Endpoint stack *stack.Stack events chan tun.Event + notifyHandle *channel.NotificationHandle incomingPacket chan *buffer.View mtu int dnsServers []netip.Addr @@ -70,7 +71,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, if tcpipErr != nil { return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) } - dev.ep.AddNotify(dev) + dev.notifyHandle = dev.ep.AddNotify(dev) tcpipErr = dev.stack.CreateNIC(1, dev.ep) if tcpipErr != nil { return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr) @@ -167,13 +168,14 @@ func (tun *netTun) WriteNotify() { func (tun *netTun) Close() error { tun.stack.RemoveNIC(1) + tun.stack.Close() + tun.ep.RemoveNotify(tun.notifyHandle) + tun.ep.Close() if tun.events != nil { close(tun.events) } - tun.ep.Close() - if tun.incomingPacket != nil { close(tun.incomingPacket) } From 75d6c67a6711190bdee63770f7796446c5bbad01 Mon Sep 17 00:00:00 2001 From: Tu Dinh Ngoc Date: Thu, 20 Jun 2024 13:28:38 +0000 Subject: [PATCH 090/173] 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 [Jason: simplified and cleaned up unit tests] Signed-off-by: Jason A. Donenfeld --- tun/checksum.go | 122 +++++++++++++++++++------------------------ tun/checksum_test.go | 63 ++++++++++++++++++++++ 2 files changed, 116 insertions(+), 69 deletions(-) diff --git a/tun/checksum.go b/tun/checksum.go index 29a8fc8..b489c56 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -1,102 +1,86 @@ package tun -import "encoding/binary" +import ( + "encoding/binary" + "math/bits" +) // TODO: Explore SIMD and/or other assembly optimizations. -// TODO: Test native endian loads. See RFC 1071 section 2 part B. func checksumNoFold(b []byte, initial uint64) uint64 { - ac := initial + tmp := make([]byte, 8) + binary.NativeEndian.PutUint64(tmp, initial) + ac := binary.BigEndian.Uint64(tmp) + var carry uint64 for len(b) >= 128 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) - ac += uint64(binary.BigEndian.Uint32(b[64:68])) - ac += uint64(binary.BigEndian.Uint32(b[68:72])) - ac += uint64(binary.BigEndian.Uint32(b[72:76])) - ac += uint64(binary.BigEndian.Uint32(b[76:80])) - ac += uint64(binary.BigEndian.Uint32(b[80:84])) - ac += uint64(binary.BigEndian.Uint32(b[84:88])) - ac += uint64(binary.BigEndian.Uint32(b[88:92])) - ac += uint64(binary.BigEndian.Uint32(b[92:96])) - ac += uint64(binary.BigEndian.Uint32(b[96:100])) - ac += uint64(binary.BigEndian.Uint32(b[100:104])) - ac += uint64(binary.BigEndian.Uint32(b[104:108])) - ac += uint64(binary.BigEndian.Uint32(b[108:112])) - ac += uint64(binary.BigEndian.Uint32(b[112:116])) - ac += uint64(binary.BigEndian.Uint32(b[116:120])) - ac += uint64(binary.BigEndian.Uint32(b[120:124])) - ac += uint64(binary.BigEndian.Uint32(b[124:128])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[56:64]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[64:72]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[72:80]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[80:88]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[88:96]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[96:104]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[104:112]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[112:120]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[120:128]), carry) + ac += carry b = b[128:] } if len(b) >= 64 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) - ac += uint64(binary.BigEndian.Uint32(b[32:36])) - ac += uint64(binary.BigEndian.Uint32(b[36:40])) - ac += uint64(binary.BigEndian.Uint32(b[40:44])) - ac += uint64(binary.BigEndian.Uint32(b[44:48])) - ac += uint64(binary.BigEndian.Uint32(b[48:52])) - ac += uint64(binary.BigEndian.Uint32(b[52:56])) - ac += uint64(binary.BigEndian.Uint32(b[56:60])) - ac += uint64(binary.BigEndian.Uint32(b[60:64])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[32:40]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[40:48]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[48:56]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[56:64]), carry) + ac += carry b = b[64:] } if len(b) >= 32 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) - ac += uint64(binary.BigEndian.Uint32(b[16:20])) - ac += uint64(binary.BigEndian.Uint32(b[20:24])) - ac += uint64(binary.BigEndian.Uint32(b[24:28])) - ac += uint64(binary.BigEndian.Uint32(b[28:32])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[16:24]), carry) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[24:32]), carry) + ac += carry b = b[32:] } if len(b) >= 16 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) - ac += uint64(binary.BigEndian.Uint32(b[8:12])) - ac += uint64(binary.BigEndian.Uint32(b[12:16])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[8:16]), carry) + ac += carry b = b[16:] } if len(b) >= 8 { - ac += uint64(binary.BigEndian.Uint32(b[:4])) - ac += uint64(binary.BigEndian.Uint32(b[4:8])) + ac, carry = bits.Add64(ac, binary.NativeEndian.Uint64(b[:8]), 0) + ac += carry b = b[8:] } if len(b) >= 4 { - ac += uint64(binary.BigEndian.Uint32(b)) + ac, carry = bits.Add64(ac, uint64(binary.NativeEndian.Uint32(b[:4])), 0) + ac += carry b = b[4:] } if len(b) >= 2 { - ac += uint64(binary.BigEndian.Uint16(b)) + ac, carry = bits.Add64(ac, uint64(binary.NativeEndian.Uint16(b[:2])), 0) + ac += carry b = b[2:] } if len(b) == 1 { - ac += uint64(b[0]) << 8 + tmp := binary.NativeEndian.Uint16([]byte{b[0], 0}) + ac, carry = bits.Add64(ac, uint64(tmp), 0) + ac += carry } - return ac + binary.NativeEndian.PutUint64(tmp, ac) + return binary.BigEndian.Uint64(tmp) } func checksum(b []byte, initial uint64) uint16 { diff --git a/tun/checksum_test.go b/tun/checksum_test.go index c1ccff5..4ea9b8b 100644 --- a/tun/checksum_test.go +++ b/tun/checksum_test.go @@ -1,11 +1,74 @@ package tun import ( + "encoding/binary" "fmt" "math/rand" "testing" + + "golang.org/x/sys/unix" ) +func checksumRef(b []byte, initial uint16) uint16 { + ac := uint64(initial) + + for len(b) >= 2 { + ac += uint64(binary.BigEndian.Uint16(b)) + b = b[2:] + } + if len(b) == 1 { + ac += uint64(b[0]) << 8 + } + + for (ac >> 16) > 0 { + ac = (ac >> 16) + (ac & 0xffff) + } + return uint16(ac) +} + +func pseudoHeaderChecksumRefNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + sum := checksumRef(srcAddr, 0) + sum = checksumRef(dstAddr, sum) + sum = checksumRef([]byte{0, protocol}, sum) + tmp := make([]byte, 2) + binary.BigEndian.PutUint16(tmp, totalLen) + return checksumRef(tmp, sum) +} + +func TestChecksum(t *testing.T) { + for length := 0; length <= 9001; length++ { + buf := make([]byte, length) + rng := rand.New(rand.NewSource(1)) + rng.Read(buf) + csum := checksum(buf, 0x1234) + csumRef := checksumRef(buf, 0x1234) + if csum != csumRef { + t.Error("Expected checksum", csumRef, "got", csum) + } + } +} + +func TestPseudoHeaderChecksum(t *testing.T) { + for _, addrLen := range []int{4, 16} { + for length := 0; length <= 9001; length++ { + srcAddr := make([]byte, addrLen) + dstAddr := make([]byte, addrLen) + buf := make([]byte, length) + rng := rand.New(rand.NewSource(1)) + rng.Read(srcAddr) + rng.Read(dstAddr) + rng.Read(buf) + phSum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) + csum := checksum(buf, phSum) + phSumRef := pseudoHeaderChecksumRefNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) + csumRef := checksumRef(buf, phSumRef) + if csum != csumRef { + t.Error("Expected checksumRef", csumRef, "got", csum) + } + } + } +} + func BenchmarkChecksum(b *testing.B) { lengths := []int{ 64, From 8a2b2bf4f49f56ef379dfb2104d1d312e4479182 Mon Sep 17 00:00:00 2001 From: ruokeqx Date: Thu, 2 Jan 2025 20:28:33 +0800 Subject: [PATCH 091/173] tun: darwin: fetch flags and mtu from if_msghdr directly Signed-off-by: ruokeqx Signed-off-by: Jason A. Donenfeld --- tun/tun_darwin.go | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/tun/tun_darwin.go b/tun/tun_darwin.go index 407b6f2..341afe3 100644 --- a/tun/tun_darwin.go +++ b/tun/tun_darwin.go @@ -6,14 +6,12 @@ package tun import ( - "errors" "fmt" "io" "net" "os" "sync" "syscall" - "time" "unsafe" "golang.org/x/sys/unix" @@ -30,18 +28,6 @@ type NativeTun struct { closeOnce sync.Once } -func retryInterfaceByIndex(index int) (iface *net.Interface, err error) { - for i := 0; i < 20; i++ { - iface, err = net.InterfaceByIndex(index) - if err != nil && errors.Is(err, unix.ENOMEM) { - time.Sleep(time.Duration(i) * time.Second / 3) - continue - } - return iface, err - } - return nil, err -} - func (tun *NativeTun) routineRouteListener(tunIfindex int) { var ( statusUp bool @@ -62,26 +48,22 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) { return } - if n < 14 { + if n < 28 { continue } - if data[3 /* type */] != unix.RTM_IFINFO { + if data[3 /* ifm_type */] != unix.RTM_IFINFO { continue } - ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifindex */]))) + ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifm_index */]))) if ifindex != tunIfindex { continue } - iface, err := retryInterfaceByIndex(ifindex) - if err != nil { - tun.errors <- err - return - } + flags := int(*(*uint32)(unsafe.Pointer(&data[8 /* ifm_flags */]))) // Up / Down event - up := (iface.Flags & net.FlagUp) != 0 + up := (flags & syscall.IFF_UP) != 0 if up != statusUp && up { tun.events <- EventUp } @@ -90,11 +72,13 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) { } statusUp = up + mtu := int(*(*uint32)(unsafe.Pointer(&data[24 /* ifm_data.ifi_mtu */]))) + // MTU changes - if iface.MTU != statusMTU { + if mtu != statusMTU { tun.events <- EventMTUUpdate } - statusMTU = iface.MTU + statusMTU = mtu } } From ace3e11ef24195c2670e619d0a743c13af89ecf4 Mon Sep 17 00:00:00 2001 From: Tom Holford Date: Sun, 4 May 2025 18:49:03 +0200 Subject: [PATCH 092/173] global: replaced unused function params with _ Signed-off-by: Jason A. Donenfeld --- conn/errors_default.go | 2 +- conn/features_default.go | 2 +- device/allowedips_test.go | 2 +- device/sticky_default.go | 2 +- device/sticky_linux.go | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conn/errors_default.go b/conn/errors_default.go index d967518..3c9b223 100644 --- a/conn/errors_default.go +++ b/conn/errors_default.go @@ -7,6 +7,6 @@ package conn -func errShouldDisableUDPGSO(err error) bool { +func errShouldDisableUDPGSO(_ error) bool { return false } diff --git a/conn/features_default.go b/conn/features_default.go index cae2bea..9fc5088 100644 --- a/conn/features_default.go +++ b/conn/features_default.go @@ -10,6 +10,6 @@ package conn import "net" -func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { +func supportsUDPOffload(_ *net.UDPConn) (txOffload, rxOffload bool) { return } diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 9ef8a76..0ce45af 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -39,7 +39,7 @@ func TestCommonBits(t *testing.T) { } } -func benchmarkTrie(peerNumber, addressNumber, addressLength int, b *testing.B) { +func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { var trie *trieEntry var peers []*Peer root := parentIndirection{&trie, 2} diff --git a/device/sticky_default.go b/device/sticky_default.go index da776e8..1751927 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -7,6 +7,6 @@ import ( "github.com/amnezia-vpn/amneziawg-go/rwcancel" ) -func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { +func (device *Device) startRouteListener(_ conn.Bind) (*rwcancel.RWCancel, error) { return nil, nil } diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 5ff9dd6..2edb628 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -9,7 +9,7 @@ * * Currently there is no way to achieve this within the net package: * 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 @@ -47,7 +47,7 @@ func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, er 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 { peer *Peer endpoint *conn.Endpoint From 8051f1714771201e1c1daccfb93f0a0847a29b21 Mon Sep 17 00:00:00 2001 From: Tom Holford Date: Sun, 4 May 2025 18:49:49 +0200 Subject: [PATCH 093/173] device: use rand.NewSource instead of rand.Seed Signed-off-by: Jason A. Donenfeld --- device/allowedips_rand_test.go | 10 +++++----- device/allowedips_test.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go index 8dd9b67..b863696 100644 --- a/device/allowedips_rand_test.go +++ b/device/allowedips_rand_test.go @@ -83,7 +83,7 @@ func TestTrieRandom(t *testing.T) { var peers []*Peer var allowedIPs AllowedIPs - rand.Seed(1) + rng := rand.New(rand.NewSource(1)) for n := 0; n < NumberOfPeers; n++ { peers = append(peers, &Peer{}) @@ -91,14 +91,14 @@ func TestTrieRandom(t *testing.T) { for n := 0; n < NumberOfAddresses; n++ { var addr4 [4]byte - rand.Read(addr4[:]) + rng.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[:]) + rng.Read(addr6[:]) cidr = uint8(rand.Intn(128) + 1) index = rand.Intn(NumberOfPeers) allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(addr6), int(cidr)), peers[index]) @@ -109,7 +109,7 @@ func TestTrieRandom(t *testing.T) { for p = 0; ; p++ { for n := 0; n < NumberOfTests; n++ { var addr4 [4]byte - rand.Read(addr4[:]) + rng.Read(addr4[:]) peer1 := slow4.Lookup(addr4[:]) peer2 := allowedIPs.Lookup(addr4[:]) if peer1 != peer2 { @@ -117,7 +117,7 @@ func TestTrieRandom(t *testing.T) { } var addr6 [16]byte - rand.Read(addr6[:]) + rng.Read(addr6[:]) peer1 = slow6.Lookup(addr6[:]) peer2 = allowedIPs.Lookup(addr6[:]) if peer1 != peer2 { diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 0ce45af..7df7da5 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -44,7 +44,7 @@ func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { var peers []*Peer root := parentIndirection{&trie, 2} - rand.Seed(1) + rng := rand.New(rand.NewSource(1)) const AddressLength = 4 @@ -54,15 +54,15 @@ func benchmarkTrie(peerNumber, addressNumber, _ int, b *testing.B) { for n := 0; n < addressNumber; n++ { var addr [AddressLength]byte - rand.Read(addr[:]) - cidr := uint8(rand.Uint32() % (AddressLength * 8)) - index := rand.Int() % peerNumber + rng.Read(addr[:]) + cidr := uint8(rng.Uint32() % (AddressLength * 8)) + index := rng.Int() % peerNumber root.insert(addr[:], cidr, peers[index]) } for n := 0; n < b.N; n++ { var addr [AddressLength]byte - rand.Read(addr[:]) + rng.Read(addr[:]) trie.lookup(addr[:]) } } From 2cad62c40bca27495120f9a5c3c5bff795124621 Mon Sep 17 00:00:00 2001 From: Kurnia D Win Date: Wed, 7 Jun 2023 12:41:02 +0700 Subject: [PATCH 094/173] rwcancel: fix wrong poll event flag on ReadyWrite It should be POLLIN because closeFd is read-only file. Signed-off-by: Kurnia D Win Signed-off-by: Jason A. Donenfeld --- rwcancel/rwcancel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rwcancel/rwcancel.go b/rwcancel/rwcancel.go index 793e764..4372453 100644 --- a/rwcancel/rwcancel.go +++ b/rwcancel/rwcancel.go @@ -64,7 +64,7 @@ func (rw *RWCancel) ReadyRead() bool { func (rw *RWCancel) ReadyWrite() bool { closeFd := int32(rw.closingReader.Fd()) - pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLOUT}} + pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLIN}} var err error for { _, err = unix.Poll(pollFds, -1) From 676809066782e22df9e17013169bb1e90fb653bb Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 15 May 2025 16:54:03 +0200 Subject: [PATCH 095/173] version: bump snapshot Signed-off-by: Jason A. Donenfeld --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index db75bb9..80f2d4b 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const Version = "0.0.20230223" +const Version = "0.0.20250515" From d5359f52f098b5a500ff348a577cff2d1321422c Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Tue, 20 May 2025 23:03:06 +0200 Subject: [PATCH 096/173] device: add support for removing allowedips individually This pairs with the recent change in wireguard-tools. Signed-off-by: Jason A. Donenfeld --- device/allowedips.go | 87 +++++++++++++++++++++++++-------------- device/allowedips_test.go | 57 +++++++++++++++++++++++++ device/uapi.go | 15 ++++++- 3 files changed, 125 insertions(+), 34 deletions(-) diff --git a/device/allowedips.go b/device/allowedips.go index b40c817..d15373c 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -223,6 +223,60 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) } } +func (node *trieEntry) remove() { + node.removeFromPeerEntries() + 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.mutex.Lock() + defer table.mutex.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() +} + func (table *AllowedIPs) RemoveByPeer(peer *Peer) { table.mutex.Lock() defer table.mutex.Unlock() @@ -230,38 +284,7 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) { var next *list.Element for elem := peer.trieEntries.Front(); elem != nil; elem = next { next = elem.Next() - node := elem.Value.(*trieEntry) - - 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() + elem.Value.(*trieEntry).remove() } } diff --git a/device/allowedips_test.go b/device/allowedips_test.go index 7df7da5..a4b08a3 100644 --- a/device/allowedips_test.go +++ b/device/allowedips_test.go @@ -101,6 +101,10 @@ func TestTrieIPv4(t *testing.T) { allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer) } + remove := func(peer *Peer, a, b, c, d byte, cidr uint8) { + allowedIPs.Remove(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 { @@ -176,6 +180,21 @@ func TestTrieIPv4(t *testing.T) { allowedIPs.RemoveByPeer(a) assertNEQ(a, 192, 168, 0, 1) + + insert(a, 1, 0, 0, 0, 32) + insert(a, 192, 0, 0, 0, 24) + assertEQ(a, 1, 0, 0, 0) + assertEQ(a, 192, 0, 0, 1) + remove(a, 192, 0, 0, 0, 32) + assertEQ(a, 192, 0, 0, 1) + remove(nil, 192, 0, 0, 0, 24) + assertEQ(a, 192, 0, 0, 1) + remove(b, 192, 0, 0, 0, 24) + assertEQ(a, 192, 0, 0, 1) + remove(a, 192, 0, 0, 0, 24) + assertNEQ(a, 192, 0, 0, 1) + remove(a, 1, 0, 0, 0, 32) + assertNEQ(a, 1, 0, 0, 0) } /* Test ported from kernel implementation: @@ -211,6 +230,15 @@ func TestTrieIPv6(t *testing.T) { allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer) } + remove := 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.Remove(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)...) @@ -223,6 +251,18 @@ func TestTrieIPv6(t *testing.T) { } } + assertNEQ := 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 NEQ failed") + } + } + insert(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128) insert(c, 0x26075300, 0x60006b00, 0, 0, 64) insert(e, 0, 0, 0, 0, 0) @@ -244,4 +284,21 @@ func TestTrieIPv6(t *testing.T) { assertEQ(h, 0x24046800, 0x40040800, 0, 0) assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010) assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef) + + insert(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + insert(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) + remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 96) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(nil, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(b, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) + assertNEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) + remove(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) + remove(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) + assertNEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) } diff --git a/device/uapi.go b/device/uapi.go index 1b5e357..870bddc 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -497,7 +497,14 @@ func (device *Device) handlePeerLine( device.allowedips.RemoveByPeer(peer.Peer) 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) if err != nil { return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err) @@ -505,7 +512,11 @@ func (device *Device) handlePeerLine( if peer.dummy { 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": if value != "1" { From 99f2e6d66f79dfc087bb6957738149900b44616d Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 22 May 2025 01:33:55 +0200 Subject: [PATCH 097/173] conn: don't enable GRO on Linux < 5.12 Kernels below 5.12 are missing this: commit 98184612aca0a9ee42b8eb0262a49900ee9eef0d Author: Norman Maurer 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 Reviewed-by: David Ahern Signed-off-by: David S. Miller 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 --- conn/controlfns_linux.go | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index 7bd3917..f0deefa 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -13,6 +13,35 @@ import ( "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() { controlFns = append(controlFns, @@ -57,5 +86,24 @@ func init() { } return err }, + + // Attempt to enable UDP_GRO + 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) { + _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) + }) + return nil + }, ) } From eeb8aae13eedffd851789ee56f1c80d7cb4382e3 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 22 May 2025 01:45:02 +0200 Subject: [PATCH 098/173] version: bump snapshot Signed-off-by: Jason A. Donenfeld --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 80f2d4b..d5524e8 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const Version = "0.0.20250515" +const Version = "0.0.20250522" From 169ed49a469bf5b05775bc1102174fd00e20def7 Mon Sep 17 00:00:00 2001 From: jmwample Date: Mon, 23 Jun 2025 14:37:49 -0600 Subject: [PATCH 099/173] fix formatting discrepancy --- device/device.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/device/device.go b/device/device.go index 2a37321..124b74e 100644 --- a/device/device.go +++ b/device/device.go @@ -92,9 +92,9 @@ type Device struct { closed chan struct{} log *Logger - isASecOn abool.AtomicBool - aSecMux sync.RWMutex - aSecCfg aSecCfgType + isASecOn abool.AtomicBool + aSecMux sync.RWMutex + aSecCfg aSecCfgType junkCreator junkCreator } From 24483d7a00033707e47a0322de6970ddc504e454 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 1 Jul 2025 14:44:37 -0700 Subject: [PATCH 100/173] conn,device: always perform PeerAwareEndpoint check It was previously suppressed if roaming was disabled for the peer. Tailscale always disables roaming as we explicitly configure conn.Endpoint's for all peers. This commit also modifies PeerAwareEndpoint usage such that wireguard-go never uses/sets it as a Peer Endpoint value. In theory we (Tailscale) always disable roaming, so we should always return early from SetEndpointFromPacket(), but this acts as an extra footgun guard and improves clarity around intended usage. Updates tailscale/corp#27502 Updates tailscale/corp#29422 Updates tailscale/corp#30042 Signed-off-by: Jordan Whited --- conn/conn.go | 12 ++++++++---- device/peer.go | 7 ++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/conn/conn.go b/conn/conn.go index 5083648..2a04e6d 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -87,17 +87,21 @@ type Endpoint interface { } // PeerAwareEndpoint is an optional Endpoint specialization for -// integrations that want to know about the outcome of cryptorouting +// 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. // -// If GetPeerEndpoint returns nil, WireGuard will be unable to respond -// to the peer until a new endpoint is written by a later packet. +// wireguard-go never installs a [PeerAwareEndpoint] as the [Endpoint] for a +// [Peer]. type PeerAwareEndpoint interface { - GetPeerEndpoint(peerPublicKey [32]byte) Endpoint + // 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 ( diff --git a/device/peer.go b/device/peer.go index f79a0af..c188c31 100644 --- a/device/peer.go +++ b/device/peer.go @@ -282,13 +282,14 @@ func (peer *Peer) Stop() { func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) { peer.endpoint.Lock() defer peer.endpoint.Unlock() + if ep, ok := endpoint.(conn.PeerAwareEndpoint); ok { + ep.FromPeer(peer.handshake.remoteStatic) + return + } if peer.endpoint.disableRoaming { return } peer.endpoint.clearSrcOnTx = false - if ep, ok := endpoint.(conn.PeerAwareEndpoint); ok { - endpoint = ep.GetPeerEndpoint(peer.handshake.remoteStatic) - } peer.endpoint.val = endpoint } From c20789848019fb494dbe9d280eb246f29b95ab85 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Mon, 7 Jul 2025 05:34:51 -0700 Subject: [PATCH 101/173] AmneziaWG v1.5 (#84) --- Dockerfile | 20 +- device/awg/awg.go | 144 ++++++ device/awg/internal/mock.go | 37 ++ device/{ => awg}/junk_creator.go | 35 +- device/{ => awg}/junk_creator_test.go | 59 ++- device/awg/special_handshake_handler.go | 73 ++++ device/awg/tag_generator.go | 190 ++++++++ device/awg/tag_generator_test.go | 189 ++++++++ device/awg/tag_junk_packet_generator.go | 59 +++ device/awg/tag_junk_packet_generator_test.go | 210 +++++++++ device/awg/tag_junk_packet_generators.go | 66 +++ device/awg/tag_junk_packet_generators_test.go | 149 +++++++ device/awg/tag_parser.go | 112 +++++ device/awg/tag_parser_test.go | 77 ++++ device/device.go | 409 ++++++++++-------- device/device_test.go | 167 +++---- device/noise-protocol.go | 42 +- device/peer.go | 11 + device/receive.go | 32 +- device/send.go | 88 ++-- device/uapi.go | 209 ++++++--- go.mod | 16 +- go.sum | 40 +- 23 files changed, 1982 insertions(+), 452 deletions(-) create mode 100644 device/awg/awg.go create mode 100644 device/awg/internal/mock.go rename device/{ => awg}/junk_creator.go (52%) rename device/{ => awg}/junk_creator_test.go (61%) create mode 100644 device/awg/special_handshake_handler.go create mode 100644 device/awg/tag_generator.go create mode 100644 device/awg/tag_generator_test.go create mode 100644 device/awg/tag_junk_packet_generator.go create mode 100644 device/awg/tag_junk_packet_generator_test.go create mode 100644 device/awg/tag_junk_packet_generators.go create mode 100644 device/awg/tag_junk_packet_generators_test.go create mode 100644 device/awg/tag_parser.go create mode 100644 device/awg/tag_parser_test.go diff --git a/Dockerfile b/Dockerfile index 12159be..6d60440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24 as awg +FROM golang:1.24.4 as awg COPY . /awg WORKDIR /awg RUN go mod download && \ @@ -7,10 +7,24 @@ RUN go mod download && \ FROM alpine:3.19 ARG AWGTOOLS_RELEASE="1.0.20241018" + +RUN apk add linux-headers build-base +COPY awg-tools /awg-tools +RUN pwd && ls -la / && ls -la /awg-tools +WORKDIR /awg-tools/src +# RUN ls -la && pwd && ls awg-tools +RUN make +RUN mkdir -p build && \ + cp wg ./build/awg && \ + cp wg-quick/linux.bash ./build/awg-quick + +RUN cp build/awg /usr/bin/awg +RUN cp build/awg-quick /usr/bin/awg-quick + RUN apk --no-cache add iproute2 iptables bash && \ cd /usr/bin/ && \ - wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ - unzip -j alpine-3.19-amneziawg-tools.zip && \ + # wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ + # unzip -j alpine-3.19-amneziawg-tools.zip && \ chmod +x /usr/bin/awg /usr/bin/awg-quick && \ ln -s /usr/bin/awg /usr/bin/wg && \ ln -s /usr/bin/awg-quick /usr/bin/wg-quick diff --git a/device/awg/awg.go b/device/awg/awg.go new file mode 100644 index 0000000..fd5a96d --- /dev/null +++ b/device/awg/awg.go @@ -0,0 +1,144 @@ +package awg + +import ( + "bytes" + "fmt" + "slices" + "strconv" + "strings" + "sync" + + "github.com/tevino/abool" +) + +type aSecCfgType struct { + IsSet bool + JunkPacketCount int + JunkPacketMinSize int + JunkPacketMaxSize int + InitHeaderJunkSize int + ResponseHeaderJunkSize int + CookieReplyHeaderJunkSize int + TransportHeaderJunkSize int + InitPacketMagicHeader uint32 + ResponsePacketMagicHeader uint32 + UnderloadPacketMagicHeader uint32 + TransportPacketMagicHeader uint32 + // InitPacketMagicHeader Limit + // ResponsePacketMagicHeader Limit + // UnderloadPacketMagicHeader Limit + // TransportPacketMagicHeader Limit +} + +type Limit struct { + Min uint32 + Max uint32 + HeaderType uint32 +} + +func NewLimit(min, max, headerType uint32) (Limit, error) { + if min > max { + return Limit{}, fmt.Errorf("min (%d) cannot be greater than max (%d)", min, max) + } + + return Limit{ + Min: min, + Max: max, + HeaderType: headerType, + }, nil +} + +func ParseMagicHeader(key, value string, defaultHeaderType uint32) (Limit, error) { + // tempAwg.ASecCfg.InitPacketMagicHeader, err = awg.NewLimit(uint32(initPacketMagicHeaderMin), uint32(initPacketMagicHeaderMax), DNewLimit(min, max, headerType)efaultMessageInitiationType) + // var min, max, headerType uint32 + // _, err := fmt.Sscanf(value, "%d-%d:%d", &min, &max, &headerType) + // if err != nil { + // return Limit{}, fmt.Errorf("invalid magic header format: %s", value) + // } + + limits := strings.Split(value, "-") + if len(limits) != 2 { + return Limit{}, fmt.Errorf("invalid format for key: %s; %s", key, value) + } + + min, err := strconv.ParseUint(limits[0], 10, 32) + if err != nil { + return Limit{}, fmt.Errorf("parse min key: %s; value: ; %w", key, limits[0], err) + } + + max, err := strconv.ParseUint(limits[1], 10, 32) + if err != nil { + return Limit{}, fmt.Errorf("parse max key: %s; value: ; %w", key, limits[0], err) + } + + limit, err := NewLimit(uint32(min), uint32(max), defaultHeaderType) + if err != nil { + return Limit{}, fmt.Errorf("new lmit key: %s; value: ; %w", key, limits[0], err) + } + + return limit, nil +} + +type Limits []Limit + +func NewLimits(limits []Limit) Limits { + slices.SortFunc(limits, func(a, b Limit) int { + if a.Min < b.Min { + return -1 + } else if a.Min > b.Min { + return 1 + } + return 0 + }) + + return Limits(limits) +} + +type Protocol struct { + IsASecOn abool.AtomicBool + // TODO: revision the need of the mutex + ASecMux sync.RWMutex + ASecCfg aSecCfgType + JunkCreator junkCreator + + HandshakeHandler SpecialHandshakeHandler +} + +func (protocol *Protocol) CreateInitHeaderJunk() ([]byte, error) { + return protocol.createHeaderJunk(protocol.ASecCfg.InitHeaderJunkSize) +} + +func (protocol *Protocol) CreateResponseHeaderJunk() ([]byte, error) { + return protocol.createHeaderJunk(protocol.ASecCfg.ResponseHeaderJunkSize) +} + +func (protocol *Protocol) CreateCookieReplyHeaderJunk() ([]byte, error) { + return protocol.createHeaderJunk(protocol.ASecCfg.CookieReplyHeaderJunkSize) +} + +func (protocol *Protocol) CreateTransportHeaderJunk(packetSize int) ([]byte, error) { + return protocol.createHeaderJunk(protocol.ASecCfg.TransportHeaderJunkSize, packetSize) +} + +func (protocol *Protocol) createHeaderJunk(junkSize int, optExtraSize ...int) ([]byte, error) { + extraSize := 0 + if len(optExtraSize) == 1 { + extraSize = optExtraSize[0] + } + + var junk []byte + protocol.ASecMux.RLock() + if junkSize != 0 { + buf := make([]byte, 0, junkSize+extraSize) + writer := bytes.NewBuffer(buf[:0]) + err := protocol.JunkCreator.AppendJunk(writer, junkSize) + if err != nil { + protocol.ASecMux.RUnlock() + return nil, err + } + junk = writer.Bytes() + } + protocol.ASecMux.RUnlock() + + return junk, nil +} diff --git a/device/awg/internal/mock.go b/device/awg/internal/mock.go new file mode 100644 index 0000000..a2e1c95 --- /dev/null +++ b/device/awg/internal/mock.go @@ -0,0 +1,37 @@ +package internal + +type mockGenerator struct { + size int +} + +func NewMockGenerator(size int) mockGenerator { + return mockGenerator{size: size} +} + +func (m mockGenerator) Generate() []byte { + return make([]byte, m.size) +} + +func (m mockGenerator) Size() int { + return m.size +} + +func (m mockGenerator) Name() string { + return "mock" +} + +type mockByteGenerator struct { + data []byte +} + +func NewMockByteGenerator(data []byte) mockByteGenerator { + return mockByteGenerator{data: data} +} + +func (bg mockByteGenerator) Generate() []byte { + return bg.data +} + +func (bg mockByteGenerator) Size() int { + return len(bg.data) +} diff --git a/device/junk_creator.go b/device/awg/junk_creator.go similarity index 52% rename from device/junk_creator.go rename to device/awg/junk_creator.go index 3a2d3b4..91fd253 100644 --- a/device/junk_creator.go +++ b/device/awg/junk_creator.go @@ -1,4 +1,4 @@ -package device +package awg import ( "bytes" @@ -8,61 +8,62 @@ import ( ) type junkCreator struct { - device *Device + aSecCfg aSecCfgType cha8Rand *v2.ChaCha8 } -func NewJunkCreator(d *Device) (junkCreator, error) { +// TODO: refactor param to only pass the junk related params +func NewJunkCreator(aSecCfg aSecCfgType) (junkCreator, error) { buf := make([]byte, 32) _, err := crand.Read(buf) if err != nil { return junkCreator{}, err } - return junkCreator{device: d, cha8Rand: v2.NewChaCha8([32]byte(buf))}, nil + return junkCreator{aSecCfg: aSecCfg, cha8Rand: v2.NewChaCha8([32]byte(buf))}, nil } // Should be called with aSecMux RLocked -func (jc *junkCreator) createJunkPackets() ([][]byte, error) { - if jc.device.aSecCfg.junkPacketCount == 0 { - return nil, nil +func (jc *junkCreator) CreateJunkPackets(junks *[][]byte) error { + if jc.aSecCfg.JunkPacketCount == 0 { + return nil } - junks := make([][]byte, 0, jc.device.aSecCfg.junkPacketCount) - for i := 0; i < jc.device.aSecCfg.junkPacketCount; i++ { + for range jc.aSecCfg.JunkPacketCount { packetSize := jc.randomPacketSize() junk, err := jc.randomJunkWithSize(packetSize) if err != nil { - return nil, fmt.Errorf("Failed to create junk packet: %v", err) + return fmt.Errorf("create junk packet: %v", err) } - junks = append(junks, junk) + *junks = append(*junks, junk) } - return junks, nil + return nil } // Should be called with aSecMux RLocked func (jc *junkCreator) randomPacketSize() int { return int( jc.cha8Rand.Uint64()%uint64( - jc.device.aSecCfg.junkPacketMaxSize-jc.device.aSecCfg.junkPacketMinSize, + jc.aSecCfg.JunkPacketMaxSize-jc.aSecCfg.JunkPacketMinSize, ), - ) + jc.device.aSecCfg.junkPacketMinSize + ) + jc.aSecCfg.JunkPacketMinSize } // Should be called with aSecMux RLocked -func (jc *junkCreator) appendJunk(writer *bytes.Buffer, size int) error { +func (jc *junkCreator) AppendJunk(writer *bytes.Buffer, size int) error { headerJunk, err := jc.randomJunkWithSize(size) if err != nil { - return fmt.Errorf("failed to create header junk: %v", err) + return fmt.Errorf("create header junk: %v", err) } _, err = writer.Write(headerJunk) if err != nil { - return fmt.Errorf("failed to write header junk: %v", err) + return fmt.Errorf("write header junk: %v", err) } return nil } // Should be called with aSecMux RLocked func (jc *junkCreator) randomJunkWithSize(size int) ([]byte, error) { + // TODO: use a memory pool to allocate junk := make([]byte, size) _, err := jc.cha8Rand.Read(junk) return junk, err diff --git a/device/junk_creator_test.go b/device/awg/junk_creator_test.go similarity index 61% rename from device/junk_creator_test.go rename to device/awg/junk_creator_test.go index d3cf2b3..424f104 100644 --- a/device/junk_creator_test.go +++ b/device/awg/junk_creator_test.go @@ -1,36 +1,27 @@ -package device +package awg import ( "bytes" "fmt" "testing" - - "github.com/amnezia-vpn/amneziawg-go/conn/bindtest" - "github.com/amnezia-vpn/amneziawg-go/tun/tuntest" ) func setUpJunkCreator(t *testing.T) (junkCreator, error) { - cfg, _ := genASecurityConfigs(t) - tun := tuntest.NewChannelTUN() - binds := bindtest.NewChannelBinds() - level := LogLevelVerbose - dev := NewDevice( - tun.TUN(), - binds[0], - NewLogger(level, ""), - ) - - if err := dev.IpcSet(cfg[0]); err != nil { - t.Errorf("failed to configure device %v", err) - dev.Close() - return junkCreator{}, err - } - - jc, err := NewJunkCreator(dev) + jc, err := NewJunkCreator(aSecCfgType{ + IsSet: true, + JunkPacketCount: 5, + JunkPacketMinSize: 500, + JunkPacketMaxSize: 1000, + InitHeaderJunkSize: 30, + ResponseHeaderJunkSize: 40, + InitPacketMagicHeader: 123456, + ResponsePacketMagicHeader: 67543, + UnderloadPacketMagicHeader: 32345, + TransportPacketMagicHeader: 123123, + }) if err != nil { t.Errorf("failed to create junk creator %v", err) - dev.Close() return junkCreator{}, err } @@ -42,8 +33,9 @@ func Test_junkCreator_createJunkPackets(t *testing.T) { if err != nil { return } - t.Run("", func(t *testing.T) { - got, err := jc.createJunkPackets() + t.Run("valid", func(t *testing.T) { + got := make([][]byte, 0, jc.aSecCfg.JunkPacketCount) + err := jc.CreateJunkPackets(&got) if err != nil { t.Errorf( "junkCreator.createJunkPackets() = %v; failed", @@ -68,7 +60,7 @@ func Test_junkCreator_createJunkPackets(t *testing.T) { } func Test_junkCreator_randomJunkWithSize(t *testing.T) { - t.Run("", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { jc, err := setUpJunkCreator(t) if err != nil { return @@ -78,7 +70,6 @@ func Test_junkCreator_randomJunkWithSize(t *testing.T) { fmt.Printf("%v\n%v\n", r1, r2) if bytes.Equal(r1, r2) { t.Errorf("same junks %v", err) - jc.device.Close() return } }) @@ -90,14 +81,14 @@ func Test_junkCreator_randomPacketSize(t *testing.T) { return } for range [30]struct{}{} { - t.Run("", func(t *testing.T) { - if got := jc.randomPacketSize(); jc.device.aSecCfg.junkPacketMinSize > got || - got > jc.device.aSecCfg.junkPacketMaxSize { + t.Run("valid", func(t *testing.T) { + if got := jc.randomPacketSize(); jc.aSecCfg.JunkPacketMinSize > got || + got > jc.aSecCfg.JunkPacketMaxSize { t.Errorf( "junkCreator.randomPacketSize() = %v, not between range [%v,%v]", got, - jc.device.aSecCfg.junkPacketMinSize, - jc.device.aSecCfg.junkPacketMaxSize, + jc.aSecCfg.JunkPacketMinSize, + jc.aSecCfg.JunkPacketMaxSize, ) } }) @@ -109,13 +100,13 @@ func Test_junkCreator_appendJunk(t *testing.T) { if err != nil { return } - t.Run("", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { s := "apple" buffer := bytes.NewBuffer([]byte(s)) - err := jc.appendJunk(buffer, 30) + err := jc.AppendJunk(buffer, 30) if err != nil && buffer.Len() != len(s)+30 { - t.Errorf("appendWithJunk() size don't match") + t.Error("appendWithJunk() size don't match") } read := make([]byte, 50) buffer.Read(read) diff --git a/device/awg/special_handshake_handler.go b/device/awg/special_handshake_handler.go new file mode 100644 index 0000000..e582d97 --- /dev/null +++ b/device/awg/special_handshake_handler.go @@ -0,0 +1,73 @@ +package awg + +import ( + "errors" + "time" + + "github.com/tevino/abool" + "go.uber.org/atomic" +) + +// TODO: atomic?/ and better way to use this +var PacketCounter *atomic.Uint64 = atomic.NewUint64(0) + +// TODO +var WaitResponse = struct { + Channel chan struct{} + ShouldWait *abool.AtomicBool +}{ + make(chan struct{}, 1), + abool.New(), +} + +type SpecialHandshakeHandler struct { + isFirstDone bool + SpecialJunk TagJunkPacketGenerators + ControlledJunk TagJunkPacketGenerators + + nextItime time.Time + ITimeout time.Duration // seconds + + IsSet bool +} + +func (handler *SpecialHandshakeHandler) Validate() error { + var errs []error + if err := handler.SpecialJunk.Validate(); err != nil { + errs = append(errs, err) + } + if err := handler.ControlledJunk.Validate(); err != nil { + errs = append(errs, err) + } + return errors.Join(errs...) +} + +func (handler *SpecialHandshakeHandler) GenerateSpecialJunk() [][]byte { + if !handler.SpecialJunk.IsDefined() { + return nil + } + + // TODO: create tests + if !handler.isFirstDone { + handler.isFirstDone = true + } else if !handler.isTimeToSendSpecial() { + return nil + } + + rv := handler.SpecialJunk.GeneratePackets() + handler.nextItime = time.Now().Add(handler.ITimeout) + + return rv +} + +func (handler *SpecialHandshakeHandler) isTimeToSendSpecial() bool { + return time.Now().After(handler.nextItime) +} + +func (handler *SpecialHandshakeHandler) GenerateControlledJunk() [][]byte { + if !handler.ControlledJunk.IsDefined() { + return nil + } + + return handler.ControlledJunk.GeneratePackets() +} diff --git a/device/awg/tag_generator.go b/device/awg/tag_generator.go new file mode 100644 index 0000000..65d8004 --- /dev/null +++ b/device/awg/tag_generator.go @@ -0,0 +1,190 @@ +package awg + +import ( + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "strconv" + "strings" + "time" + + v2 "math/rand/v2" + // "go.uber.org/atomic" +) + +type Generator interface { + Generate() []byte + Size() int +} + +type newGenerator func(string) (Generator, error) + +type BytesGenerator struct { + value []byte + size int +} + +func (bg *BytesGenerator) Generate() []byte { + return bg.value +} + +func (bg *BytesGenerator) Size() int { + return bg.size +} + +func newBytesGenerator(param string) (Generator, error) { + hasPrefix := strings.HasPrefix(param, "0x") || strings.HasPrefix(param, "0X") + if !hasPrefix { + return nil, fmt.Errorf("not correct hex: %s", param) + } + + hex, err := hexToBytes(param) + if err != nil { + return nil, fmt.Errorf("hexToBytes: %w", err) + } + + return &BytesGenerator{value: hex, size: len(hex)}, nil +} + +func hexToBytes(hexStr string) ([]byte, error) { + hexStr = strings.TrimPrefix(hexStr, "0x") + hexStr = strings.TrimPrefix(hexStr, "0X") + + // Ensure even length (pad with leading zero if needed) + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + + return hex.DecodeString(hexStr) +} + +type RandomPacketGenerator struct { + cha8Rand *v2.ChaCha8 + size int +} + +func (rpg *RandomPacketGenerator) Generate() []byte { + junk := make([]byte, rpg.size) + rpg.cha8Rand.Read(junk) + return junk +} + +func (rpg *RandomPacketGenerator) Size() int { + return rpg.size +} + +func newRandomPacketGenerator(param string) (Generator, error) { + size, err := strconv.Atoi(param) + if err != nil { + return nil, fmt.Errorf("random packet parse int: %w", err) + } + + if size > 1000 { + return nil, fmt.Errorf("random packet size must be less than 1000") + } + + buf := make([]byte, 32) + _, err = crand.Read(buf) + if err != nil { + return nil, fmt.Errorf("random packet crand read: %w", err) + } + + return &RandomPacketGenerator{ + cha8Rand: v2.NewChaCha8([32]byte(buf)), + size: size, + }, nil +} + +type TimestampGenerator struct { +} + +func (tg *TimestampGenerator) Generate() []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(time.Now().Unix())) + return buf +} + +func (tg *TimestampGenerator) Size() int { + return 8 +} + +func newTimestampGenerator(param string) (Generator, error) { + if len(param) != 0 { + return nil, fmt.Errorf("timestamp param needs to be empty: %s", param) + } + + return &TimestampGenerator{}, nil +} + +type WaitTimeoutGenerator struct { + waitTimeout time.Duration +} + +func (wtg *WaitTimeoutGenerator) Generate() []byte { + time.Sleep(wtg.waitTimeout) + return []byte{} +} + +func (wtg *WaitTimeoutGenerator) Size() int { + return 0 +} + +func newWaitTimeoutGenerator(param string) (Generator, error) { + timeout, err := strconv.Atoi(param) + if err != nil { + return nil, fmt.Errorf("timeout parse int: %w", err) + } + + if timeout > 5000 { + return nil, fmt.Errorf("timeout must be less than 5000ms") + } + + return &WaitTimeoutGenerator{ + waitTimeout: time.Duration(timeout) * time.Millisecond, + }, nil +} + +type PacketCounterGenerator struct { +} + +func (c *PacketCounterGenerator) Generate() []byte { + buf := make([]byte, 8) + // TODO: better way to handle counter tag + binary.BigEndian.PutUint64(buf, PacketCounter.Load()) + return buf +} + +func (c *PacketCounterGenerator) Size() int { + return 8 +} + +func newPacketCounterGenerator(param string) (Generator, error) { + if len(param) != 0 { + return nil, fmt.Errorf("packet counter param needs to be empty: %s", param) + } + + return &PacketCounterGenerator{}, nil +} + +type WaitResponseGenerator struct { +} + +func (c *WaitResponseGenerator) Generate() []byte { + WaitResponse.ShouldWait.Set() + <-WaitResponse.Channel + WaitResponse.ShouldWait.UnSet() + return []byte{} +} + +func (c *WaitResponseGenerator) Size() int { + return 0 +} + +func newWaitResponseGenerator(param string) (Generator, error) { + if len(param) != 0 { + return nil, fmt.Errorf("wait response param needs to be empty: %s", param) + } + + return &WaitResponseGenerator{}, nil +} diff --git a/device/awg/tag_generator_test.go b/device/awg/tag_generator_test.go new file mode 100644 index 0000000..4950b33 --- /dev/null +++ b/device/awg/tag_generator_test.go @@ -0,0 +1,189 @@ +package awg + +import ( + "encoding/binary" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_newBytesGenerator(t *testing.T) { + type args struct { + param string + } + tests := []struct { + name string + args args + want []byte + wantErr error + }{ + { + name: "empty", + args: args{ + param: "", + }, + wantErr: fmt.Errorf("not correct hex"), + }, + { + name: "wrong start", + args: args{ + param: "123456", + }, + wantErr: fmt.Errorf("not correct hex"), + }, + { + name: "not only hex value with X", + args: args{ + param: "0X12345q", + }, + wantErr: fmt.Errorf("not correct hex"), + }, + { + name: "not only hex value with x", + args: args{ + param: "0x12345q", + }, + wantErr: fmt.Errorf("not correct hex"), + }, + { + name: "valid hex", + args: args{ + param: "0xf6ab3267fa", + }, + want: []byte{0xf6, 0xab, 0x32, 0x67, 0xfa}, + }, + { + name: "valid hex with odd length", + args: args{ + param: "0xfab3267fa", + }, + want: []byte{0xf, 0xab, 0x32, 0x67, 0xfa}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newBytesGenerator(tt.args.param) + + if tt.wantErr != nil { + require.ErrorAs(t, err, &tt.wantErr) + require.Nil(t, got) + return + } + + require.Nil(t, err) + require.NotNil(t, got) + + gotValues := got.Generate() + require.Equal(t, tt.want, gotValues) + }) + } +} + +func Test_newRandomPacketGenerator(t *testing.T) { + type args struct { + param string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "empty", + args: args{ + param: "", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "not an int", + args: args{ + param: "x", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "too large", + args: args{ + param: "1001", + }, + wantErr: fmt.Errorf("random packet size must be less than 1000"), + }, + { + name: "valid", + args: args{ + param: "12", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newRandomPacketGenerator(tt.args.param) + if tt.wantErr != nil { + require.ErrorAs(t, err, &tt.wantErr) + require.Nil(t, got) + return + } + + require.Nil(t, err) + require.NotNil(t, got) + first := got.Generate() + + second := got.Generate() + require.NotEqual(t, first, second) + }) + } +} + +func TestPacketCounterGenerator(t *testing.T) { + tests := []struct { + name string + param string + wantErr bool + }{ + { + name: "Valid empty param", + param: "", + wantErr: false, + }, + { + name: "Invalid non-empty param", + param: "anything", + wantErr: true, + }, + } + + for _, tc := range tests { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + gen, err := newPacketCounterGenerator(tc.param) + if tc.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, 8, gen.Size()) + + // Reset counter to known value for test + initialCount := uint64(42) + PacketCounter.Store(initialCount) + + output := gen.Generate() + require.Equal(t, 8, len(output)) + + // Verify counter value in output + counterValue := binary.BigEndian.Uint64(output) + require.Equal(t, initialCount, counterValue) + + // Increment counter and verify change + PacketCounter.Add(1) + output = gen.Generate() + counterValue = binary.BigEndian.Uint64(output) + require.Equal(t, initialCount+1, counterValue) + }) + } +} diff --git a/device/awg/tag_junk_packet_generator.go b/device/awg/tag_junk_packet_generator.go new file mode 100644 index 0000000..fdbebc8 --- /dev/null +++ b/device/awg/tag_junk_packet_generator.go @@ -0,0 +1,59 @@ +package awg + +import ( + "fmt" + "strconv" +) + +type TagJunkPacketGenerator struct { + name string + tagValue string + + packetSize int + generators []Generator +} + +func newTagJunkPacketGenerator(name, tagValue string, size int) TagJunkPacketGenerator { + return TagJunkPacketGenerator{ + name: name, + tagValue: tagValue, + generators: make([]Generator, 0, size), + } +} + +func (tg *TagJunkPacketGenerator) append(generator Generator) { + tg.generators = append(tg.generators, generator) + tg.packetSize += generator.Size() +} + +func (tg *TagJunkPacketGenerator) generatePacket() []byte { + packet := make([]byte, 0, tg.packetSize) + for _, generator := range tg.generators { + packet = append(packet, generator.Generate()...) + } + + return packet +} + +func (tg *TagJunkPacketGenerator) Name() string { + return tg.name +} + +func (tg *TagJunkPacketGenerator) nameIndex() (int, error) { + if len(tg.name) != 2 { + return 0, fmt.Errorf("name must be 2 character long: %s", tg.name) + } + + index, err := strconv.Atoi(tg.name[1:2]) + if err != nil { + return 0, fmt.Errorf("name 2 char should be an int %w", err) + } + return index, nil +} + +func (tg *TagJunkPacketGenerator) IpcGetFields() IpcFields { + return IpcFields{ + Key: tg.name, + Value: tg.tagValue, + } +} diff --git a/device/awg/tag_junk_packet_generator_test.go b/device/awg/tag_junk_packet_generator_test.go new file mode 100644 index 0000000..309d425 --- /dev/null +++ b/device/awg/tag_junk_packet_generator_test.go @@ -0,0 +1,210 @@ +package awg + +import ( + "testing" + + "github.com/amnezia-vpn/amneziawg-go/device/awg/internal" + "github.com/stretchr/testify/require" +) + +func TestNewTagJunkGenerator(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + genName string + size int + expected TagJunkPacketGenerator + }{ + { + name: "Create new generator with empty name", + genName: "", + size: 0, + expected: TagJunkPacketGenerator{ + name: "", + packetSize: 0, + generators: make([]Generator, 0), + }, + }, + { + name: "Create new generator with valid name", + genName: "T1", + size: 0, + expected: TagJunkPacketGenerator{ + name: "T1", + packetSize: 0, + generators: make([]Generator, 0), + }, + }, + { + name: "Create new generator with non-zero size", + genName: "T2", + size: 5, + expected: TagJunkPacketGenerator{ + name: "T2", + packetSize: 0, + generators: make([]Generator, 5), + }, + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := newTagJunkPacketGenerator(tc.genName, "", tc.size) + require.Equal(t, tc.expected.name, result.name) + require.Equal(t, tc.expected.packetSize, result.packetSize) + require.Equal(t, cap(result.generators), len(tc.expected.generators)) + }) + } +} + +func TestTagJunkGeneratorAppend(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + initialState TagJunkPacketGenerator + mockSize int + expectedLength int + expectedSize int + }{ + { + name: "Append to empty generator", + initialState: newTagJunkPacketGenerator("T1", "", 0), + mockSize: 5, + expectedLength: 1, + expectedSize: 5, + }, + { + name: "Append to non-empty generator", + initialState: TagJunkPacketGenerator{ + name: "T2", + packetSize: 10, + generators: make([]Generator, 2), + }, + mockSize: 7, + expectedLength: 3, // 2 existing + 1 new + expectedSize: 17, // 10 + 7 + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + tg := tc.initialState + mockGen := internal.NewMockGenerator(tc.mockSize) + + tg.append(mockGen) + + require.Equal(t, tc.expectedLength, len(tg.generators)) + require.Equal(t, tc.expectedSize, tg.packetSize) + }) + } +} + +func TestTagJunkGeneratorGenerate(t *testing.T) { + t.Parallel() + + // Create mock generators for testing + mockGen1 := internal.NewMockByteGenerator([]byte{0x01, 0x02}) + mockGen2 := internal.NewMockByteGenerator([]byte{0x03, 0x04, 0x05}) + + testCases := []struct { + name string + setupGenerator func() TagJunkPacketGenerator + expected []byte + }{ + { + name: "Generate with empty generators", + setupGenerator: func() TagJunkPacketGenerator { + return newTagJunkPacketGenerator("T1", "", 0) + }, + expected: []byte{}, + }, + { + name: "Generate with single generator", + setupGenerator: func() TagJunkPacketGenerator { + tg := newTagJunkPacketGenerator("T2", "", 0) + tg.append(mockGen1) + return tg + }, + expected: []byte{0x01, 0x02}, + }, + { + name: "Generate with multiple generators", + setupGenerator: func() TagJunkPacketGenerator { + tg := newTagJunkPacketGenerator("T3", "", 0) + tg.append(mockGen1) + tg.append(mockGen2) + return tg + }, + expected: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + tg := tc.setupGenerator() + result := tg.generatePacket() + + require.Equal(t, tc.expected, result) + }) + } +} + +func TestTagJunkGeneratorNameIndex(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + generatorName string + expectedIndex int + expectError bool + }{ + { + name: "Valid name with digit", + generatorName: "T5", + expectedIndex: 5, + expectError: false, + }, + { + name: "Invalid name - too short", + generatorName: "T", + expectError: true, + }, + { + name: "Invalid name - too long", + generatorName: "T55", + expectError: true, + }, + { + name: "Invalid name - non-digit second character", + generatorName: "TX", + expectError: true, + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + tg := TagJunkPacketGenerator{name: tc.generatorName} + index, err := tg.nameIndex() + + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedIndex, index) + } + }) + } +} diff --git a/device/awg/tag_junk_packet_generators.go b/device/awg/tag_junk_packet_generators.go new file mode 100644 index 0000000..9921eb0 --- /dev/null +++ b/device/awg/tag_junk_packet_generators.go @@ -0,0 +1,66 @@ +package awg + +import "fmt" + +type TagJunkPacketGenerators struct { + tagGenerators []TagJunkPacketGenerator + length int + DefaultJunkCount int // Jc +} + +func (generators *TagJunkPacketGenerators) AppendGenerator( + generator TagJunkPacketGenerator, +) { + generators.tagGenerators = append(generators.tagGenerators, generator) + generators.length++ +} + +func (generators *TagJunkPacketGenerators) IsDefined() bool { + return len(generators.tagGenerators) > 0 +} + +// validate that packets were defined consecutively +func (generators *TagJunkPacketGenerators) Validate() error { + seen := make([]bool, len(generators.tagGenerators)) + for _, generator := range generators.tagGenerators { + index, err := generator.nameIndex() + if index > len(generators.tagGenerators) { + return fmt.Errorf("junk packet index should be consecutive") + } + if err != nil { + return fmt.Errorf("name index: %w", err) + } else { + seen[index-1] = true + } + } + + for _, found := range seen { + if !found { + return fmt.Errorf("junk packet index should be consecutive") + } + } + + return nil +} + +func (generators *TagJunkPacketGenerators) GeneratePackets() [][]byte { + var rv = make([][]byte, 0, generators.length+generators.DefaultJunkCount) + + for i, tagGenerator := range generators.tagGenerators { + rv = append(rv, make([]byte, tagGenerator.packetSize)) + copy(rv[i], tagGenerator.generatePacket()) + PacketCounter.Inc() + } + PacketCounter.Add(uint64(generators.DefaultJunkCount)) + + return rv +} + +func (tg *TagJunkPacketGenerators) IpcGetFields() []IpcFields { + rv := make([]IpcFields, 0, len(tg.tagGenerators)) + for _, generator := range tg.tagGenerators { + rv = append(rv, generator.IpcGetFields()) + } + + return rv +} diff --git a/device/awg/tag_junk_packet_generators_test.go b/device/awg/tag_junk_packet_generators_test.go new file mode 100644 index 0000000..6b1fd47 --- /dev/null +++ b/device/awg/tag_junk_packet_generators_test.go @@ -0,0 +1,149 @@ +package awg + +import ( + "testing" + + "github.com/amnezia-vpn/amneziawg-go/device/awg/internal" + "github.com/stretchr/testify/require" +) + +func TestTagJunkGeneratorHandlerAppendGenerator(t *testing.T) { + tests := []struct { + name string + generator TagJunkPacketGenerator + }{ + { + name: "append single generator", + generator: newTagJunkPacketGenerator("t1", "", 10), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + generators := &TagJunkPacketGenerators{} + + // Initial length should be 0 + require.Equal(t, 0, generators.length) + require.Empty(t, generators.tagGenerators) + + // After append, length should be 1 and generator should be added + generators.AppendGenerator(tt.generator) + require.Equal(t, 1, generators.length) + require.Len(t, generators.tagGenerators, 1) + require.Equal(t, tt.generator, generators.tagGenerators[0]) + }) + } +} + +func TestTagJunkGeneratorHandlerValidate(t *testing.T) { + tests := []struct { + name string + generators []TagJunkPacketGenerator + wantErr bool + errMsg string + }{ + { + name: "bad start", + generators: []TagJunkPacketGenerator{ + newTagJunkPacketGenerator("t3", "", 10), + newTagJunkPacketGenerator("t4", "", 10), + }, + wantErr: true, + errMsg: "junk packet index should be consecutive", + }, + { + name: "non-consecutive indices", + generators: []TagJunkPacketGenerator{ + newTagJunkPacketGenerator("t1", "", 10), + newTagJunkPacketGenerator("t3", "", 10), // Missing t2 + }, + wantErr: true, + errMsg: "junk packet index should be consecutive", + }, + { + name: "consecutive indices", + generators: []TagJunkPacketGenerator{ + newTagJunkPacketGenerator("t1", "", 10), + newTagJunkPacketGenerator("t2", "", 10), + newTagJunkPacketGenerator("t3", "", 10), + newTagJunkPacketGenerator("t4", "", 10), + newTagJunkPacketGenerator("t5", "", 10), + }, + }, + { + name: "nameIndex error", + generators: []TagJunkPacketGenerator{ + newTagJunkPacketGenerator("error", "", 10), + }, + wantErr: true, + errMsg: "name must be 2 character long", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + generators := &TagJunkPacketGenerators{} + for _, gen := range tt.generators { + generators.AppendGenerator(gen) + } + + err := generators.Validate() + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + }) + } +} + +func TestTagJunkGeneratorHandlerGenerate(t *testing.T) { + mockByte1 := []byte{0x01, 0x02} + mockByte2 := []byte{0x03, 0x04, 0x05} + mockGen1 := internal.NewMockByteGenerator(mockByte1) + mockGen2 := internal.NewMockByteGenerator(mockByte2) + + tests := []struct { + name string + setupGenerator func() []TagJunkPacketGenerator + expected [][]byte + }{ + { + name: "generate with no default junk", + setupGenerator: func() []TagJunkPacketGenerator { + tg1 := newTagJunkPacketGenerator("t1", "", 0) + tg1.append(mockGen1) + tg1.append(mockGen2) + tg2 := newTagJunkPacketGenerator("t2", "", 0) + tg2.append(mockGen2) + tg2.append(mockGen1) + + return []TagJunkPacketGenerator{tg1, tg2} + }, + expected: [][]byte{ + append(mockByte1, mockByte2...), + append(mockByte2, mockByte1...), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + generators := &TagJunkPacketGenerators{} + tagGenerators := tt.setupGenerator() + for _, gen := range tagGenerators { + generators.AppendGenerator(gen) + } + + result := generators.GeneratePackets() + require.Equal(t, result, tt.expected) + }) + } +} diff --git a/device/awg/tag_parser.go b/device/awg/tag_parser.go new file mode 100644 index 0000000..2b09226 --- /dev/null +++ b/device/awg/tag_parser.go @@ -0,0 +1,112 @@ +package awg + +import ( + "fmt" + "maps" + "regexp" + "strings" +) + +type IpcFields struct{ Key, Value string } + +type EnumTag string + +const ( + BytesEnumTag EnumTag = "b" + CounterEnumTag EnumTag = "c" + TimestampEnumTag EnumTag = "t" + RandomBytesEnumTag EnumTag = "r" + WaitTimeoutEnumTag EnumTag = "wt" + WaitResponseEnumTag EnumTag = "wr" +) + +var generatorCreator = map[EnumTag]newGenerator{ + BytesEnumTag: newBytesGenerator, + CounterEnumTag: newPacketCounterGenerator, + TimestampEnumTag: newTimestampGenerator, + RandomBytesEnumTag: newRandomPacketGenerator, + WaitTimeoutEnumTag: newWaitTimeoutGenerator, + // WaitResponseEnumTag: newWaitResponseGenerator, +} + +// helper map to determine enumTags are unique +var uniqueTags = map[EnumTag]bool{ + CounterEnumTag: false, + TimestampEnumTag: false, +} + +type Tag struct { + Name EnumTag + Param string +} + +func parseTag(input string) (Tag, error) { + // Regular expression to match + re := regexp.MustCompile(`([a-zA-Z]+)(?:\s+([^>]+))?>`) + + match := re.FindStringSubmatch(input) + tag := Tag{ + Name: EnumTag(match[1]), + } + if len(match) > 2 && match[2] != "" { + tag.Param = strings.TrimSpace(match[2]) + } + + return tag, nil +} + +func Parse(name, input string) (TagJunkPacketGenerator, error) { + inputSlice := strings.Split(input, "<") + if len(inputSlice) <= 1 { + return TagJunkPacketGenerator{}, fmt.Errorf("empty input: %s", input) + } + + uniqueTagCheck := make(map[EnumTag]bool, len(uniqueTags)) + maps.Copy(uniqueTagCheck, uniqueTags) + + // skip byproduct of split + inputSlice = inputSlice[1:] + rv := newTagJunkPacketGenerator(name, input, len(inputSlice)) + for _, inputParam := range inputSlice { + if len(inputParam) <= 1 { + return TagJunkPacketGenerator{}, fmt.Errorf( + "empty tag in input: %s", + inputSlice, + ) + } else if strings.Count(inputParam, ">") != 1 { + return TagJunkPacketGenerator{}, fmt.Errorf("ill formated input: %s", input) + } + + tag, _ := parseTag(inputParam) + creator, ok := generatorCreator[tag.Name] + if !ok { + return TagJunkPacketGenerator{}, fmt.Errorf("invalid tag: %s", tag.Name) + } + if present, ok := uniqueTagCheck[tag.Name]; ok { + if present { + return TagJunkPacketGenerator{}, fmt.Errorf( + "tag %s needs to be unique", + tag.Name, + ) + } + uniqueTagCheck[tag.Name] = true + } + generator, err := creator(tag.Param) + if err != nil { + return TagJunkPacketGenerator{}, fmt.Errorf("gen: %w", err) + } + + // TODO: handle counter tag + // if tag.Name == CounterEnumTag { + // packetCounter, ok := generator.(*PacketCounterGenerator) + // if !ok { + // log.Fatalf("packet counter generator expected, got %T", generator) + // } + // PacketCounter = packetCounter.counter + // } + + rv.append(generator) + } + + return rv, nil +} diff --git a/device/awg/tag_parser_test.go b/device/awg/tag_parser_test.go new file mode 100644 index 0000000..8f828ec --- /dev/null +++ b/device/awg/tag_parser_test.go @@ -0,0 +1,77 @@ +package awg + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + type args struct { + name string + input string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "invalid name", + args: args{name: "apple", input: ""}, + wantErr: fmt.Errorf("ill formated input"), + }, + { + name: "empty", + args: args{name: "i1", input: ""}, + wantErr: fmt.Errorf("ill formated input"), + }, + { + name: "extra >", + args: args{name: "i1", input: ">"}, + wantErr: fmt.Errorf("ill formated input"), + }, + { + name: "extra <", + args: args{name: "i1", input: "<"}, + wantErr: fmt.Errorf("empty tag in input"), + }, + { + name: "empty <>", + args: args{name: "i1", input: "<>"}, + wantErr: fmt.Errorf("empty tag in input"), + }, + { + name: "invalid tag", + args: args{name: "i1", input: ""}, + wantErr: fmt.Errorf("invalid tag"), + }, + { + name: "counter uniqueness violation", + args: args{name: "i1", input: ""}, + wantErr: fmt.Errorf("parse tag needs to be unique"), + }, + { + name: "timestamp uniqueness violation", + args: args{name: "i1", input: ""}, + wantErr: fmt.Errorf("parse tag needs to be unique"), + }, + { + name: "valid", + args: args{input: ""}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.args.name, tt.args.input) + + // TODO: ErrorAs doesn't work as you think + if tt.wantErr != nil { + require.ErrorAs(t, err, &tt.wantErr) + return + } + require.Nil(t, err) + }) + } +} diff --git a/device/device.go b/device/device.go index 124b74e..1829352 100644 --- a/device/device.go +++ b/device/device.go @@ -6,19 +6,55 @@ package device import ( + "errors" "runtime" "sync" "sync/atomic" "time" "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device/awg" "github.com/amnezia-vpn/amneziawg-go/ipc" "github.com/amnezia-vpn/amneziawg-go/ratelimiter" "github.com/amnezia-vpn/amneziawg-go/rwcancel" "github.com/amnezia-vpn/amneziawg-go/tun" - "github.com/tevino/abool/v2" ) +type Version uint8 + +const ( + VersionDefault Version = iota + VersionAwg + VersionAwgSpecialHandshake +) + +// TODO: +type AtomicVersion struct { + value atomic.Uint32 +} + +func NewAtomicVersion(v Version) *AtomicVersion { + av := &AtomicVersion{} + av.Store(v) + return av +} + +func (av *AtomicVersion) Load() Version { + return Version(av.value.Load()) +} + +func (av *AtomicVersion) Store(v Version) { + av.value.Store(uint32(v)) +} + +func (av *AtomicVersion) CompareAndSwap(old, new Version) bool { + return av.value.CompareAndSwap(uint32(old), uint32(new)) +} + +func (av *AtomicVersion) Swap(new Version) Version { + return Version(av.value.Swap(uint32(new))) +} + type Device struct { state struct { // state holds the device's state. It is accessed atomically. @@ -92,23 +128,8 @@ type Device struct { closed chan struct{} log *Logger - isASecOn abool.AtomicBool - aSecMux sync.RWMutex - aSecCfg aSecCfgType - junkCreator junkCreator -} - -type aSecCfgType struct { - isSet bool - junkPacketCount int - junkPacketMinSize int - junkPacketMaxSize int - initPacketJunkSize int - responsePacketJunkSize int - initPacketMagicHeader uint32 - responsePacketMagicHeader uint32 - underloadPacketMagicHeader uint32 - transportPacketMagicHeader uint32 + version Version + awg awg.Protocol } // deviceState represents the state of a Device. @@ -557,251 +578,261 @@ func (device *Device) BindClose() error { device.net.Unlock() return err } -func (device *Device) isAdvancedSecurityOn() bool { - return device.isASecOn.IsSet() +func (device *Device) isAWG() bool { + return device.version >= VersionAwg } func (device *Device) resetProtocol() { // restore default message type values - MessageInitiationType = 1 - MessageResponseType = 2 - MessageCookieReplyType = 3 - MessageTransportType = 4 + MessageInitiationType = DefaultMessageInitiationType + MessageResponseType = DefaultMessageResponseType + MessageCookieReplyType = DefaultMessageCookieReplyType + MessageTransportType = DefaultMessageTransportType } -func (device *Device) handlePostConfig(tempASecCfg *aSecCfgType) (err error) { - - if !tempASecCfg.isSet { - return err +func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { + if !tempAwg.ASecCfg.IsSet && !tempAwg.HandshakeHandler.IsSet { + return nil } + var errs []error + isASecOn := false - device.aSecMux.Lock() - if tempASecCfg.junkPacketCount < 0 { - err = ipcErrorf( + device.awg.ASecMux.Lock() + if tempAwg.ASecCfg.JunkPacketCount < 0 { + errs = append(errs, ipcErrorf( ipc.IpcErrorInvalid, "JunkPacketCount should be non negative", + ), ) } - device.aSecCfg.junkPacketCount = tempASecCfg.junkPacketCount - if tempASecCfg.junkPacketCount != 0 { + device.awg.ASecCfg.JunkPacketCount = tempAwg.ASecCfg.JunkPacketCount + if tempAwg.ASecCfg.JunkPacketCount != 0 { isASecOn = true } - device.aSecCfg.junkPacketMinSize = tempASecCfg.junkPacketMinSize - if tempASecCfg.junkPacketMinSize != 0 { + device.awg.ASecCfg.JunkPacketMinSize = tempAwg.ASecCfg.JunkPacketMinSize + if tempAwg.ASecCfg.JunkPacketMinSize != 0 { isASecOn = true } - if device.aSecCfg.junkPacketCount > 0 && - tempASecCfg.junkPacketMaxSize == tempASecCfg.junkPacketMinSize { + if device.awg.ASecCfg.JunkPacketCount > 0 && + tempAwg.ASecCfg.JunkPacketMaxSize == tempAwg.ASecCfg.JunkPacketMinSize { - tempASecCfg.junkPacketMaxSize++ // to make rand gen work + tempAwg.ASecCfg.JunkPacketMaxSize++ // to make rand gen work } - if tempASecCfg.junkPacketMaxSize >= MaxSegmentSize { - device.aSecCfg.junkPacketMinSize = 0 - device.aSecCfg.junkPacketMaxSize = 1 - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d; %w", - tempASecCfg.junkPacketMaxSize, - MaxSegmentSize, - err, - ) - } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", - tempASecCfg.junkPacketMaxSize, - MaxSegmentSize, - ) - } - } else if tempASecCfg.junkPacketMaxSize < tempASecCfg.junkPacketMinSize { - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - "maxSize: %d; should be greater than minSize: %d; %w", - tempASecCfg.junkPacketMaxSize, - tempASecCfg.junkPacketMinSize, - err, - ) - } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - "maxSize: %d; should be greater than minSize: %d", - tempASecCfg.junkPacketMaxSize, - tempASecCfg.junkPacketMinSize, - ) - } + if tempAwg.ASecCfg.JunkPacketMaxSize >= MaxSegmentSize { + device.awg.ASecCfg.JunkPacketMinSize = 0 + device.awg.ASecCfg.JunkPacketMaxSize = 1 + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", + tempAwg.ASecCfg.JunkPacketMaxSize, + MaxSegmentSize, + )) + } else if tempAwg.ASecCfg.JunkPacketMaxSize < tempAwg.ASecCfg.JunkPacketMinSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + "maxSize: %d; should be greater than minSize: %d", + tempAwg.ASecCfg.JunkPacketMaxSize, + tempAwg.ASecCfg.JunkPacketMinSize, + )) } else { - device.aSecCfg.junkPacketMaxSize = tempASecCfg.junkPacketMaxSize + device.awg.ASecCfg.JunkPacketMaxSize = tempAwg.ASecCfg.JunkPacketMaxSize } - if tempASecCfg.junkPacketMaxSize != 0 { + if tempAwg.ASecCfg.JunkPacketMaxSize != 0 { isASecOn = true } - if MessageInitiationSize+tempASecCfg.initPacketJunkSize >= MaxSegmentSize { - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d; %w`, - tempASecCfg.initPacketJunkSize, - MaxSegmentSize, - err, - ) - } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempASecCfg.initPacketJunkSize, - MaxSegmentSize, - ) - } + newInitSize := MessageInitiationSize + tempAwg.ASecCfg.InitHeaderJunkSize + + if newInitSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.ASecCfg.InitHeaderJunkSize, + MaxSegmentSize, + ), + ) } else { - device.aSecCfg.initPacketJunkSize = tempASecCfg.initPacketJunkSize + device.awg.ASecCfg.InitHeaderJunkSize = tempAwg.ASecCfg.InitHeaderJunkSize } - if tempASecCfg.initPacketJunkSize != 0 { + if tempAwg.ASecCfg.InitHeaderJunkSize != 0 { isASecOn = true } - if MessageResponseSize+tempASecCfg.responsePacketJunkSize >= MaxSegmentSize { - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d; %w`, - tempASecCfg.responsePacketJunkSize, - MaxSegmentSize, - err, - ) - } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempASecCfg.responsePacketJunkSize, - MaxSegmentSize, - ) - } + newResponseSize := MessageResponseSize + tempAwg.ASecCfg.ResponseHeaderJunkSize + + if newResponseSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.ASecCfg.ResponseHeaderJunkSize, + MaxSegmentSize, + ), + ) } else { - device.aSecCfg.responsePacketJunkSize = tempASecCfg.responsePacketJunkSize + device.awg.ASecCfg.ResponseHeaderJunkSize = tempAwg.ASecCfg.ResponseHeaderJunkSize } - if tempASecCfg.responsePacketJunkSize != 0 { + if tempAwg.ASecCfg.ResponseHeaderJunkSize != 0 { isASecOn = true } - if tempASecCfg.initPacketMagicHeader > 4 { + newCookieSize := MessageCookieReplySize + tempAwg.ASecCfg.CookieReplyHeaderJunkSize + + if newCookieSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `cookie reply size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.ASecCfg.CookieReplyHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.ASecCfg.CookieReplyHeaderJunkSize = tempAwg.ASecCfg.CookieReplyHeaderJunkSize + } + + if tempAwg.ASecCfg.CookieReplyHeaderJunkSize != 0 { + isASecOn = true + } + + newTransportSize := MessageTransportSize + tempAwg.ASecCfg.TransportHeaderJunkSize + + if newTransportSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `transport size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.ASecCfg.TransportHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.ASecCfg.TransportHeaderJunkSize = tempAwg.ASecCfg.TransportHeaderJunkSize + } + + if tempAwg.ASecCfg.TransportHeaderJunkSize != 0 { + isASecOn = true + } + + if tempAwg.ASecCfg.InitPacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating init_packet_magic_header") - device.aSecCfg.initPacketMagicHeader = tempASecCfg.initPacketMagicHeader - MessageInitiationType = device.aSecCfg.initPacketMagicHeader + device.awg.ASecCfg.InitPacketMagicHeader = tempAwg.ASecCfg.InitPacketMagicHeader + MessageInitiationType = device.awg.ASecCfg.InitPacketMagicHeader } else { device.log.Verbosef("UAPI: Using default init type") - MessageInitiationType = 1 + MessageInitiationType = DefaultMessageInitiationType } - if tempASecCfg.responsePacketMagicHeader > 4 { + if tempAwg.ASecCfg.ResponsePacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating response_packet_magic_header") - device.aSecCfg.responsePacketMagicHeader = tempASecCfg.responsePacketMagicHeader - MessageResponseType = device.aSecCfg.responsePacketMagicHeader + device.awg.ASecCfg.ResponsePacketMagicHeader = tempAwg.ASecCfg.ResponsePacketMagicHeader + MessageResponseType = device.awg.ASecCfg.ResponsePacketMagicHeader } else { device.log.Verbosef("UAPI: Using default response type") - MessageResponseType = 2 + MessageResponseType = DefaultMessageResponseType } - if tempASecCfg.underloadPacketMagicHeader > 4 { + if tempAwg.ASecCfg.UnderloadPacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating underload_packet_magic_header") - device.aSecCfg.underloadPacketMagicHeader = tempASecCfg.underloadPacketMagicHeader - MessageCookieReplyType = device.aSecCfg.underloadPacketMagicHeader + device.awg.ASecCfg.UnderloadPacketMagicHeader = tempAwg.ASecCfg.UnderloadPacketMagicHeader + MessageCookieReplyType = device.awg.ASecCfg.UnderloadPacketMagicHeader } else { device.log.Verbosef("UAPI: Using default underload type") - MessageCookieReplyType = 3 + MessageCookieReplyType = DefaultMessageCookieReplyType } - if tempASecCfg.transportPacketMagicHeader > 4 { + if tempAwg.ASecCfg.TransportPacketMagicHeader > 4 { isASecOn = true device.log.Verbosef("UAPI: Updating transport_packet_magic_header") - device.aSecCfg.transportPacketMagicHeader = tempASecCfg.transportPacketMagicHeader - MessageTransportType = device.aSecCfg.transportPacketMagicHeader + device.awg.ASecCfg.TransportPacketMagicHeader = tempAwg.ASecCfg.TransportPacketMagicHeader + MessageTransportType = device.awg.ASecCfg.TransportPacketMagicHeader } else { device.log.Verbosef("UAPI: Using default transport type") - MessageTransportType = 4 + MessageTransportType = DefaultMessageTransportType } - isSameMap := map[uint32]bool{} - isSameMap[MessageInitiationType] = true - isSameMap[MessageResponseType] = true - isSameMap[MessageCookieReplyType] = true - isSameMap[MessageTransportType] = true + isSameHeaderMap := map[uint32]struct{}{ + MessageInitiationType: {}, + MessageResponseType: {}, + MessageCookieReplyType: {}, + MessageTransportType: {}, + } // size will be different if same values - if len(isSameMap) != 4 { - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d; %w`, - MessageInitiationType, - MessageResponseType, - MessageCookieReplyType, - MessageTransportType, - err, - ) - } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d`, - MessageInitiationType, - MessageResponseType, - MessageCookieReplyType, - MessageTransportType, - ) + if len(isSameHeaderMap) != 4 { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d`, + MessageInitiationType, + MessageResponseType, + MessageCookieReplyType, + MessageTransportType, + ), + ) + } + + isSameSizeMap := map[int]struct{}{ + newInitSize: {}, + newResponseSize: {}, + newCookieSize: {}, + newTransportSize: {}, + } + + if len(isSameSizeMap) != 4 { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `new sizes should differ; init: %d; response: %d; cookie: %d; trans: %d`, + newInitSize, + newResponseSize, + newCookieSize, + newTransportSize, + ), + ) + } else { + msgTypeToJunkSize = map[uint32]int{ + MessageInitiationType: device.awg.ASecCfg.InitHeaderJunkSize, + MessageResponseType: device.awg.ASecCfg.ResponseHeaderJunkSize, + MessageCookieReplyType: device.awg.ASecCfg.CookieReplyHeaderJunkSize, + MessageTransportType: device.awg.ASecCfg.TransportHeaderJunkSize, + } + + packetSizeToMsgType = map[int]uint32{ + newInitSize: MessageInitiationType, + newResponseSize: MessageResponseType, + newCookieSize: MessageCookieReplyType, + newTransportSize: MessageTransportType, } } - newInitSize := MessageInitiationSize + device.aSecCfg.initPacketJunkSize - newResponseSize := MessageResponseSize + device.aSecCfg.responsePacketJunkSize + device.awg.IsASecOn.SetTo(isASecOn) + var err error + device.awg.JunkCreator, err = awg.NewJunkCreator(device.awg.ASecCfg) + if err != nil { + errs = append(errs, err) + } - if newInitSize == newResponseSize { - if err != nil { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `new init size:%d; and new response size:%d; should differ; %w`, - newInitSize, - newResponseSize, - err, - ) + if tempAwg.HandshakeHandler.IsSet { + if err := tempAwg.HandshakeHandler.Validate(); err != nil { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, "handshake handler validate: %w", err)) } else { - err = ipcErrorf( - ipc.IpcErrorInvalid, - `new init size:%d; and new response size:%d; should differ`, - newInitSize, - newResponseSize, - ) + device.awg.HandshakeHandler = tempAwg.HandshakeHandler + device.awg.HandshakeHandler.ControlledJunk.DefaultJunkCount = tempAwg.ASecCfg.JunkPacketCount + device.awg.HandshakeHandler.SpecialJunk.DefaultJunkCount = tempAwg.ASecCfg.JunkPacketCount + device.version = VersionAwgSpecialHandshake } } else { - packetSizeToMsgType = map[int]uint32{ - newInitSize: MessageInitiationType, - newResponseSize: MessageResponseType, - MessageCookieReplySize: MessageCookieReplyType, - MessageTransportSize: MessageTransportType, - } - - msgTypeToJunkSize = map[uint32]int{ - MessageInitiationType: device.aSecCfg.initPacketJunkSize, - MessageResponseType: device.aSecCfg.responsePacketJunkSize, - MessageCookieReplyType: 0, - MessageTransportType: 0, - } + device.version = VersionAwg } - device.isASecOn.SetTo(isASecOn) - device.junkCreator, err = NewJunkCreator(device) - device.aSecMux.Unlock() + device.awg.ASecMux.Unlock() - return err + return errors.Join(errs...) } diff --git a/device/device_test.go b/device/device_test.go index f66d326..5824cf9 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -7,19 +7,22 @@ package device import ( "bytes" + "context" "encoding/hex" "fmt" "io" "math/rand" "net/netip" "os" + "os/signal" "runtime" "runtime/pprof" "sync" - "sync/atomic" "testing" "time" + "go.uber.org/atomic" + "github.com/amnezia-vpn/amneziawg-go/conn" "github.com/amnezia-vpn/amneziawg-go/conn/bindtest" "github.com/amnezia-vpn/amneziawg-go/tun" @@ -50,7 +53,7 @@ func uapiCfg(cfg ...string) 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) { +func genConfigs(tb testing.TB, cfg ...string) (cfgs, endpointCfgs [2]string) { var key1, key2 NoisePrivateKey _, err := rand.Read(key1[:]) if err != nil { @@ -62,7 +65,8 @@ func genConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { } pub1, pub2 := key1.publicKey(), key2.publicKey() - cfgs[0] = uapiCfg( + args0 := append([]string(nil), cfg...) + args0 = append(args0, []string{ "private_key", hex.EncodeToString(key1[:]), "listen_port", "0", "replace_peers", "true", @@ -70,12 +74,16 @@ func genConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { "protocol_version", "1", "replace_allowed_ips", "true", "allowed_ip", "1.0.0.2/32", - ) + }...) + cfgs[0] = uapiCfg(args0...) + endpointCfgs[0] = uapiCfg( "public_key", hex.EncodeToString(pub2[:]), "endpoint", "127.0.0.1:%d", ) - cfgs[1] = uapiCfg( + + args1 := append([]string(nil), cfg...) + args1 = append(args1, []string{ "private_key", hex.EncodeToString(key2[:]), "listen_port", "0", "replace_peers", "true", @@ -83,66 +91,9 @@ func genConfigs(tb testing.TB) (cfgs, endpointCfgs [2]string) { "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 -} + }...) -func genASecurityConfigs(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", - "jc", "5", - "jmin", "500", - "jmax", "1000", - "s1", "30", - "s2", "40", - "h1", "123456", - "h2", "67543", - "h4", "32345", - "h3", "123123", - "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", - "jc", "5", - "jmin", "500", - "jmax", "1000", - "s1", "30", - "s2", "40", - "h1", "123456", - "h2", "67543", - "h4", "32345", - "h3", "123123", - "public_key", hex.EncodeToString(pub1[:]), - "protocol_version", "1", - "replace_allowed_ips", "true", - "allowed_ip", "1.0.0.1/32", - ) + cfgs[1] = uapiCfg(args1...) endpointCfgs[1] = uapiCfg( "public_key", hex.EncodeToString(pub1[:]), "endpoint", "127.0.0.1:%d", @@ -185,9 +136,10 @@ func (pair *testPair) Send( // 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) + timer := time.NewTimer(6 * time.Second) defer timer.Stop() var err error select { @@ -214,14 +166,12 @@ func (pair *testPair) Send( // genTestPair creates a testPair. func genTestPair( tb testing.TB, - realSocket, withASecurity bool, + realSocket bool, + extraCfg ...string, ) (pair testPair) { var cfg, endpointCfg [2]string - if withASecurity { - cfg, endpointCfg = genASecurityConfigs(tb) - } else { - cfg, endpointCfg = genConfigs(tb) - } + cfg, endpointCfg = genConfigs(tb, extraCfg...) + var binds [2]conn.Bind if realSocket { binds[0], binds[1] = conn.NewDefaultBind(), conn.NewDefaultBind() @@ -265,7 +215,7 @@ func genTestPair( func TestTwoDevicePing(t *testing.T) { goroutineLeakCheck(t) - pair := genTestPair(t, true, false) + pair := genTestPair(t, true) t.Run("ping 1.0.0.1", func(t *testing.T) { pair.Send(t, Ping, nil) }) @@ -274,9 +224,23 @@ func TestTwoDevicePing(t *testing.T) { }) } -func TestASecurityTwoDevicePing(t *testing.T) { +// Run test with -race=false to avoid the race for setting the default msgTypes 2 times +func TestAWGDevicePing(t *testing.T) { goroutineLeakCheck(t) - pair := genTestPair(t, true, true) + + pair := genTestPair(t, true, + "jc", "5", + "jmin", "500", + "jmax", "1000", + "s1", "30", + "s2", "40", + "s3", "50", + "s4", "5", + "h1", "123456", + "h2", "67543", + "h3", "123123", + "h4", "32345", + ) t.Run("ping 1.0.0.1", func(t *testing.T) { pair.Send(t, Ping, nil) }) @@ -285,13 +249,58 @@ func TestASecurityTwoDevicePing(t *testing.T) { }) } +// Needs to be stopped with Ctrl-C +func TestAWGHandshakeDevicePing(t *testing.T) { + t.Skip("This test is intended to be run manually, not as part of the test suite.") + + signalContext, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + isRunning := atomic.NewBool(true) + go func() { + <-signalContext.Done() + fmt.Println("Waiting to finish") + isRunning.Store(false) + }() + + goroutineLeakCheck(t) + pair := genTestPair(t, true, + "i1", "", + "i2", "", + "j1", "", + "j2", "", + "j3", "", + "itime", "60", + // "jc", "1", + // "jmin", "500", + // "jmax", "1000", + // "s1", "30", + // "s2", "40", + // "h1", "123456", + // "h2", "67543", + // "h4", "32345", + // "h3", "123123", + ) + t.Run("ping 1.0.0.1", func(t *testing.T) { + for isRunning.Load() { + pair.Send(t, Ping, nil) + time.Sleep(2 * time.Second) + } + }) + t.Run("ping 1.0.0.2", func(t *testing.T) { + for isRunning.Load() { + pair.Send(t, Pong, nil) + time.Sleep(2 * time.Second) + } + }) +} + func TestUpDown(t *testing.T) { goroutineLeakCheck(t) const itrials = 50 const otrials = 10 for n := 0; n < otrials; n++ { - pair := genTestPair(t, false, false) + 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[:]))) @@ -325,7 +334,7 @@ func TestUpDown(t *testing.T) { // 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, false) + pair := genTestPair(t, true) done := make(chan struct{}) const warmupIters = 10 @@ -406,7 +415,7 @@ func TestConcurrencySafety(t *testing.T) { } func BenchmarkLatency(b *testing.B) { - pair := genTestPair(b, true, false) + pair := genTestPair(b, true) // Establish a connection. pair.Send(b, Ping, nil) @@ -420,7 +429,7 @@ func BenchmarkLatency(b *testing.B) { } func BenchmarkThroughput(b *testing.B) { - pair := genTestPair(b, true, false) + pair := genTestPair(b, true) // Establish a connection. pair.Send(b, Ping, nil) @@ -464,7 +473,7 @@ func BenchmarkThroughput(b *testing.B) { } func BenchmarkUAPIGet(b *testing.B) { - pair := genTestPair(b, true, false) + pair := genTestPair(b, true) pair.Send(b, Ping, nil) pair.Send(b, Pong, nil) b.ReportAllocs() diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 789eb16..f637b24 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -52,11 +52,18 @@ const ( WGLabelCookie = "cookie--" ) +const ( + DefaultMessageInitiationType uint32 = 1 + DefaultMessageResponseType uint32 = 2 + DefaultMessageCookieReplyType uint32 = 3 + DefaultMessageTransportType uint32 = 4 +) + var ( - MessageInitiationType uint32 = 1 - MessageResponseType uint32 = 2 - MessageCookieReplyType uint32 = 3 - MessageTransportType uint32 = 4 + MessageInitiationType uint32 = DefaultMessageInitiationType + MessageResponseType uint32 = DefaultMessageResponseType + MessageCookieReplyType uint32 = DefaultMessageCookieReplyType + MessageTransportType uint32 = DefaultMessageTransportType ) const ( @@ -75,9 +82,10 @@ const ( MessageTransportOffsetContent = 16 ) -var packetSizeToMsgType map[int]uint32 - -var msgTypeToJunkSize map[uint32]int +var ( + packetSizeToMsgType map[int]uint32 + msgTypeToJunkSize map[uint32]int +) /* Type is an 8-bit field, followed by 3 nul bytes, * by marshalling the messages in little-endian byteorder @@ -197,12 +205,12 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e handshake.mixHash(handshake.remoteStatic[:]) - device.aSecMux.RLock() + device.awg.ASecMux.RLock() msg := MessageInitiation{ Type: MessageInitiationType, Ephemeral: handshake.localEphemeral.publicKey(), } - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() handshake.mixKey(msg.Ephemeral[:]) handshake.mixHash(msg.Ephemeral[:]) @@ -256,12 +264,12 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { chainKey [blake2s.Size]byte ) - device.aSecMux.RLock() + device.awg.ASecMux.RLock() if msg.Type != MessageInitiationType { - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() return nil } - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() device.staticIdentity.RLock() defer device.staticIdentity.RUnlock() @@ -376,9 +384,9 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } var msg MessageResponse - device.aSecMux.RLock() + device.awg.ASecMux.RLock() msg.Type = MessageResponseType - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() msg.Sender = handshake.localIndex msg.Receiver = handshake.remoteIndex @@ -428,12 +436,12 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { - device.aSecMux.RLock() + device.awg.ASecMux.RLock() if msg.Type != MessageResponseType { - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() return nil } - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() // lookup handshake by receiver diff --git a/device/peer.go b/device/peer.go index 8f88b2a..e8a5168 100644 --- a/device/peer.go +++ b/device/peer.go @@ -13,6 +13,7 @@ import ( "time" "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device/awg" ) type Peer struct { @@ -113,6 +114,16 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { return peer, nil } +func (peer *Peer) SendAndCountBuffers(buffers [][]byte) error { + err := peer.SendBuffers(buffers) + if err == nil { + awg.PacketCounter.Add(uint64(len(buffers))) + return nil + } + + return err +} + func (peer *Peer) SendBuffers(buffers [][]byte) error { peer.device.net.RLock() defer peer.device.net.RUnlock() diff --git a/device/receive.go b/device/receive.go index 0a4910a..6daba0d 100644 --- a/device/receive.go +++ b/device/receive.go @@ -129,7 +129,7 @@ func (device *Device) RoutineReceiveIncoming( } deathSpiral = 0 - device.aSecMux.RLock() + device.awg.ASecMux.RLock() // handle each packet in the batch for i, size := range sizes[:count] { if size < MinMessageSize { @@ -137,10 +137,14 @@ func (device *Device) RoutineReceiveIncoming( } // check size of packet - packet := bufsArrs[i][:size] var msgType uint32 - if device.isAdvancedSecurityOn() { + if device.isAWG() { + // TODO: + // if awg.WaitResponse.ShouldWait.IsSet() { + // awg.WaitResponse.Channel <- struct{}{} + // } + if assumedMsgType, ok := packetSizeToMsgType[size]; ok { junkSize := msgTypeToJunkSize[assumedMsgType] // transport size can align with other header types; @@ -149,19 +153,29 @@ func (device *Device) RoutineReceiveIncoming( if msgType == assumedMsgType { packet = packet[junkSize:] } else { - device.log.Verbosef("Transport packet lined up with another msg type") + device.log.Verbosef("transport packet lined up with another msg type") msgType = binary.LittleEndian.Uint32(packet[:4]) } } else { - msgType = binary.LittleEndian.Uint32(packet[:4]) + transportJunkSize := device.awg.ASecCfg.TransportHeaderJunkSize + msgType = binary.LittleEndian.Uint32(packet[transportJunkSize : transportJunkSize+4]) if msgType != MessageTransportType { - device.log.Verbosef("ASec: Received message with unknown type") + // probably a junk packet + device.log.Verbosef("aSec: Received message with unknown type: %d", msgType) continue } + + // remove junk from bufsArrs by shifting the packet + // this buffer is also used for decryption, so it needs to be corrected + copy(bufsArrs[i][:size], packet[transportJunkSize:]) + size -= transportJunkSize + // need to reinitialize packet as well + packet = packet[:size] } } else { msgType = binary.LittleEndian.Uint32(packet[:4]) } + switch msgType { // check if transport @@ -245,7 +259,7 @@ func (device *Device) RoutineReceiveIncoming( default: } } - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() for peer, elemsContainer := range elemsByPeer { if peer.isRunning.Load() { peer.queue.inbound.c <- elemsContainer @@ -304,7 +318,7 @@ func (device *Device) RoutineHandshake(id int) { for elem := range device.queue.handshake.c { - device.aSecMux.RLock() + device.awg.ASecMux.RLock() // handle cookie fields and ratelimiting @@ -456,7 +470,7 @@ func (device *Device) RoutineHandshake(id int) { peer.SendKeepalive() } skip: - device.aSecMux.RUnlock() + device.awg.ASecMux.RUnlock() device.PutMessageBuffer(elem.buffer) } } diff --git a/device/send.go b/device/send.go index 7f0faa3..04ca2ad 100644 --- a/device/send.go +++ b/device/send.go @@ -124,12 +124,30 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } var sendBuffer [][]byte + // so only packet processed for cookie generation var junkedHeader []byte - if peer.device.isAdvancedSecurityOn() { - peer.device.aSecMux.RLock() - junks, err := peer.device.junkCreator.createJunkPackets() - peer.device.aSecMux.RUnlock() + if peer.device.version >= VersionAwg { + var junks [][]byte + if peer.device.version == VersionAwgSpecialHandshake { + peer.device.awg.ASecMux.RLock() + // set junks depending on packet type + junks = peer.device.awg.HandshakeHandler.GenerateSpecialJunk() + if junks == nil { + junks = peer.device.awg.HandshakeHandler.GenerateControlledJunk() + if junks != nil { + peer.device.log.Verbosef("%v - Controlled junks sent", peer) + } + } else { + peer.device.log.Verbosef("%v - Special junks sent", peer) + } + peer.device.awg.ASecMux.RUnlock() + } else { + junks = make([][]byte, 0, peer.device.awg.ASecCfg.JunkPacketCount) + } + peer.device.awg.ASecMux.RLock() + err := peer.device.awg.JunkCreator.CreateJunkPackets(&junks) + peer.device.awg.ASecMux.RUnlock() if err != nil { peer.device.log.Errorf("%v - %v", peer, err) @@ -145,19 +163,11 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { } } - peer.device.aSecMux.RLock() - if peer.device.aSecCfg.initPacketJunkSize != 0 { - buf := make([]byte, 0, peer.device.aSecCfg.initPacketJunkSize) - writer := bytes.NewBuffer(buf[:0]) - err = peer.device.junkCreator.appendJunk(writer, peer.device.aSecCfg.initPacketJunkSize) - if err != nil { - peer.device.log.Errorf("%v - %v", peer, err) - peer.device.aSecMux.RUnlock() - return err - } - junkedHeader = writer.Bytes() + junkedHeader, err = peer.device.awg.CreateInitHeaderJunk() + if err != nil { + peer.device.log.Errorf("%v - %v", peer, err) + return err } - peer.device.aSecMux.RUnlock() } var buf [MessageInitiationSize]byte @@ -172,7 +182,7 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { sendBuffer = append(sendBuffer, junkedHeader) - err = peer.SendBuffers(sendBuffer) + err = peer.SendAndCountBuffers(sendBuffer) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -193,22 +203,13 @@ func (peer *Peer) SendHandshakeResponse() error { peer.device.log.Errorf("%v - Failed to create response message: %v", peer, err) return err } - var junkedHeader []byte - if peer.device.isAdvancedSecurityOn() { - peer.device.aSecMux.RLock() - if peer.device.aSecCfg.responsePacketJunkSize != 0 { - buf := make([]byte, 0, peer.device.aSecCfg.responsePacketJunkSize) - writer := bytes.NewBuffer(buf[:0]) - err = peer.device.junkCreator.appendJunk(writer, peer.device.aSecCfg.responsePacketJunkSize) - if err != nil { - peer.device.aSecMux.RUnlock() - peer.device.log.Errorf("%v - %v", peer, err) - return err - } - junkedHeader = writer.Bytes() - } - peer.device.aSecMux.RUnlock() + + junkedHeader, err := peer.device.awg.CreateResponseHeaderJunk() + if err != nil { + peer.device.log.Errorf("%v - %v", peer, err) + return err } + var buf [MessageResponseSize]byte writer := bytes.NewBuffer(buf[:0]) @@ -228,7 +229,7 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketSent() // TODO: allocation could be avoided - err = peer.SendBuffers([][]byte{junkedHeader}) + err = peer.SendAndCountBuffers([][]byte{junkedHeader}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } @@ -251,11 +252,19 @@ func (device *Device) SendHandshakeCookie( return err } + junkedHeader, err := device.awg.CreateCookieReplyHeaderJunk() + if err != nil { + device.log.Errorf("%v - %v", device, err) + return err + } + var buf [MessageCookieReplySize]byte writer := bytes.NewBuffer(buf[:0]) binary.Write(writer, binary.LittleEndian, reply) + + junkedHeader = append(junkedHeader, writer.Bytes()...) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{junkedHeader}, initiatingElem.endpoint) return nil } @@ -576,6 +585,14 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { for _, elem := range elemsContainer.elems { if len(elem.packet) != MessageKeepaliveSize { dataSent = true + + junkedHeader, err := device.awg.CreateTransportHeaderJunk(len(elem.packet)) + if err != nil { + device.log.Errorf("%v - %v", device, err) + continue + } + + elem.packet = append(junkedHeader, elem.packet...) } bufs = append(bufs, elem.packet) } @@ -583,10 +600,11 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - err := peer.SendBuffers(bufs) + err := peer.SendAndCountBuffers(bufs) if dataSent { peer.timersDataSent() } + for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutOutboundElement(elem) diff --git a/device/uapi.go b/device/uapi.go index 870bddc..e9f962a 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,6 +18,7 @@ import ( "sync" "time" + "github.com/amnezia-vpn/amneziawg-go/device/awg" "github.com/amnezia-vpn/amneziawg-go/ipc" ) @@ -97,33 +98,51 @@ func (device *Device) IpcGetOperation(w io.Writer) error { sendf("fwmark=%d", device.net.fwmark) } - if device.isAdvancedSecurityOn() { - if device.aSecCfg.junkPacketCount != 0 { - sendf("jc=%d", device.aSecCfg.junkPacketCount) + if device.isAWG() { + if device.awg.ASecCfg.JunkPacketCount != 0 { + sendf("jc=%d", device.awg.ASecCfg.JunkPacketCount) } - if device.aSecCfg.junkPacketMinSize != 0 { - sendf("jmin=%d", device.aSecCfg.junkPacketMinSize) + if device.awg.ASecCfg.JunkPacketMinSize != 0 { + sendf("jmin=%d", device.awg.ASecCfg.JunkPacketMinSize) } - if device.aSecCfg.junkPacketMaxSize != 0 { - sendf("jmax=%d", device.aSecCfg.junkPacketMaxSize) + if device.awg.ASecCfg.JunkPacketMaxSize != 0 { + sendf("jmax=%d", device.awg.ASecCfg.JunkPacketMaxSize) } - if device.aSecCfg.initPacketJunkSize != 0 { - sendf("s1=%d", device.aSecCfg.initPacketJunkSize) + if device.awg.ASecCfg.InitHeaderJunkSize != 0 { + sendf("s1=%d", device.awg.ASecCfg.InitHeaderJunkSize) } - if device.aSecCfg.responsePacketJunkSize != 0 { - sendf("s2=%d", device.aSecCfg.responsePacketJunkSize) + if device.awg.ASecCfg.ResponseHeaderJunkSize != 0 { + sendf("s2=%d", device.awg.ASecCfg.ResponseHeaderJunkSize) } - if device.aSecCfg.initPacketMagicHeader != 0 { - sendf("h1=%d", device.aSecCfg.initPacketMagicHeader) + if device.awg.ASecCfg.CookieReplyHeaderJunkSize != 0 { + sendf("s3=%d", device.awg.ASecCfg.CookieReplyHeaderJunkSize) } - if device.aSecCfg.responsePacketMagicHeader != 0 { - sendf("h2=%d", device.aSecCfg.responsePacketMagicHeader) + if device.awg.ASecCfg.TransportHeaderJunkSize != 0 { + sendf("s4=%d", device.awg.ASecCfg.TransportHeaderJunkSize) } - if device.aSecCfg.underloadPacketMagicHeader != 0 { - sendf("h3=%d", device.aSecCfg.underloadPacketMagicHeader) + if device.awg.ASecCfg.InitPacketMagicHeader != 0 { + sendf("h1=%d", device.awg.ASecCfg.InitPacketMagicHeader) } - if device.aSecCfg.transportPacketMagicHeader != 0 { - sendf("h4=%d", device.aSecCfg.transportPacketMagicHeader) + if device.awg.ASecCfg.ResponsePacketMagicHeader != 0 { + sendf("h2=%d", device.awg.ASecCfg.ResponsePacketMagicHeader) + } + if device.awg.ASecCfg.UnderloadPacketMagicHeader != 0 { + sendf("h3=%d", device.awg.ASecCfg.UnderloadPacketMagicHeader) + } + if device.awg.ASecCfg.TransportPacketMagicHeader != 0 { + sendf("h4=%d", device.awg.ASecCfg.TransportPacketMagicHeader) + } + + specialJunkIpcFields := device.awg.HandshakeHandler.SpecialJunk.IpcGetFields() + for _, field := range specialJunkIpcFields { + sendf("%s=%s", field.Key, field.Value) + } + controlledJunkIpcFields := device.awg.HandshakeHandler.ControlledJunk.IpcGetFields() + for _, field := range controlledJunkIpcFields { + sendf("%s=%s", field.Key, field.Value) + } + if device.awg.HandshakeHandler.ITimeout != 0 { + sendf("itime=%d", device.awg.HandshakeHandler.ITimeout/time.Second) } } @@ -180,13 +199,13 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { peer := new(ipcSetPeer) deviceConfig := true - tempASecCfg := aSecCfgType{} + tempAwg := awg.Protocol{} scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if line == "" { // Blank line means terminate operation. - err := device.handlePostConfig(&tempASecCfg) + err := device.handlePostConfig(&tempAwg) if err != nil { return err } @@ -217,7 +236,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { var err error if deviceConfig { - err = device.handleDeviceLine(key, value, &tempASecCfg) + err = device.handleDeviceLine(key, value, &tempAwg) } else { err = device.handlePeerLine(peer, key, value) } @@ -225,7 +244,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return err } } - err = device.handlePostConfig(&tempASecCfg) + err = device.handlePostConfig(&tempAwg) if err != nil { return err } @@ -237,7 +256,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return nil } -func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgType) error { +func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) error { switch key { case "private_key": var sk NoisePrivateKey @@ -278,7 +297,11 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy case "replace_peers": 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.RemoveAllPeers() @@ -286,80 +309,138 @@ func (device *Device) handleDeviceLine(key, value string, tempASecCfg *aSecCfgTy case "jc": junkPacketCount, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_count %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_count %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_count") - tempASecCfg.junkPacketCount = junkPacketCount - tempASecCfg.isSet = true + tempAwg.ASecCfg.JunkPacketCount = junkPacketCount + tempAwg.ASecCfg.IsSet = true case "jmin": junkPacketMinSize, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_min_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_min_size %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_min_size") - tempASecCfg.junkPacketMinSize = junkPacketMinSize - tempASecCfg.isSet = true + tempAwg.ASecCfg.JunkPacketMinSize = junkPacketMinSize + tempAwg.ASecCfg.IsSet = true case "jmax": junkPacketMaxSize, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse junk_packet_max_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_max_size %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_max_size") - tempASecCfg.junkPacketMaxSize = junkPacketMaxSize - tempASecCfg.isSet = true + tempAwg.ASecCfg.JunkPacketMaxSize = junkPacketMaxSize + tempAwg.ASecCfg.IsSet = true case "s1": initPacketJunkSize, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse init_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse init_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating init_packet_junk_size") - tempASecCfg.initPacketJunkSize = initPacketJunkSize - tempASecCfg.isSet = true + tempAwg.ASecCfg.InitHeaderJunkSize = initPacketJunkSize + tempAwg.ASecCfg.IsSet = true case "s2": responsePacketJunkSize, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse response_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse response_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating response_packet_junk_size") - tempASecCfg.responsePacketJunkSize = responsePacketJunkSize - tempASecCfg.isSet = true + tempAwg.ASecCfg.ResponseHeaderJunkSize = responsePacketJunkSize + tempAwg.ASecCfg.IsSet = true + + case "s3": + cookieReplyPacketJunkSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "parse cookie_reply_packet_junk_size %w", err) + } + device.log.Verbosef("UAPI: Updating cookie_reply_packet_junk_size") + tempAwg.ASecCfg.CookieReplyHeaderJunkSize = cookieReplyPacketJunkSize + tempAwg.ASecCfg.IsSet = true + + case "s4": + transportPacketJunkSize, err := strconv.Atoi(value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "parse transport_packet_junk_size %w", err) + } + device.log.Verbosef("UAPI: Updating transport_packet_junk_size") + tempAwg.ASecCfg.TransportHeaderJunkSize = transportPacketJunkSize + tempAwg.ASecCfg.IsSet = true case "h1": initPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse init_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse init_packet_magic_header %w", err) } - tempASecCfg.initPacketMagicHeader = uint32(initPacketMagicHeader) - tempASecCfg.isSet = true + tempAwg.ASecCfg.InitPacketMagicHeader = uint32(initPacketMagicHeader) + tempAwg.ASecCfg.IsSet = true case "h2": responsePacketMagicHeader, err := strconv.ParseUint(value, 10, 32) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse response_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse response_packet_magic_header %w", err) } - tempASecCfg.responsePacketMagicHeader = uint32(responsePacketMagicHeader) - tempASecCfg.isSet = true + tempAwg.ASecCfg.ResponsePacketMagicHeader = uint32(responsePacketMagicHeader) + tempAwg.ASecCfg.IsSet = true case "h3": underloadPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse underload_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse underload_packet_magic_header %w", err) } - tempASecCfg.underloadPacketMagicHeader = uint32(underloadPacketMagicHeader) - tempASecCfg.isSet = true + tempAwg.ASecCfg.UnderloadPacketMagicHeader = uint32(underloadPacketMagicHeader) + tempAwg.ASecCfg.IsSet = true case "h4": transportPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "faield to parse transport_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "parse transport_packet_magic_header %w", err) + } + tempAwg.ASecCfg.TransportPacketMagicHeader = uint32(transportPacketMagicHeader) + tempAwg.ASecCfg.IsSet = true + case "i1", "i2", "i3", "i4", "i5": + if len(value) == 0 { + device.log.Verbosef("UAPI: received empty %s", key) + return nil } - tempASecCfg.transportPacketMagicHeader = uint32(transportPacketMagicHeader) - tempASecCfg.isSet = true + generators, err := awg.Parse(key, value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "invalid %s: %w", key, err) + } + device.log.Verbosef("UAPI: Updating %s", key) + tempAwg.HandshakeHandler.SpecialJunk.AppendGenerator(generators) + tempAwg.HandshakeHandler.IsSet = true + case "j1", "j2", "j3": + if len(value) == 0 { + device.log.Verbosef("UAPI: received empty %s", key) + return nil + } + + generators, err := awg.Parse(key, value) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "invalid %s: %w", key, err) + } + device.log.Verbosef("UAPI: Updating %s", key) + + tempAwg.HandshakeHandler.ControlledJunk.AppendGenerator(generators) + tempAwg.HandshakeHandler.IsSet = true + case "itime": + if len(value) == 0 { + device.log.Verbosef("UAPI: received empty itime") + return nil + } + + itime, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "parse itime %w", err) + } + device.log.Verbosef("UAPI: Updating itime") + + tempAwg.HandshakeHandler.ITimeout = time.Duration(itime) * time.Second + tempAwg.HandshakeHandler.IsSet = true default: return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) } @@ -432,7 +513,11 @@ func (device *Device) handlePeerLine( case "update_only": // allow disabling of creation 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 { device.RemovePeer(peer.handshake.remoteStatic) @@ -478,7 +563,11 @@ func (device *Device) handlePeerLine( secs, err := strconv.ParseUint(value, 10, 16) 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)) @@ -489,7 +578,11 @@ func (device *Device) handlePeerLine( case "replace_allowed_ips": device.log.Verbosef("%v - UAPI: Removing all allowedips", peer.Peer) 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 { return nil @@ -568,7 +661,11 @@ func (device *Device) IpcHandle(socket net.Conn) { return } 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 } err = device.IpcGetOperation(buffered.Writer) diff --git a/go.mod b/go.mod index 99569f3..5e5f34d 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,23 @@ module github.com/amnezia-vpn/amneziawg-go -go 1.24 +go 1.24.4 require ( + github.com/stretchr/testify v1.10.0 + github.com/tevino/abool v1.2.0 github.com/tevino/abool/v2 v2.1.0 - golang.org/x/crypto v0.37.0 - golang.org/x/net v0.39.0 - golang.org/x/sys v0.32.0 + go.uber.org/atomic v1.11.0 + golang.org/x/crypto v0.39.0 + golang.org/x/net v0.41.0 + golang.org/x/sys v0.33.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c + gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/btree v1.1.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/time v0.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b8ac0bd..6b8f36b 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,40 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= +github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= -gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= +gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= +gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= +gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= +gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f h1:zmc4cHEcCudRt2O8VsCW7nYLfAsbVY2i910/DAop1TM= +gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= From 3f19f1c657d4a338f61eb2495eb4a2a8a6ac4843 Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov Date: Mon, 7 Jul 2025 15:15:29 +0200 Subject: [PATCH 102/173] fix: restore Dockerfile --- Dockerfile | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6d60440..f165899 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,23 +8,10 @@ RUN go mod download && \ FROM alpine:3.19 ARG AWGTOOLS_RELEASE="1.0.20241018" -RUN apk add linux-headers build-base -COPY awg-tools /awg-tools -RUN pwd && ls -la / && ls -la /awg-tools -WORKDIR /awg-tools/src -# RUN ls -la && pwd && ls awg-tools -RUN make -RUN mkdir -p build && \ - cp wg ./build/awg && \ - cp wg-quick/linux.bash ./build/awg-quick - -RUN cp build/awg /usr/bin/awg -RUN cp build/awg-quick /usr/bin/awg-quick - RUN apk --no-cache add iproute2 iptables bash && \ cd /usr/bin/ && \ - # wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ - # unzip -j alpine-3.19-amneziawg-tools.zip && \ + wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \ + unzip -j alpine-3.19-amneziawg-tools.zip && \ chmod +x /usr/bin/awg /usr/bin/awg-quick && \ ln -s /usr/bin/awg /usr/bin/wg && \ ln -s /usr/bin/awg-quick /usr/bin/wg-quick From 1f398ae148a8a8183e855289b4a4492dde0490dd Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 3 Jul 2025 20:54:49 -0700 Subject: [PATCH 103/173] conn,device: implement InitiationAwareEndpoint To be implemented by [magicsock.lazyEndpoint], which is responsible for triggering JIT peer configuration. Updates tailscale/corp#20732 Updates tailscale/corp#30042 Signed-off-by: Jordan Whited --- conn/conn.go | 15 +++++++++++++++ device/noise-protocol.go | 8 +++++++- device/noise_test.go | 30 +++++++++++++++++++++++++++++- device/receive.go | 2 +- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/conn/conn.go b/conn/conn.go index 2a04e6d..b949641 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -86,6 +86,21 @@ type Endpoint interface { 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. diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 555ce91..ad5838e 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -16,6 +16,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" + "github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/tai64n" ) @@ -338,7 +339,7 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e return &msg, nil } -func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { +func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation, endpoint conn.Endpoint) *Peer { var ( hash [blake2s.Size]byte chainKey [blake2s.Size]byte @@ -372,6 +373,11 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { // lookup peer + initEP, ok := endpoint.(conn.InitiationAwareEndpoint) + if ok { + initEP.InitiationMessagePublicKey(peerPK) + } + peer := device.LookupPeer(peerPK) if peer == nil || !peer.isRunning.Load() { return nil diff --git a/device/noise_test.go b/device/noise_test.go index 7d6af1d..160bee5 100644 --- a/device/noise_test.go +++ b/device/noise_test.go @@ -8,6 +8,7 @@ package device import ( "bytes" "encoding/binary" + "net/netip" "testing" "github.com/tailscale/wireguard-go/conn" @@ -56,6 +57,26 @@ func assertEqual(t *testing.T, a, b []byte) { } } +type initAwareEP struct { + calledWith *[32]byte +} + +var _ conn.Endpoint = (*initAwareEP)(nil) +var _ conn.InitiationAwareEndpoint = (*initAwareEP)(nil) + +func (i *initAwareEP) ClearSrc() {} +func (i *initAwareEP) SrcToString() string { return "" } +func (i *initAwareEP) DstToString() string { return "" } +func (i *initAwareEP) DstToBytes() []byte { return nil } +func (i *initAwareEP) DstIP() netip.Addr { return netip.Addr{} } +func (i *initAwareEP) SrcIP() netip.Addr { return netip.Addr{} } + +func (i *initAwareEP) InitiationMessagePublicKey(peerPublicKey [32]byte) { + calledWith := [32]byte{} + copy(calledWith[:], peerPublicKey[:]) + i.calledWith = &calledWith +} + func TestNoiseHandshake(t *testing.T) { dev1 := randDevice(t) dev2 := randDevice(t) @@ -93,10 +114,17 @@ func TestNoiseHandshake(t *testing.T) { writer := bytes.NewBuffer(packet) err = binary.Write(writer, binary.LittleEndian, msg1) assertNil(t, err) - peer := dev2.ConsumeMessageInitiation(msg1) + initEP := &initAwareEP{} + peer := dev2.ConsumeMessageInitiation(msg1, initEP) if peer == nil { t.Fatal("handshake failed at initiation message") } + if initEP.calledWith == nil { + t.Fatal("initAwareEP never called") + } + if *initEP.calledWith != dev1.staticIdentity.publicKey { + t.Fatal("initAwareEP called with unexpected public key") + } assertEqual( t, diff --git a/device/receive.go b/device/receive.go index bc37f91..e74de1a 100644 --- a/device/receive.go +++ b/device/receive.go @@ -359,7 +359,7 @@ func (device *Device) RoutineHandshake(id int) { // consume initiation - peer := device.ConsumeMessageInitiation(&msg) + peer := device.ConsumeMessageInitiation(&msg, elem.endpoint) if peer == nil { device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString()) goto skip From 4064566ecaf999e6d097f16f47c16453fbc67d8e Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Thu, 10 Jul 2025 21:36:38 -0700 Subject: [PATCH 104/173] device: fix keepalive detection in TX path Updates tailscale/corp#30364 Signed-off-by: Jordan Whited --- device/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device/send.go b/device/send.go index bf854b7..c8bb079 100644 --- a/device/send.go +++ b/device/send.go @@ -517,7 +517,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { dataSent := false elemsContainer.Lock() for _, elem := range elemsContainer.elems { - if len(elem.packet) != MessageKeepaliveSize { + if len(elem.packet[MessageEncapsulatingTransportSize:]) != MessageKeepaliveSize { dataSent = true } bufs = append(bufs, elem.packet) From 1d0488a3d7da6b6ed79202519f30e7a286e0d4e6 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 16 Jul 2025 08:43:20 -0700 Subject: [PATCH 105/173] conn,device: eval conn.PeerAwareEndpoint per-packet Peer.SetEndpointFromPacket is not called per-packet. It is guaranteed to be called at least once per packet batch. Updates tailscale/corp#30042 Updates tailscale/corp#20732 Signed-off-by: Jordan Whited --- conn/conn.go | 5 +++-- device/peer.go | 4 ---- device/receive.go | 3 +++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conn/conn.go b/conn/conn.go index b949641..f178161 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -109,8 +109,9 @@ type InitiationAwareEndpoint interface { // to learn the identification WireGuard can derive from the session // or handshake. // -// wireguard-go never installs a [PeerAwareEndpoint] as the [Endpoint] for a -// [Peer]. +// 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 diff --git a/device/peer.go b/device/peer.go index c188c31..064feb2 100644 --- a/device/peer.go +++ b/device/peer.go @@ -282,10 +282,6 @@ func (peer *Peer) Stop() { func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) { peer.endpoint.Lock() defer peer.endpoint.Unlock() - if ep, ok := endpoint.(conn.PeerAwareEndpoint); ok { - ep.FromPeer(peer.handshake.remoteStatic) - return - } if peer.endpoint.disableRoaming { return } diff --git a/device/receive.go b/device/receive.go index e74de1a..02c8f21 100644 --- a/device/receive.go +++ b/device/receive.go @@ -460,6 +460,9 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { peer.timersHandshakeComplete() 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 { From f6542209f40f3f8f9e3dc9403d331ad2881fd7e3 Mon Sep 17 00:00:00 2001 From: Mark Puha Date: Mon, 1 Sep 2025 14:04:52 +0200 Subject: [PATCH 106/173] feat: awg 2.0 (#91) * feat: ranged H1-H4 * feat: S3, S4 support * chore: updated awg-tools version --------- Co-authored-by: Yaroslav Gurov --- Dockerfile | 2 +- device/awg/awg.go | 160 +++----- device/awg/junk_creator.go | 64 ++-- device/awg/junk_creator_test.go | 82 ++-- device/awg/magic_header.go | 97 +++++ device/awg/magic_header_test.go | 488 ++++++++++++++++++++++++ device/awg/prng.go | 50 +++ device/awg/special_handshake_handler.go | 43 +-- device/awg/tag_generator.go | 127 +++--- device/awg/tag_generator_test.go | 140 ++++++- device/awg/tag_parser.go | 20 +- device/awg/tag_parser_test.go | 2 +- device/cookie.go | 3 +- device/cookie_test.go | 2 +- device/device.go | 337 ++++++++++------ device/device_test.go | 26 +- device/noise-protocol.go | 46 ++- device/receive.go | 48 +-- device/send.go | 53 ++- device/uapi.go | 149 +++----- go.mod | 2 +- go.sum | 14 +- 22 files changed, 1352 insertions(+), 603 deletions(-) create mode 100644 device/awg/magic_header.go create mode 100644 device/awg/magic_header_test.go create mode 100644 device/awg/prng.go diff --git a/Dockerfile b/Dockerfile index f165899..98a7e9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN go mod download && \ go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin FROM alpine:3.19 -ARG AWGTOOLS_RELEASE="1.0.20241018" +ARG AWGTOOLS_RELEASE="1.0.20250901" RUN apk --no-cache add iproute2 iptables bash && \ cd /usr/bin/ && \ diff --git a/device/awg/awg.go b/device/awg/awg.go index fd5a96d..888a42e 100644 --- a/device/awg/awg.go +++ b/device/awg/awg.go @@ -3,142 +3,88 @@ package awg import ( "bytes" "fmt" - "slices" - "strconv" - "strings" "sync" "github.com/tevino/abool" ) -type aSecCfgType struct { - IsSet bool - JunkPacketCount int - JunkPacketMinSize int - JunkPacketMaxSize int - InitHeaderJunkSize int - ResponseHeaderJunkSize int - CookieReplyHeaderJunkSize int - TransportHeaderJunkSize int - InitPacketMagicHeader uint32 - ResponsePacketMagicHeader uint32 - UnderloadPacketMagicHeader uint32 - TransportPacketMagicHeader uint32 - // InitPacketMagicHeader Limit - // ResponsePacketMagicHeader Limit - // UnderloadPacketMagicHeader Limit - // TransportPacketMagicHeader Limit -} +type Cfg struct { + IsSet bool + JunkPacketCount int + JunkPacketMinSize int + JunkPacketMaxSize int + InitHeaderJunkSize int + ResponseHeaderJunkSize int + CookieReplyHeaderJunkSize int + TransportHeaderJunkSize int -type Limit struct { - Min uint32 - Max uint32 - HeaderType uint32 -} - -func NewLimit(min, max, headerType uint32) (Limit, error) { - if min > max { - return Limit{}, fmt.Errorf("min (%d) cannot be greater than max (%d)", min, max) - } - - return Limit{ - Min: min, - Max: max, - HeaderType: headerType, - }, nil -} - -func ParseMagicHeader(key, value string, defaultHeaderType uint32) (Limit, error) { - // tempAwg.ASecCfg.InitPacketMagicHeader, err = awg.NewLimit(uint32(initPacketMagicHeaderMin), uint32(initPacketMagicHeaderMax), DNewLimit(min, max, headerType)efaultMessageInitiationType) - // var min, max, headerType uint32 - // _, err := fmt.Sscanf(value, "%d-%d:%d", &min, &max, &headerType) - // if err != nil { - // return Limit{}, fmt.Errorf("invalid magic header format: %s", value) - // } - - limits := strings.Split(value, "-") - if len(limits) != 2 { - return Limit{}, fmt.Errorf("invalid format for key: %s; %s", key, value) - } - - min, err := strconv.ParseUint(limits[0], 10, 32) - if err != nil { - return Limit{}, fmt.Errorf("parse min key: %s; value: ; %w", key, limits[0], err) - } - - max, err := strconv.ParseUint(limits[1], 10, 32) - if err != nil { - return Limit{}, fmt.Errorf("parse max key: %s; value: ; %w", key, limits[0], err) - } - - limit, err := NewLimit(uint32(min), uint32(max), defaultHeaderType) - if err != nil { - return Limit{}, fmt.Errorf("new lmit key: %s; value: ; %w", key, limits[0], err) - } - - return limit, nil -} - -type Limits []Limit - -func NewLimits(limits []Limit) Limits { - slices.SortFunc(limits, func(a, b Limit) int { - if a.Min < b.Min { - return -1 - } else if a.Min > b.Min { - return 1 - } - return 0 - }) - - return Limits(limits) + MagicHeaders MagicHeaders } type Protocol struct { - IsASecOn abool.AtomicBool + IsOn abool.AtomicBool // TODO: revision the need of the mutex - ASecMux sync.RWMutex - ASecCfg aSecCfgType - JunkCreator junkCreator + Mux sync.RWMutex + Cfg Cfg + JunkCreator JunkCreator HandshakeHandler SpecialHandshakeHandler } func (protocol *Protocol) CreateInitHeaderJunk() ([]byte, error) { - return protocol.createHeaderJunk(protocol.ASecCfg.InitHeaderJunkSize) + protocol.Mux.RLock() + defer protocol.Mux.RUnlock() + + return protocol.createHeaderJunk(protocol.Cfg.InitHeaderJunkSize, 0) } func (protocol *Protocol) CreateResponseHeaderJunk() ([]byte, error) { - return protocol.createHeaderJunk(protocol.ASecCfg.ResponseHeaderJunkSize) + protocol.Mux.RLock() + defer protocol.Mux.RUnlock() + + return protocol.createHeaderJunk(protocol.Cfg.ResponseHeaderJunkSize, 0) } func (protocol *Protocol) CreateCookieReplyHeaderJunk() ([]byte, error) { - return protocol.createHeaderJunk(protocol.ASecCfg.CookieReplyHeaderJunkSize) + protocol.Mux.RLock() + defer protocol.Mux.RUnlock() + + return protocol.createHeaderJunk(protocol.Cfg.CookieReplyHeaderJunkSize, 0) } func (protocol *Protocol) CreateTransportHeaderJunk(packetSize int) ([]byte, error) { - return protocol.createHeaderJunk(protocol.ASecCfg.TransportHeaderJunkSize, packetSize) + protocol.Mux.RLock() + defer protocol.Mux.RUnlock() + + return protocol.createHeaderJunk(protocol.Cfg.TransportHeaderJunkSize, packetSize) } -func (protocol *Protocol) createHeaderJunk(junkSize int, optExtraSize ...int) ([]byte, error) { - extraSize := 0 - if len(optExtraSize) == 1 { - extraSize = optExtraSize[0] +func (protocol *Protocol) createHeaderJunk(junkSize int, extraSize int) ([]byte, error) { + if junkSize == 0 { + return nil, nil } - var junk []byte - protocol.ASecMux.RLock() - if junkSize != 0 { - buf := make([]byte, 0, junkSize+extraSize) - writer := bytes.NewBuffer(buf[:0]) - err := protocol.JunkCreator.AppendJunk(writer, junkSize) - if err != nil { - protocol.ASecMux.RUnlock() - return nil, err + buf := make([]byte, 0, junkSize+extraSize) + writer := bytes.NewBuffer(buf[:0]) + + err := protocol.JunkCreator.AppendJunk(writer, junkSize) + if err != nil { + return nil, fmt.Errorf("append junk: %w", err) + } + + return writer.Bytes(), nil +} + +func (protocol *Protocol) GetMagicHeaderMinFor(msgType uint32) (uint32, error) { + for _, magicHeader := range protocol.Cfg.MagicHeaders.Values { + if magicHeader.Min <= msgType && msgType <= magicHeader.Max { + return magicHeader.Min, nil } - junk = writer.Bytes() } - protocol.ASecMux.RUnlock() - return junk, nil + return 0, fmt.Errorf("no header for value: %d", msgType) +} + +func (protocol *Protocol) GetMsgType(defaultMsgType uint32) (uint32, error) { + return protocol.Cfg.MagicHeaders.Get(defaultMsgType) } diff --git a/device/awg/junk_creator.go b/device/awg/junk_creator.go index 91fd253..8ba2918 100644 --- a/device/awg/junk_creator.go +++ b/device/awg/junk_creator.go @@ -2,69 +2,49 @@ package awg import ( "bytes" - crand "crypto/rand" "fmt" - v2 "math/rand/v2" ) -type junkCreator struct { - aSecCfg aSecCfgType - cha8Rand *v2.ChaCha8 +type JunkCreator struct { + cfg Cfg + randomGenerator PRNG[int] } // TODO: refactor param to only pass the junk related params -func NewJunkCreator(aSecCfg aSecCfgType) (junkCreator, error) { - buf := make([]byte, 32) - _, err := crand.Read(buf) - if err != nil { - return junkCreator{}, err - } - return junkCreator{aSecCfg: aSecCfg, cha8Rand: v2.NewChaCha8([32]byte(buf))}, nil +func NewJunkCreator(cfg Cfg) JunkCreator { + return JunkCreator{cfg: cfg, randomGenerator: NewPRNG[int]()} } -// Should be called with aSecMux RLocked -func (jc *junkCreator) CreateJunkPackets(junks *[][]byte) error { - if jc.aSecCfg.JunkPacketCount == 0 { - return nil +// Should be called with awg mux RLocked +func (jc *JunkCreator) CreateJunkPackets(junks *[][]byte) { + if jc.cfg.JunkPacketCount == 0 { + return } - for range jc.aSecCfg.JunkPacketCount { + for range jc.cfg.JunkPacketCount { packetSize := jc.randomPacketSize() - junk, err := jc.randomJunkWithSize(packetSize) - if err != nil { - return fmt.Errorf("create junk packet: %v", err) - } + junk := jc.randomJunkWithSize(packetSize) *junks = append(*junks, junk) } - return nil + return } -// Should be called with aSecMux RLocked -func (jc *junkCreator) randomPacketSize() int { - return int( - jc.cha8Rand.Uint64()%uint64( - jc.aSecCfg.JunkPacketMaxSize-jc.aSecCfg.JunkPacketMinSize, - ), - ) + jc.aSecCfg.JunkPacketMinSize +// Should be called with awg mux RLocked +func (jc *JunkCreator) randomPacketSize() int { + return jc.randomGenerator.RandomSizeInRange(jc.cfg.JunkPacketMinSize, jc.cfg.JunkPacketMaxSize) } -// Should be called with aSecMux RLocked -func (jc *junkCreator) AppendJunk(writer *bytes.Buffer, size int) error { - headerJunk, err := jc.randomJunkWithSize(size) - if err != nil { - return fmt.Errorf("create header junk: %v", err) - } - _, err = writer.Write(headerJunk) +// Should be called with awg mux RLocked +func (jc *JunkCreator) AppendJunk(writer *bytes.Buffer, size int) error { + headerJunk := jc.randomJunkWithSize(size) + _, err := writer.Write(headerJunk) if err != nil { return fmt.Errorf("write header junk: %v", err) } return nil } -// Should be called with aSecMux RLocked -func (jc *junkCreator) randomJunkWithSize(size int) ([]byte, error) { - // TODO: use a memory pool to allocate - junk := make([]byte, size) - _, err := jc.cha8Rand.Read(junk) - return junk, err +// Should be called with awg mux RLocked +func (jc *JunkCreator) randomJunkWithSize(size int) []byte { + return jc.randomGenerator.ReadSize(size) } diff --git a/device/awg/junk_creator_test.go b/device/awg/junk_creator_test.go index 424f104..cdf752b 100644 --- a/device/awg/junk_creator_test.go +++ b/device/awg/junk_creator_test.go @@ -6,43 +6,34 @@ import ( "testing" ) -func setUpJunkCreator(t *testing.T) (junkCreator, error) { - jc, err := NewJunkCreator(aSecCfgType{ - IsSet: true, - JunkPacketCount: 5, - JunkPacketMinSize: 500, - JunkPacketMaxSize: 1000, - InitHeaderJunkSize: 30, - ResponseHeaderJunkSize: 40, - InitPacketMagicHeader: 123456, - ResponsePacketMagicHeader: 67543, - UnderloadPacketMagicHeader: 32345, - TransportPacketMagicHeader: 123123, +func setUpJunkCreator() JunkCreator { + mh, _ := NewMagicHeaders( + []MagicHeader{ + NewMagicHeaderSameValue(123456), + NewMagicHeaderSameValue(67543), + NewMagicHeaderSameValue(32345), + NewMagicHeaderSameValue(123123), + }, + ) + + jc := NewJunkCreator(Cfg{ + IsSet: true, + JunkPacketCount: 5, + JunkPacketMinSize: 500, + JunkPacketMaxSize: 1000, + InitHeaderJunkSize: 30, + ResponseHeaderJunkSize: 40, + MagicHeaders: mh, }) - if err != nil { - t.Errorf("failed to create junk creator %v", err) - return junkCreator{}, err - } - - return jc, nil + return jc } func Test_junkCreator_createJunkPackets(t *testing.T) { - jc, err := setUpJunkCreator(t) - if err != nil { - return - } + jc := setUpJunkCreator() t.Run("valid", func(t *testing.T) { - got := make([][]byte, 0, jc.aSecCfg.JunkPacketCount) - err := jc.CreateJunkPackets(&got) - if err != nil { - t.Errorf( - "junkCreator.createJunkPackets() = %v; failed", - err, - ) - return - } + got := make([][]byte, 0, jc.cfg.JunkPacketCount) + jc.CreateJunkPackets(&got) seen := make(map[string]bool) for _, junk := range got { key := string(junk) @@ -61,34 +52,28 @@ func Test_junkCreator_createJunkPackets(t *testing.T) { func Test_junkCreator_randomJunkWithSize(t *testing.T) { t.Run("valid", func(t *testing.T) { - jc, err := setUpJunkCreator(t) - if err != nil { - return - } - r1, _ := jc.randomJunkWithSize(10) - r2, _ := jc.randomJunkWithSize(10) + jc := setUpJunkCreator() + r1 := jc.randomJunkWithSize(10) + r2 := jc.randomJunkWithSize(10) fmt.Printf("%v\n%v\n", r1, r2) if bytes.Equal(r1, r2) { - t.Errorf("same junks %v", err) + t.Errorf("same junks") return } }) } func Test_junkCreator_randomPacketSize(t *testing.T) { - jc, err := setUpJunkCreator(t) - if err != nil { - return - } + jc := setUpJunkCreator() for range [30]struct{}{} { t.Run("valid", func(t *testing.T) { - if got := jc.randomPacketSize(); jc.aSecCfg.JunkPacketMinSize > got || - got > jc.aSecCfg.JunkPacketMaxSize { + if got := jc.randomPacketSize(); jc.cfg.JunkPacketMinSize > got || + got > jc.cfg.JunkPacketMaxSize { t.Errorf( "junkCreator.randomPacketSize() = %v, not between range [%v,%v]", got, - jc.aSecCfg.JunkPacketMinSize, - jc.aSecCfg.JunkPacketMaxSize, + jc.cfg.JunkPacketMinSize, + jc.cfg.JunkPacketMaxSize, ) } }) @@ -96,10 +81,7 @@ func Test_junkCreator_randomPacketSize(t *testing.T) { } func Test_junkCreator_appendJunk(t *testing.T) { - jc, err := setUpJunkCreator(t) - if err != nil { - return - } + jc := setUpJunkCreator() t.Run("valid", func(t *testing.T) { s := "apple" buffer := bytes.NewBuffer([]byte(s)) diff --git a/device/awg/magic_header.go b/device/awg/magic_header.go new file mode 100644 index 0000000..aaf4e97 --- /dev/null +++ b/device/awg/magic_header.go @@ -0,0 +1,97 @@ +package awg + +import ( + "cmp" + "fmt" + "slices" + "strconv" + "strings" +) + +type MagicHeader struct { + Min uint32 + Max uint32 +} + +func NewMagicHeaderSameValue(value uint32) MagicHeader { + return MagicHeader{Min: value, Max: value} +} + +func NewMagicHeader(min, max uint32) (MagicHeader, error) { + if min > max { + return MagicHeader{}, fmt.Errorf("min (%d) cannot be greater than max (%d)", min, max) + } + + return MagicHeader{Min: min, Max: max}, nil +} + +func ParseMagicHeader(key, value string) (MagicHeader, error) { + hyphenIdx := strings.Index(value, "-") + if hyphenIdx == -1 { + // if there is no hyphen, we treat it as single magic header value + magicHeader, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return MagicHeader{}, fmt.Errorf("parse key: %s; value: %s; %w", key, value, err) + } + + return NewMagicHeader(uint32(magicHeader), uint32(magicHeader)) + } + + minStr := value[:hyphenIdx] + maxStr := value[hyphenIdx+1:] + if len(minStr) == 0 || len(maxStr) == 0 { + return MagicHeader{}, fmt.Errorf("invalid value for key: %s; value: %s; expected format: min-max", key, value) + } + + min, err := strconv.ParseUint(minStr, 10, 32) + if err != nil { + return MagicHeader{}, fmt.Errorf("parse min key: %s; value: %s; %w", key, minStr, err) + } + + max, err := strconv.ParseUint(maxStr, 10, 32) + if err != nil { + return MagicHeader{}, fmt.Errorf("parse max key: %s; value: %s; %w", key, maxStr, err) + } + + magicHeader, err := NewMagicHeader(uint32(min), uint32(max)) + if err != nil { + return MagicHeader{}, fmt.Errorf("new magicHeader key: %s; value: %s-%s; %w", key, minStr, maxStr, err) + } + + return magicHeader, nil +} + +type MagicHeaders struct { + Values []MagicHeader + randomGenerator RandomNumberGenerator[uint32] +} + +func NewMagicHeaders(headerValues []MagicHeader) (MagicHeaders, error) { + if len(headerValues) != 4 { + return MagicHeaders{}, fmt.Errorf("all header types should be included: %v", headerValues) + } + + sortedMagicHeaders := slices.SortedFunc(slices.Values(headerValues), func(lhs MagicHeader, rhs MagicHeader) int { + return cmp.Compare(lhs.Min, rhs.Min) + }) + + for i := range 3 { + if sortedMagicHeaders[i].Max >= sortedMagicHeaders[i+1].Min { + return MagicHeaders{}, fmt.Errorf( + "magic headers shouldn't overlap; %v > %v", + sortedMagicHeaders[i].Max, + sortedMagicHeaders[i+1].Min, + ) + } + } + + return MagicHeaders{Values: headerValues, randomGenerator: NewPRNG[uint32]()}, nil +} + +func (mh *MagicHeaders) Get(defaultMsgType uint32) (uint32, error) { + if defaultMsgType == 0 || defaultMsgType > 4 { + return 0, fmt.Errorf("invalid msg type: %d", defaultMsgType) + } + + return mh.randomGenerator.RandomSizeInRange(mh.Values[defaultMsgType-1].Min, mh.Values[defaultMsgType-1].Max), nil +} diff --git a/device/awg/magic_header_test.go b/device/awg/magic_header_test.go new file mode 100644 index 0000000..72a823e --- /dev/null +++ b/device/awg/magic_header_test.go @@ -0,0 +1,488 @@ +package awg + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewMagicHeaderSameValue(t *testing.T) { + tests := []struct { + name string + value uint32 + expected MagicHeader + }{ + { + name: "zero value", + value: 0, + expected: MagicHeader{Min: 0, Max: 0}, + }, + { + name: "small value", + value: 1, + expected: MagicHeader{Min: 1, Max: 1}, + }, + { + name: "large value", + value: 4294967295, // max uint32 + expected: MagicHeader{Min: 4294967295, Max: 4294967295}, + }, + { + name: "medium value", + value: 1000, + expected: MagicHeader{Min: 1000, Max: 1000}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := NewMagicHeaderSameValue(tt.value) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestNewMagicHeader(t *testing.T) { + tests := []struct { + name string + min uint32 + max uint32 + expected MagicHeader + errorMsg string + }{ + { + name: "valid range", + min: 1, + max: 10, + expected: MagicHeader{Min: 1, Max: 10}, + }, + { + name: "equal values", + min: 5, + max: 5, + expected: MagicHeader{Min: 5, Max: 5}, + }, + { + name: "zero range", + min: 0, + max: 0, + expected: MagicHeader{Min: 0, Max: 0}, + }, + { + name: "max uint32 range", + min: 4294967294, + max: 4294967295, + expected: MagicHeader{Min: 4294967294, Max: 4294967295}, + }, + { + name: "min greater than max", + min: 10, + max: 5, + expected: MagicHeader{}, + errorMsg: "min (10) cannot be greater than max (5)", + }, + { + name: "large min greater than max", + min: 4294967295, + max: 1, + expected: MagicHeader{}, + errorMsg: "min (4294967295) cannot be greater than max (1)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result, err := NewMagicHeader(tt.min, tt.max) + + if tt.errorMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + require.Equal(t, MagicHeader{}, result) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +func TestParseMagicHeader(t *testing.T) { + tests := []struct { + name string + key string + value string + expected MagicHeader + errorMsg string + }{ + { + name: "single value", + key: "header1", + value: "100", + expected: MagicHeader{Min: 100, Max: 100}, + }, + { + name: "valid range", + key: "header2", + value: "10-20", + expected: MagicHeader{Min: 10, Max: 20}, + }, + { + name: "zero single value", + key: "header3", + value: "0", + expected: MagicHeader{Min: 0, Max: 0}, + }, + { + name: "zero range", + key: "header4", + value: "0-0", + expected: MagicHeader{Min: 0, Max: 0}, + }, + { + name: "max uint32 single", + key: "header5", + value: "4294967295", + expected: MagicHeader{Min: 4294967295, Max: 4294967295}, + }, + { + name: "max uint32 range", + key: "header6", + value: "4294967294-4294967295", + expected: MagicHeader{Min: 4294967294, Max: 4294967295}, + }, + { + name: "invalid single value - not number", + key: "header7", + value: "abc", + expected: MagicHeader{}, + errorMsg: "parse key: header7; value: abc;", + }, + { + name: "invalid single value - negative", + key: "header8", + value: "-5", + expected: MagicHeader{}, + errorMsg: "invalid value for key: header8; value: -5;", + }, + { + name: "invalid single value - too large", + key: "header9", + value: "4294967296", + expected: MagicHeader{}, + errorMsg: "parse key: header9; value: 4294967296;", + }, + { + name: "invalid range - min not number", + key: "header10", + value: "abc-10", + expected: MagicHeader{}, + errorMsg: "parse min key: header10; value: abc;", + }, + { + name: "invalid range - max not number", + key: "header11", + value: "10-abc", + expected: MagicHeader{}, + errorMsg: "parse max key: header11; value: abc;", + }, + { + name: "invalid range - min greater than max", + key: "header12", + value: "20-10", + expected: MagicHeader{}, + errorMsg: "new magicHeader key: header12; value: 20-10;", + }, + { + name: "invalid range - too many parts", + key: "header13", + value: "10-20-30", + expected: MagicHeader{}, + errorMsg: "parse key: header13; value: 10-20-30;", + }, + { + name: "empty value", + key: "header14", + value: "", + expected: MagicHeader{}, + errorMsg: "parse key: header14; value: ;", + }, + { + name: "hyphen only", + key: "header15", + value: "-", + expected: MagicHeader{}, + errorMsg: "invalid value for key: header15; value: -;", + }, + { + name: "empty min", + key: "header16", + value: "-10", + expected: MagicHeader{}, + errorMsg: "invalid value for key: header16; value: -10;", + }, + { + name: "empty max", + key: "header17", + value: "10-", + expected: MagicHeader{}, + errorMsg: "invalid value for key: header17; value: 10-;", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result, err := ParseMagicHeader(tt.key, tt.value) + + if tt.errorMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + require.Equal(t, MagicHeader{}, result) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +func TestNewMagicHeaders(t *testing.T) { + tests := []struct { + name string + magicHeaders []MagicHeader + errorMsg string + }{ + { + name: "valid non-overlapping headers", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 11, Max: 20}, + {Min: 21, Max: 30}, + {Min: 31, Max: 40}, + }, + }, + { + name: "valid adjacent headers", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 1}, + {Min: 2, Max: 2}, + {Min: 3, Max: 3}, + {Min: 4, Max: 4}, + }, + }, + { + name: "valid zero-based headers", + magicHeaders: []MagicHeader{ + {Min: 0, Max: 0}, + {Min: 1, Max: 1}, + {Min: 2, Max: 2}, + {Min: 3, Max: 3}, + }, + }, + { + name: "valid large value headers", + magicHeaders: []MagicHeader{ + {Min: 4294967290, Max: 4294967291}, + {Min: 4294967292, Max: 4294967293}, + {Min: 4294967294, Max: 4294967294}, + {Min: 4294967295, Max: 4294967295}, + }, + }, + { + name: "too few headers", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 11, Max: 20}, + {Min: 21, Max: 30}, + }, + errorMsg: "all header types should be included:", + }, + { + name: "too many headers", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 11, Max: 20}, + {Min: 21, Max: 30}, + {Min: 31, Max: 40}, + {Min: 41, Max: 50}, + }, + errorMsg: "all header types should be included:", + }, + { + name: "empty headers", + magicHeaders: []MagicHeader{}, + errorMsg: "all header types should be included:", + }, + { + name: "overlapping headers", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 15}, + {Min: 10, Max: 20}, + {Min: 25, Max: 30}, + {Min: 35, Max: 40}, + }, + errorMsg: "magic headers shouldn't overlap;", + }, + { + name: "overlapping headers at limit-first", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 10, Max: 20}, + {Min: 25, Max: 30}, + {Min: 35, Max: 40}, + }, + errorMsg: "magic headers shouldn't overlap;", + }, + { + name: "overlapping headers at limit-second", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 15, Max: 25}, + {Min: 25, Max: 30}, + {Min: 35, Max: 40}, + }, + errorMsg: "magic headers shouldn't overlap;", + }, + { + name: "overlapping headers at limit-third", + magicHeaders: []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 15, Max: 25}, + {Min: 30, Max: 35}, + {Min: 35, Max: 40}, + }, + errorMsg: "magic headers shouldn't overlap;", + }, + { + name: "identical ranges", + magicHeaders: []MagicHeader{ + {Min: 10, Max: 20}, + {Min: 10, Max: 20}, + {Min: 25, Max: 30}, + {Min: 35, Max: 40}, + }, + errorMsg: "magic headers shouldn't overlap;", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result, err := NewMagicHeaders(tt.magicHeaders) + + if tt.errorMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + require.Equal(t, MagicHeaders{}, result) + } else { + require.NoError(t, err) + require.Equal(t, tt.magicHeaders, result.Values) + require.NotNil(t, result.randomGenerator) + } + }) + } +} + +// Mock PRNG for testing +type mockPRNG struct { + returnValue uint32 +} + +func (m *mockPRNG) RandomSizeInRange(min, max uint32) uint32 { + return m.returnValue +} + +func (m *mockPRNG) Get() uint64 { + return 0 +} +func (m *mockPRNG) ReadSize(size int) []byte { + return make([]byte, size) +} + +func TestMagicHeaders_Get(t *testing.T) { + // Create test headers + headers := []MagicHeader{ + {Min: 1, Max: 10}, + {Min: 11, Max: 20}, + {Min: 21, Max: 30}, + {Min: 31, Max: 40}, + } + + tests := []struct { + name string + defaultMsgType uint32 + mockValue uint32 + expectedValue uint32 + errorMsg string + }{ + { + name: "valid type 1", + defaultMsgType: 1, + mockValue: 5, + expectedValue: 5, + }, + { + name: "valid type 2", + defaultMsgType: 2, + mockValue: 15, + expectedValue: 15, + }, + { + name: "valid type 3", + defaultMsgType: 3, + mockValue: 25, + expectedValue: 25, + }, + { + name: "valid type 4", + defaultMsgType: 4, + mockValue: 35, + expectedValue: 35, + }, + { + name: "invalid type 0", + defaultMsgType: 0, + mockValue: 0, + expectedValue: 0, + errorMsg: "invalid msg type: 0", + }, + { + name: "invalid type 5", + defaultMsgType: 5, + mockValue: 0, + expectedValue: 0, + errorMsg: "invalid msg type: 5", + }, + { + name: "invalid type max uint32", + defaultMsgType: 4294967295, + mockValue: 0, + expectedValue: 0, + errorMsg: "invalid msg type: 4294967295", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Create a new instance with mock PRNG for each test + testMagicHeaders := MagicHeaders{ + Values: headers, + randomGenerator: &mockPRNG{returnValue: tt.mockValue}, + } + + result, err := testMagicHeaders.Get(tt.defaultMsgType) + + if tt.errorMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + require.Equal(t, uint32(0), result) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedValue, result) + } + }) + } +} diff --git a/device/awg/prng.go b/device/awg/prng.go new file mode 100644 index 0000000..e7661d7 --- /dev/null +++ b/device/awg/prng.go @@ -0,0 +1,50 @@ +package awg + +import ( + crand "crypto/rand" + v2 "math/rand/v2" + + "golang.org/x/exp/constraints" +) + +type RandomNumberGenerator[T constraints.Integer] interface { + RandomSizeInRange(min, max T) T + Get() uint64 + ReadSize(size int) []byte +} + +type PRNG[T constraints.Integer] struct { + cha8Rand *v2.ChaCha8 +} + +func NewPRNG[T constraints.Integer]() PRNG[T] { + buf := make([]byte, 32) + _, _ = crand.Read(buf) + + return PRNG[T]{ + cha8Rand: v2.NewChaCha8([32]byte(buf)), + } +} + +func (p PRNG[T]) RandomSizeInRange(min, max T) T { + if min > max { + panic("min must be less than max") + } + + if min == max { + return min + } + + return T(p.Get()%uint64(max-min)) + min +} + +func (p PRNG[T]) Get() uint64 { + return p.cha8Rand.Uint64() +} + +func (p PRNG[T]) ReadSize(size int) []byte { + // TODO: use a memory pool to allocate + buf := make([]byte, size) + _, _ = p.cha8Rand.Read(buf) + return buf +} diff --git a/device/awg/special_handshake_handler.go b/device/awg/special_handshake_handler.go index e582d97..d740879 100644 --- a/device/awg/special_handshake_handler.go +++ b/device/awg/special_handshake_handler.go @@ -1,9 +1,6 @@ package awg import ( - "errors" - "time" - "github.com/tevino/abool" "go.uber.org/atomic" ) @@ -21,25 +18,13 @@ var WaitResponse = struct { } type SpecialHandshakeHandler struct { - isFirstDone bool - SpecialJunk TagJunkPacketGenerators - ControlledJunk TagJunkPacketGenerators - - nextItime time.Time - ITimeout time.Duration // seconds + SpecialJunk TagJunkPacketGenerators IsSet bool } func (handler *SpecialHandshakeHandler) Validate() error { - var errs []error - if err := handler.SpecialJunk.Validate(); err != nil { - errs = append(errs, err) - } - if err := handler.ControlledJunk.Validate(); err != nil { - errs = append(errs, err) - } - return errors.Join(errs...) + return handler.SpecialJunk.Validate() } func (handler *SpecialHandshakeHandler) GenerateSpecialJunk() [][]byte { @@ -47,27 +32,5 @@ func (handler *SpecialHandshakeHandler) GenerateSpecialJunk() [][]byte { return nil } - // TODO: create tests - if !handler.isFirstDone { - handler.isFirstDone = true - } else if !handler.isTimeToSendSpecial() { - return nil - } - - rv := handler.SpecialJunk.GeneratePackets() - handler.nextItime = time.Now().Add(handler.ITimeout) - - return rv -} - -func (handler *SpecialHandshakeHandler) isTimeToSendSpecial() bool { - return time.Now().After(handler.nextItime) -} - -func (handler *SpecialHandshakeHandler) GenerateControlledJunk() [][]byte { - if !handler.ControlledJunk.IsDefined() { - return nil - } - - return handler.ControlledJunk.GeneratePackets() + return handler.SpecialJunk.GeneratePackets() } diff --git a/device/awg/tag_generator.go b/device/awg/tag_generator.go index 65d8004..3a1d497 100644 --- a/device/awg/tag_generator.go +++ b/device/awg/tag_generator.go @@ -59,43 +59,110 @@ func hexToBytes(hexStr string) ([]byte, error) { return hex.DecodeString(hexStr) } -type RandomPacketGenerator struct { +type randomGeneratorBase struct { cha8Rand *v2.ChaCha8 size int } -func (rpg *RandomPacketGenerator) Generate() []byte { - junk := make([]byte, rpg.size) - rpg.cha8Rand.Read(junk) - return junk -} - -func (rpg *RandomPacketGenerator) Size() int { - return rpg.size -} - -func newRandomPacketGenerator(param string) (Generator, error) { +func newRandomGeneratorBase(param string) (*randomGeneratorBase, error) { size, err := strconv.Atoi(param) if err != nil { - return nil, fmt.Errorf("random packet parse int: %w", err) + return nil, fmt.Errorf("parse int: %w", err) } if size > 1000 { - return nil, fmt.Errorf("random packet size must be less than 1000") + return nil, fmt.Errorf("size must be less than 1000") } buf := make([]byte, 32) _, err = crand.Read(buf) if err != nil { - return nil, fmt.Errorf("random packet crand read: %w", err) + return nil, fmt.Errorf("crand read: %w", err) } - return &RandomPacketGenerator{ + return &randomGeneratorBase{ cha8Rand: v2.NewChaCha8([32]byte(buf)), size: size, }, nil } +func (rpg *randomGeneratorBase) generate() []byte { + junk := make([]byte, rpg.size) + rpg.cha8Rand.Read(junk) + return junk +} + +func (rpg *randomGeneratorBase) Size() int { + return rpg.size +} + +type RandomBytesGenerator struct { + *randomGeneratorBase +} + +func newRandomBytesGenerator(param string) (Generator, error) { + rpgBase, err := newRandomGeneratorBase(param) + if err != nil { + return nil, fmt.Errorf("new random bytes generator: %w", err) + } + + return &RandomBytesGenerator{randomGeneratorBase: rpgBase}, nil +} + +func (rpg *RandomBytesGenerator) Generate() []byte { + return rpg.generate() +} + +const alphanumericChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +type RandomASCIIGenerator struct { + *randomGeneratorBase +} + +func newRandomASCIIGenerator(param string) (Generator, error) { + rpgBase, err := newRandomGeneratorBase(param) + if err != nil { + return nil, fmt.Errorf("new random ascii generator: %w", err) + } + + return &RandomASCIIGenerator{randomGeneratorBase: rpgBase}, nil +} + +func (rpg *RandomASCIIGenerator) Generate() []byte { + junk := rpg.generate() + + result := make([]byte, rpg.size) + for i, b := range junk { + result[i] = alphanumericChars[b%byte(len(alphanumericChars))] + } + + return result +} + +type RandomDigitGenerator struct { + *randomGeneratorBase +} + +func newRandomDigitGenerator(param string) (Generator, error) { + rpgBase, err := newRandomGeneratorBase(param) + if err != nil { + return nil, fmt.Errorf("new random digit generator: %w", err) + } + + return &RandomDigitGenerator{randomGeneratorBase: rpgBase}, nil +} + +func (rpg *RandomDigitGenerator) Generate() []byte { + junk := rpg.generate() + + result := make([]byte, rpg.size) + for i, b := range junk { + result[i] = '0' + (b % 10) // Convert to digit character + } + + return result +} + type TimestampGenerator struct { } @@ -117,34 +184,6 @@ func newTimestampGenerator(param string) (Generator, error) { return &TimestampGenerator{}, nil } -type WaitTimeoutGenerator struct { - waitTimeout time.Duration -} - -func (wtg *WaitTimeoutGenerator) Generate() []byte { - time.Sleep(wtg.waitTimeout) - return []byte{} -} - -func (wtg *WaitTimeoutGenerator) Size() int { - return 0 -} - -func newWaitTimeoutGenerator(param string) (Generator, error) { - timeout, err := strconv.Atoi(param) - if err != nil { - return nil, fmt.Errorf("timeout parse int: %w", err) - } - - if timeout > 5000 { - return nil, fmt.Errorf("timeout must be less than 5000ms") - } - - return &WaitTimeoutGenerator{ - waitTimeout: time.Duration(timeout) * time.Millisecond, - }, nil -} - type PacketCounterGenerator struct { } diff --git a/device/awg/tag_generator_test.go b/device/awg/tag_generator_test.go index 4950b33..43efa67 100644 --- a/device/awg/tag_generator_test.go +++ b/device/awg/tag_generator_test.go @@ -8,7 +8,9 @@ import ( "github.com/stretchr/testify/require" ) -func Test_newBytesGenerator(t *testing.T) { +func TestNewBytesGenerator(t *testing.T) { + t.Parallel() + type args struct { param string } @@ -63,6 +65,8 @@ func Test_newBytesGenerator(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := newBytesGenerator(tt.args.param) if tt.wantErr != nil { @@ -80,7 +84,9 @@ func Test_newBytesGenerator(t *testing.T) { } } -func Test_newRandomPacketGenerator(t *testing.T) { +func TestNewRandomBytesGenerator(t *testing.T) { + t.Parallel() + type args struct { param string } @@ -117,9 +123,134 @@ func Test_newRandomPacketGenerator(t *testing.T) { }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := newRandomPacketGenerator(tt.args.param) + t.Parallel() + + got, err := newRandomBytesGenerator(tt.args.param) + if tt.wantErr != nil { + require.ErrorAs(t, err, &tt.wantErr) + require.Nil(t, got) + return + } + + require.Nil(t, err) + require.NotNil(t, got) + first := got.Generate() + + second := got.Generate() + require.NotEqual(t, first, second) + }) + } +} + +func TestNewRandomASCIIGenerator(t *testing.T) { + t.Parallel() + + type args struct { + param string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "empty", + args: args{ + param: "", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "not an int", + args: args{ + param: "x", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "too large", + args: args{ + param: "1001", + }, + wantErr: fmt.Errorf("random packet size must be less than 1000"), + }, + { + name: "valid", + args: args{ + param: "12", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := newRandomASCIIGenerator(tt.args.param) + if tt.wantErr != nil { + require.ErrorAs(t, err, &tt.wantErr) + require.Nil(t, got) + return + } + + require.Nil(t, err) + require.NotNil(t, got) + first := got.Generate() + + second := got.Generate() + require.NotEqual(t, first, second) + }) + } +} + +func TestNewRandomDigitGenerator(t *testing.T) { + t.Parallel() + + type args struct { + param string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "empty", + args: args{ + param: "", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "not an int", + args: args{ + param: "x", + }, + wantErr: fmt.Errorf("parse int"), + }, + { + name: "too large", + args: args{ + param: "1001", + }, + wantErr: fmt.Errorf("random packet size must be less than 1000"), + }, + { + name: "valid", + args: args{ + param: "12", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := newRandomDigitGenerator(tt.args.param) if tt.wantErr != nil { require.ErrorAs(t, err, &tt.wantErr) require.Nil(t, got) @@ -137,6 +268,8 @@ func Test_newRandomPacketGenerator(t *testing.T) { } func TestPacketCounterGenerator(t *testing.T) { + t.Parallel() + tests := []struct { name string param string @@ -155,7 +288,6 @@ func TestPacketCounterGenerator(t *testing.T) { } for _, tc := range tests { - tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/device/awg/tag_parser.go b/device/awg/tag_parser.go index 2b09226..06ba49b 100644 --- a/device/awg/tag_parser.go +++ b/device/awg/tag_parser.go @@ -12,21 +12,21 @@ type IpcFields struct{ Key, Value string } type EnumTag string const ( - BytesEnumTag EnumTag = "b" - CounterEnumTag EnumTag = "c" - TimestampEnumTag EnumTag = "t" - RandomBytesEnumTag EnumTag = "r" - WaitTimeoutEnumTag EnumTag = "wt" - WaitResponseEnumTag EnumTag = "wr" + BytesEnumTag EnumTag = "b" + CounterEnumTag EnumTag = "c" + TimestampEnumTag EnumTag = "t" + RandomBytesEnumTag EnumTag = "r" + RandomASCIIEnumTag EnumTag = "rc" + RandomDigitEnumTag EnumTag = "rd" ) var generatorCreator = map[EnumTag]newGenerator{ BytesEnumTag: newBytesGenerator, CounterEnumTag: newPacketCounterGenerator, TimestampEnumTag: newTimestampGenerator, - RandomBytesEnumTag: newRandomPacketGenerator, - WaitTimeoutEnumTag: newWaitTimeoutGenerator, - // WaitResponseEnumTag: newWaitResponseGenerator, + RandomBytesEnumTag: newRandomBytesGenerator, + RandomASCIIEnumTag: newRandomASCIIGenerator, + RandomDigitEnumTag: newRandomDigitGenerator, } // helper map to determine enumTags are unique @@ -55,7 +55,7 @@ func parseTag(input string) (Tag, error) { return tag, nil } -func Parse(name, input string) (TagJunkPacketGenerator, error) { +func ParseTagJunkGenerator(name, input string) (TagJunkPacketGenerator, error) { inputSlice := strings.Split(input, "<") if len(inputSlice) <= 1 { return TagJunkPacketGenerator{}, fmt.Errorf("empty input: %s", input) diff --git a/device/awg/tag_parser_test.go b/device/awg/tag_parser_test.go index 8f828ec..3229cee 100644 --- a/device/awg/tag_parser_test.go +++ b/device/awg/tag_parser_test.go @@ -64,7 +64,7 @@ func TestParse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := Parse(tt.args.name, tt.args.input) + _, err := ParseTagJunkGenerator(tt.args.name, tt.args.input) // TODO: ErrorAs doesn't work as you think if tt.wantErr != nil { diff --git a/device/cookie.go b/device/cookie.go index a093c8b..6a0463c 100644 --- a/device/cookie.go +++ b/device/cookie.go @@ -118,6 +118,7 @@ func (st *CookieChecker) CreateReply( msg []byte, recv uint32, src []byte, + msgType uint32, ) (*MessageCookieReply, error) { st.RLock() @@ -153,7 +154,7 @@ func (st *CookieChecker) CreateReply( smac1 := smac2 - blake2s.Size128 reply := new(MessageCookieReply) - reply.Type = MessageCookieReplyType + reply.Type = msgType reply.Receiver = recv _, err := rand.Read(reply.Nonce[:]) diff --git a/device/cookie_test.go b/device/cookie_test.go index c937290..e5a2bd4 100644 --- a/device/cookie_test.go +++ b/device/cookie_test.go @@ -99,7 +99,7 @@ func TestCookieMAC1(t *testing.T) { 0x8c, 0xe1, 0xe8, 0xfa, 0x67, 0x20, 0x80, 0x6d, } generator.AddMacs(msg) - reply, err := checker.CreateReply(msg, 1377, src) + reply, err := checker.CreateReply(msg, 1377, src, DefaultMessageCookieReplyType) if err != nil { t.Fatal("Failed to create cookie reply:", err) } diff --git a/device/device.go b/device/device.go index 1829352..46cf04e 100644 --- a/device/device.go +++ b/device/device.go @@ -6,7 +6,9 @@ package device import ( + "encoding/binary" "errors" + "fmt" "runtime" "sync" "sync/atomic" @@ -578,6 +580,7 @@ func (device *Device) BindClose() error { device.net.Unlock() return err } + func (device *Device) isAWG() bool { return device.version >= VersionAwg } @@ -591,171 +594,123 @@ func (device *Device) resetProtocol() { } func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { - if !tempAwg.ASecCfg.IsSet && !tempAwg.HandshakeHandler.IsSet { + if !tempAwg.Cfg.IsSet && !tempAwg.HandshakeHandler.IsSet { return nil } var errs []error - isASecOn := false - device.awg.ASecMux.Lock() - if tempAwg.ASecCfg.JunkPacketCount < 0 { + isAwgOn := false + device.awg.Mux.Lock() + if tempAwg.Cfg.JunkPacketCount < 0 { errs = append(errs, ipcErrorf( ipc.IpcErrorInvalid, "JunkPacketCount should be non negative", ), ) } - device.awg.ASecCfg.JunkPacketCount = tempAwg.ASecCfg.JunkPacketCount - if tempAwg.ASecCfg.JunkPacketCount != 0 { - isASecOn = true + device.awg.Cfg.JunkPacketCount = tempAwg.Cfg.JunkPacketCount + if tempAwg.Cfg.JunkPacketCount != 0 { + isAwgOn = true } - device.awg.ASecCfg.JunkPacketMinSize = tempAwg.ASecCfg.JunkPacketMinSize - if tempAwg.ASecCfg.JunkPacketMinSize != 0 { - isASecOn = true + device.awg.Cfg.JunkPacketMinSize = tempAwg.Cfg.JunkPacketMinSize + if tempAwg.Cfg.JunkPacketMinSize != 0 { + isAwgOn = true } - if device.awg.ASecCfg.JunkPacketCount > 0 && - tempAwg.ASecCfg.JunkPacketMaxSize == tempAwg.ASecCfg.JunkPacketMinSize { + if device.awg.Cfg.JunkPacketCount > 0 && + tempAwg.Cfg.JunkPacketMaxSize == tempAwg.Cfg.JunkPacketMinSize { - tempAwg.ASecCfg.JunkPacketMaxSize++ // to make rand gen work + tempAwg.Cfg.JunkPacketMaxSize++ // to make rand gen work } - if tempAwg.ASecCfg.JunkPacketMaxSize >= MaxSegmentSize { - device.awg.ASecCfg.JunkPacketMinSize = 0 - device.awg.ASecCfg.JunkPacketMaxSize = 1 + if tempAwg.Cfg.JunkPacketMaxSize >= MaxSegmentSize { + device.awg.Cfg.JunkPacketMinSize = 0 + device.awg.Cfg.JunkPacketMaxSize = 1 errs = append(errs, ipcErrorf( ipc.IpcErrorInvalid, "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", - tempAwg.ASecCfg.JunkPacketMaxSize, + tempAwg.Cfg.JunkPacketMaxSize, MaxSegmentSize, )) - } else if tempAwg.ASecCfg.JunkPacketMaxSize < tempAwg.ASecCfg.JunkPacketMinSize { + } else if tempAwg.Cfg.JunkPacketMaxSize < tempAwg.Cfg.JunkPacketMinSize { errs = append(errs, ipcErrorf( ipc.IpcErrorInvalid, "maxSize: %d; should be greater than minSize: %d", - tempAwg.ASecCfg.JunkPacketMaxSize, - tempAwg.ASecCfg.JunkPacketMinSize, + tempAwg.Cfg.JunkPacketMaxSize, + tempAwg.Cfg.JunkPacketMinSize, )) } else { - device.awg.ASecCfg.JunkPacketMaxSize = tempAwg.ASecCfg.JunkPacketMaxSize + device.awg.Cfg.JunkPacketMaxSize = tempAwg.Cfg.JunkPacketMaxSize } - if tempAwg.ASecCfg.JunkPacketMaxSize != 0 { - isASecOn = true + if tempAwg.Cfg.JunkPacketMaxSize != 0 { + isAwgOn = true } - newInitSize := MessageInitiationSize + tempAwg.ASecCfg.InitHeaderJunkSize + magicHeaders := make([]awg.MagicHeader, 4) - if newInitSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( + if len(tempAwg.Cfg.MagicHeaders.Values) != 4 { + return ipcErrorf( ipc.IpcErrorInvalid, - `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.ASecCfg.InitHeaderJunkSize, - MaxSegmentSize, - ), + "magic headers should have 4 values; got: %d", + len(tempAwg.Cfg.MagicHeaders.Values), ) - } else { - device.awg.ASecCfg.InitHeaderJunkSize = tempAwg.ASecCfg.InitHeaderJunkSize } - if tempAwg.ASecCfg.InitHeaderJunkSize != 0 { - isASecOn = true - } - - newResponseSize := MessageResponseSize + tempAwg.ASecCfg.ResponseHeaderJunkSize - - if newResponseSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.ASecCfg.ResponseHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.ASecCfg.ResponseHeaderJunkSize = tempAwg.ASecCfg.ResponseHeaderJunkSize - } - - if tempAwg.ASecCfg.ResponseHeaderJunkSize != 0 { - isASecOn = true - } - - newCookieSize := MessageCookieReplySize + tempAwg.ASecCfg.CookieReplyHeaderJunkSize - - if newCookieSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `cookie reply size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.ASecCfg.CookieReplyHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.ASecCfg.CookieReplyHeaderJunkSize = tempAwg.ASecCfg.CookieReplyHeaderJunkSize - } - - if tempAwg.ASecCfg.CookieReplyHeaderJunkSize != 0 { - isASecOn = true - } - - newTransportSize := MessageTransportSize + tempAwg.ASecCfg.TransportHeaderJunkSize - - if newTransportSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `transport size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.ASecCfg.TransportHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.ASecCfg.TransportHeaderJunkSize = tempAwg.ASecCfg.TransportHeaderJunkSize - } - - if tempAwg.ASecCfg.TransportHeaderJunkSize != 0 { - isASecOn = true - } - - if tempAwg.ASecCfg.InitPacketMagicHeader > 4 { - isASecOn = true + if tempAwg.Cfg.MagicHeaders.Values[0].Min > 4 { + isAwgOn = true device.log.Verbosef("UAPI: Updating init_packet_magic_header") - device.awg.ASecCfg.InitPacketMagicHeader = tempAwg.ASecCfg.InitPacketMagicHeader - MessageInitiationType = device.awg.ASecCfg.InitPacketMagicHeader + magicHeaders[0] = tempAwg.Cfg.MagicHeaders.Values[0] + + MessageInitiationType = magicHeaders[0].Min } else { device.log.Verbosef("UAPI: Using default init type") MessageInitiationType = DefaultMessageInitiationType + magicHeaders[0] = awg.NewMagicHeaderSameValue(DefaultMessageInitiationType) } - if tempAwg.ASecCfg.ResponsePacketMagicHeader > 4 { - isASecOn = true + if tempAwg.Cfg.MagicHeaders.Values[1].Min > 4 { + isAwgOn = true + device.log.Verbosef("UAPI: Updating response_packet_magic_header") - device.awg.ASecCfg.ResponsePacketMagicHeader = tempAwg.ASecCfg.ResponsePacketMagicHeader - MessageResponseType = device.awg.ASecCfg.ResponsePacketMagicHeader + magicHeaders[1] = tempAwg.Cfg.MagicHeaders.Values[1] + MessageResponseType = magicHeaders[1].Min } else { device.log.Verbosef("UAPI: Using default response type") MessageResponseType = DefaultMessageResponseType + magicHeaders[1] = awg.NewMagicHeaderSameValue(DefaultMessageResponseType) } - if tempAwg.ASecCfg.UnderloadPacketMagicHeader > 4 { - isASecOn = true + if tempAwg.Cfg.MagicHeaders.Values[2].Min > 4 { + isAwgOn = true + device.log.Verbosef("UAPI: Updating underload_packet_magic_header") - device.awg.ASecCfg.UnderloadPacketMagicHeader = tempAwg.ASecCfg.UnderloadPacketMagicHeader - MessageCookieReplyType = device.awg.ASecCfg.UnderloadPacketMagicHeader + magicHeaders[2] = tempAwg.Cfg.MagicHeaders.Values[2] + MessageCookieReplyType = magicHeaders[2].Min } else { device.log.Verbosef("UAPI: Using default underload type") MessageCookieReplyType = DefaultMessageCookieReplyType + magicHeaders[2] = awg.NewMagicHeaderSameValue(DefaultMessageCookieReplyType) } - if tempAwg.ASecCfg.TransportPacketMagicHeader > 4 { - isASecOn = true + if tempAwg.Cfg.MagicHeaders.Values[3].Min > 4 { + isAwgOn = true + device.log.Verbosef("UAPI: Updating transport_packet_magic_header") - device.awg.ASecCfg.TransportPacketMagicHeader = tempAwg.ASecCfg.TransportPacketMagicHeader - MessageTransportType = device.awg.ASecCfg.TransportPacketMagicHeader + magicHeaders[3] = tempAwg.Cfg.MagicHeaders.Values[3] + MessageTransportType = magicHeaders[3].Min } else { device.log.Verbosef("UAPI: Using default transport type") MessageTransportType = DefaultMessageTransportType + magicHeaders[3] = awg.NewMagicHeaderSameValue(DefaultMessageTransportType) + } + + var err error + device.awg.Cfg.MagicHeaders, err = awg.NewMagicHeaders(magicHeaders) + if err != nil { + errs = append(errs, ipcErrorf(ipc.IpcErrorInvalid, "new magic headers: %w", err)) } isSameHeaderMap := map[uint32]struct{}{ @@ -778,6 +733,78 @@ func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { ) } + newInitSize := MessageInitiationSize + tempAwg.Cfg.InitHeaderJunkSize + + if newInitSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.Cfg.InitHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.Cfg.InitHeaderJunkSize = tempAwg.Cfg.InitHeaderJunkSize + } + + if tempAwg.Cfg.InitHeaderJunkSize != 0 { + isAwgOn = true + } + + newResponseSize := MessageResponseSize + tempAwg.Cfg.ResponseHeaderJunkSize + + if newResponseSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.Cfg.ResponseHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.Cfg.ResponseHeaderJunkSize = tempAwg.Cfg.ResponseHeaderJunkSize + } + + if tempAwg.Cfg.ResponseHeaderJunkSize != 0 { + isAwgOn = true + } + + newCookieSize := MessageCookieReplySize + tempAwg.Cfg.CookieReplyHeaderJunkSize + + if newCookieSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `cookie reply size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.Cfg.CookieReplyHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.Cfg.CookieReplyHeaderJunkSize = tempAwg.Cfg.CookieReplyHeaderJunkSize + } + + if tempAwg.Cfg.CookieReplyHeaderJunkSize != 0 { + isAwgOn = true + } + + newTransportSize := MessageTransportSize + tempAwg.Cfg.TransportHeaderJunkSize + + if newTransportSize >= MaxSegmentSize { + errs = append(errs, ipcErrorf( + ipc.IpcErrorInvalid, + `transport size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, + tempAwg.Cfg.TransportHeaderJunkSize, + MaxSegmentSize, + ), + ) + } else { + device.awg.Cfg.TransportHeaderJunkSize = tempAwg.Cfg.TransportHeaderJunkSize + } + + if tempAwg.Cfg.TransportHeaderJunkSize != 0 { + isAwgOn = true + } + isSameSizeMap := map[int]struct{}{ newInitSize: {}, newResponseSize: {}, @@ -797,10 +824,10 @@ func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { ) } else { msgTypeToJunkSize = map[uint32]int{ - MessageInitiationType: device.awg.ASecCfg.InitHeaderJunkSize, - MessageResponseType: device.awg.ASecCfg.ResponseHeaderJunkSize, - MessageCookieReplyType: device.awg.ASecCfg.CookieReplyHeaderJunkSize, - MessageTransportType: device.awg.ASecCfg.TransportHeaderJunkSize, + MessageInitiationType: device.awg.Cfg.InitHeaderJunkSize, + MessageResponseType: device.awg.Cfg.ResponseHeaderJunkSize, + MessageCookieReplyType: device.awg.Cfg.CookieReplyHeaderJunkSize, + MessageTransportType: device.awg.Cfg.TransportHeaderJunkSize, } packetSizeToMsgType = map[int]uint32{ @@ -811,12 +838,8 @@ func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { } } - device.awg.IsASecOn.SetTo(isASecOn) - var err error - device.awg.JunkCreator, err = awg.NewJunkCreator(device.awg.ASecCfg) - if err != nil { - errs = append(errs, err) - } + device.awg.IsOn.SetTo(isAwgOn) + device.awg.JunkCreator = awg.NewJunkCreator(device.awg.Cfg) if tempAwg.HandshakeHandler.IsSet { if err := tempAwg.HandshakeHandler.Validate(); err != nil { @@ -824,15 +847,91 @@ func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { ipc.IpcErrorInvalid, "handshake handler validate: %w", err)) } else { device.awg.HandshakeHandler = tempAwg.HandshakeHandler - device.awg.HandshakeHandler.ControlledJunk.DefaultJunkCount = tempAwg.ASecCfg.JunkPacketCount - device.awg.HandshakeHandler.SpecialJunk.DefaultJunkCount = tempAwg.ASecCfg.JunkPacketCount + device.awg.HandshakeHandler.SpecialJunk.DefaultJunkCount = tempAwg.Cfg.JunkPacketCount device.version = VersionAwgSpecialHandshake } } else { device.version = VersionAwg } - device.awg.ASecMux.Unlock() + device.awg.Mux.Unlock() return errors.Join(errs...) } + +func (device *Device) ProcessAWGPacket(size int, packet *[]byte, buffer *[MaxMessageSize]byte) (uint32, error) { + // TODO: + // if awg.WaitResponse.ShouldWait.IsSet() { + // awg.WaitResponse.Channel <- struct{}{} + // } + + expectedMsgType, isKnownSize := packetSizeToMsgType[size] + if !isKnownSize { + msgType, err := device.handleTransport(size, packet, buffer) + + if err != nil { + return 0, fmt.Errorf("handle transport: %w", err) + } + + return msgType, nil + } + + junkSize := msgTypeToJunkSize[expectedMsgType] + + // transport size can align with other header types; + // making sure we have the right actualMsgType + actualMsgType, err := device.getMsgType(packet, junkSize) + if err != nil { + return 0, fmt.Errorf("get msg type: %w", err) + } + + if actualMsgType == expectedMsgType { + *packet = (*packet)[junkSize:] + return actualMsgType, nil + } + + device.log.Verbosef("awg: transport packet lined up with another msg type") + + msgType, err := device.handleTransport(size, packet, buffer) + if err != nil { + return 0, fmt.Errorf("handle transport: %w", err) + } + + return msgType, nil +} + +func (device *Device) getMsgType(packet *[]byte, junkSize int) (uint32, error) { + msgTypeValue := binary.LittleEndian.Uint32((*packet)[junkSize : junkSize+4]) + msgType, err := device.awg.GetMagicHeaderMinFor(msgTypeValue) + + if err != nil { + return 0, fmt.Errorf("get magic header min: %w", err) + } + + return msgType, nil +} + +func (device *Device) handleTransport(size int, packet *[]byte, buffer *[MaxMessageSize]byte) (uint32, error) { + junkSize := device.awg.Cfg.TransportHeaderJunkSize + + msgType, err := device.getMsgType(packet, junkSize) + if err != nil { + return 0, fmt.Errorf("get msg type: %w", err) + } + + if msgType != MessageTransportType { + // probably a junk packet + return 0, fmt.Errorf("Received message with unknown type: %d", msgType) + } + + if junkSize > 0 { + // remove junk from buffer by shifting the packet + // this buffer is also used for decryption, so it needs to be corrected + copy((*buffer)[:size], (*packet)[junkSize:]) + size -= junkSize + // need to reinitialize packet as well + (*packet) = (*packet)[:size] + } + + return msgType, nil +} diff --git a/device/device_test.go b/device/device_test.go index 5824cf9..2f66185 100644 --- a/device/device_test.go +++ b/device/device_test.go @@ -232,14 +232,14 @@ func TestAWGDevicePing(t *testing.T) { "jc", "5", "jmin", "500", "jmax", "1000", - "s1", "30", - "s2", "40", - "s3", "50", - "s4", "5", - "h1", "123456", - "h2", "67543", - "h3", "123123", - "h4", "32345", + "s1", "15", + "s2", "18", + "s3", "20", + "s4", "25", + "h1", "123456-123500", + "h2", "67543-67550", + "h3", "123123-123200", + "h4", "32345-32350", ) t.Run("ping 1.0.0.1", func(t *testing.T) { pair.Send(t, Ping, nil) @@ -264,12 +264,10 @@ func TestAWGHandshakeDevicePing(t *testing.T) { goroutineLeakCheck(t) pair := genTestPair(t, true, - "i1", "", - "i2", "", - "j1", "", - "j2", "", - "j3", "", - "itime", "60", + "i1", "", + "i2", "", + "i3", "", + "i4", "", // "jc", "1", // "jmin", "500", // "jmax", "1000", diff --git a/device/noise-protocol.go b/device/noise-protocol.go index f637b24..6e6fe58 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -205,12 +205,22 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e handshake.mixHash(handshake.remoteStatic[:]) - device.awg.ASecMux.RLock() + msgType := DefaultMessageInitiationType + if device.isAWG() { + device.awg.Mux.RLock() + msgType, err = device.awg.GetMsgType(DefaultMessageInitiationType) + if err != nil { + device.awg.Mux.RUnlock() + return nil, fmt.Errorf("get message type: %w", err) + } + + device.awg.Mux.RUnlock() + } + msg := MessageInitiation{ - Type: MessageInitiationType, + Type: msgType, Ephemeral: handshake.localEphemeral.publicKey(), } - device.awg.ASecMux.RUnlock() handshake.mixKey(msg.Ephemeral[:]) handshake.mixHash(msg.Ephemeral[:]) @@ -264,12 +274,13 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { chainKey [blake2s.Size]byte ) - device.awg.ASecMux.RLock() + device.awg.Mux.RLock() + if msg.Type != MessageInitiationType { - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() return nil } - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() device.staticIdentity.RLock() defer device.staticIdentity.RUnlock() @@ -384,9 +395,19 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } var msg MessageResponse - device.awg.ASecMux.RLock() - msg.Type = MessageResponseType - device.awg.ASecMux.RUnlock() + if device.isAWG() { + device.awg.Mux.RLock() + msg.Type, err = device.awg.GetMsgType(DefaultMessageResponseType) + if err != nil { + device.awg.Mux.RUnlock() + return nil, fmt.Errorf("get message type: %w", err) + } + + device.awg.Mux.RUnlock() + } else { + msg.Type = DefaultMessageResponseType + } + msg.Sender = handshake.localIndex msg.Receiver = handshake.remoteIndex @@ -436,12 +457,13 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { - device.awg.ASecMux.RLock() + device.awg.Mux.RLock() + if msg.Type != MessageResponseType { - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() return nil } - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() // lookup handshake by receiver diff --git a/device/receive.go b/device/receive.go index 6daba0d..4c34799 100644 --- a/device/receive.go +++ b/device/receive.go @@ -129,7 +129,7 @@ func (device *Device) RoutineReceiveIncoming( } deathSpiral = 0 - device.awg.ASecMux.RLock() + device.awg.Mux.RLock() // handle each packet in the batch for i, size := range sizes[:count] { if size < MinMessageSize { @@ -140,37 +140,11 @@ func (device *Device) RoutineReceiveIncoming( packet := bufsArrs[i][:size] var msgType uint32 if device.isAWG() { - // TODO: - // if awg.WaitResponse.ShouldWait.IsSet() { - // awg.WaitResponse.Channel <- struct{}{} - // } + msgType, err = device.ProcessAWGPacket(size, &packet, bufsArrs[i]) - if assumedMsgType, ok := packetSizeToMsgType[size]; ok { - junkSize := msgTypeToJunkSize[assumedMsgType] - // transport size can align with other header types; - // making sure we have the right msgType - msgType = binary.LittleEndian.Uint32(packet[junkSize : junkSize+4]) - if msgType == assumedMsgType { - packet = packet[junkSize:] - } else { - device.log.Verbosef("transport packet lined up with another msg type") - msgType = binary.LittleEndian.Uint32(packet[:4]) - } - } else { - transportJunkSize := device.awg.ASecCfg.TransportHeaderJunkSize - msgType = binary.LittleEndian.Uint32(packet[transportJunkSize : transportJunkSize+4]) - if msgType != MessageTransportType { - // probably a junk packet - device.log.Verbosef("aSec: Received message with unknown type: %d", msgType) - continue - } - - // remove junk from bufsArrs by shifting the packet - // this buffer is also used for decryption, so it needs to be corrected - copy(bufsArrs[i][:size], packet[transportJunkSize:]) - size -= transportJunkSize - // need to reinitialize packet as well - packet = packet[:size] + if err != nil { + device.log.Verbosef("awg: process packet: %v", err) + continue } } else { msgType = binary.LittleEndian.Uint32(packet[:4]) @@ -259,7 +233,7 @@ func (device *Device) RoutineReceiveIncoming( default: } } - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() for peer, elemsContainer := range elemsByPeer { if peer.isRunning.Load() { peer.queue.inbound.c <- elemsContainer @@ -318,7 +292,7 @@ func (device *Device) RoutineHandshake(id int) { for elem := range device.queue.handshake.c { - device.awg.ASecMux.RLock() + device.awg.Mux.RLock() // handle cookie fields and ratelimiting @@ -405,6 +379,9 @@ func (device *Device) RoutineHandshake(id int) { goto skip } + // have to reassign msgType for ranged msgType to work + msg.Type = elem.msgType + // consume initiation peer := device.ConsumeMessageInitiation(&msg) if peer == nil { @@ -437,6 +414,9 @@ func (device *Device) RoutineHandshake(id int) { goto skip } + // have to reassign msgType for ranged msgType to work + msg.Type = elem.msgType + // consume response peer := device.ConsumeMessageResponse(&msg) @@ -470,7 +450,7 @@ func (device *Device) RoutineHandshake(id int) { peer.SendKeepalive() } skip: - device.awg.ASecMux.RUnlock() + device.awg.Mux.RUnlock() device.PutMessageBuffer(elem.buffer) } } diff --git a/device/send.go b/device/send.go index 04ca2ad..0861a04 100644 --- a/device/send.go +++ b/device/send.go @@ -130,29 +130,19 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { if peer.device.version >= VersionAwg { var junks [][]byte if peer.device.version == VersionAwgSpecialHandshake { - peer.device.awg.ASecMux.RLock() + peer.device.awg.Mux.RLock() // set junks depending on packet type junks = peer.device.awg.HandshakeHandler.GenerateSpecialJunk() - if junks == nil { - junks = peer.device.awg.HandshakeHandler.GenerateControlledJunk() - if junks != nil { - peer.device.log.Verbosef("%v - Controlled junks sent", peer) - } - } else { + if junks != nil { peer.device.log.Verbosef("%v - Special junks sent", peer) } - peer.device.awg.ASecMux.RUnlock() + peer.device.awg.Mux.RUnlock() } else { - junks = make([][]byte, 0, peer.device.awg.ASecCfg.JunkPacketCount) - } - peer.device.awg.ASecMux.RLock() - err := peer.device.awg.JunkCreator.CreateJunkPackets(&junks) - peer.device.awg.ASecMux.RUnlock() - - if err != nil { - peer.device.log.Errorf("%v - %v", peer, err) - return err + junks = make([][]byte, 0, peer.device.awg.Cfg.JunkPacketCount) } + peer.device.awg.Mux.RLock() + peer.device.awg.JunkCreator.CreateJunkPackets(&junks) + peer.device.awg.Mux.RUnlock() if len(junks) > 0 { err = peer.SendBuffers(junks) @@ -242,10 +232,24 @@ func (device *Device) SendHandshakeCookie( device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8]) + msgType := DefaultMessageCookieReplyType + if device.isAWG() { + device.awg.Mux.RLock() + + var err error + msgType, err = device.awg.GetMsgType(DefaultMessageCookieReplyType) + device.awg.Mux.RUnlock() + if err != nil { + device.log.Errorf("Get message type for cookie reply: %v", err) + return err + } + } + reply, err := device.cookieChecker.CreateReply( initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes(), + msgType, ) if err != nil { device.log.Errorf("Failed to create cookie reply: %v", err) @@ -528,7 +532,20 @@ func (device *Device) RoutineEncryption(id int) { fieldReceiver := header[4:8] fieldNonce := header[8:16] - binary.LittleEndian.PutUint32(fieldType, MessageTransportType) + msgType := DefaultMessageTransportType + if device.isAWG() { + device.awg.Mux.RLock() + + var err error + msgType, err = device.awg.GetMsgType(DefaultMessageTransportType) + device.awg.Mux.RUnlock() + if err != nil { + device.log.Errorf("get message type for transport: %v", err) + continue + } + } + + binary.LittleEndian.PutUint32(fieldType, msgType) binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) diff --git a/device/uapi.go b/device/uapi.go index e9f962a..6c4be05 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -99,51 +99,42 @@ func (device *Device) IpcGetOperation(w io.Writer) error { } if device.isAWG() { - if device.awg.ASecCfg.JunkPacketCount != 0 { - sendf("jc=%d", device.awg.ASecCfg.JunkPacketCount) + if device.awg.Cfg.JunkPacketCount != 0 { + sendf("jc=%d", device.awg.Cfg.JunkPacketCount) } - if device.awg.ASecCfg.JunkPacketMinSize != 0 { - sendf("jmin=%d", device.awg.ASecCfg.JunkPacketMinSize) + if device.awg.Cfg.JunkPacketMinSize != 0 { + sendf("jmin=%d", device.awg.Cfg.JunkPacketMinSize) } - if device.awg.ASecCfg.JunkPacketMaxSize != 0 { - sendf("jmax=%d", device.awg.ASecCfg.JunkPacketMaxSize) + if device.awg.Cfg.JunkPacketMaxSize != 0 { + sendf("jmax=%d", device.awg.Cfg.JunkPacketMaxSize) } - if device.awg.ASecCfg.InitHeaderJunkSize != 0 { - sendf("s1=%d", device.awg.ASecCfg.InitHeaderJunkSize) + if device.awg.Cfg.InitHeaderJunkSize != 0 { + sendf("s1=%d", device.awg.Cfg.InitHeaderJunkSize) } - if device.awg.ASecCfg.ResponseHeaderJunkSize != 0 { - sendf("s2=%d", device.awg.ASecCfg.ResponseHeaderJunkSize) + if device.awg.Cfg.ResponseHeaderJunkSize != 0 { + sendf("s2=%d", device.awg.Cfg.ResponseHeaderJunkSize) } - if device.awg.ASecCfg.CookieReplyHeaderJunkSize != 0 { - sendf("s3=%d", device.awg.ASecCfg.CookieReplyHeaderJunkSize) + if device.awg.Cfg.CookieReplyHeaderJunkSize != 0 { + sendf("s3=%d", device.awg.Cfg.CookieReplyHeaderJunkSize) } - if device.awg.ASecCfg.TransportHeaderJunkSize != 0 { - sendf("s4=%d", device.awg.ASecCfg.TransportHeaderJunkSize) + if device.awg.Cfg.TransportHeaderJunkSize != 0 { + sendf("s4=%d", device.awg.Cfg.TransportHeaderJunkSize) } - if device.awg.ASecCfg.InitPacketMagicHeader != 0 { - sendf("h1=%d", device.awg.ASecCfg.InitPacketMagicHeader) - } - if device.awg.ASecCfg.ResponsePacketMagicHeader != 0 { - sendf("h2=%d", device.awg.ASecCfg.ResponsePacketMagicHeader) - } - if device.awg.ASecCfg.UnderloadPacketMagicHeader != 0 { - sendf("h3=%d", device.awg.ASecCfg.UnderloadPacketMagicHeader) - } - if device.awg.ASecCfg.TransportPacketMagicHeader != 0 { - sendf("h4=%d", device.awg.ASecCfg.TransportPacketMagicHeader) + for i, magicHeader := range device.awg.Cfg.MagicHeaders.Values { + if magicHeader.Min > 4 { + if magicHeader.Min == magicHeader.Max { + sendf("h%d=%d", i+1, magicHeader.Min) + continue + } + + sendf("h%d=%d-%d", i+1, magicHeader.Min, magicHeader.Max) + } } specialJunkIpcFields := device.awg.HandshakeHandler.SpecialJunk.IpcGetFields() for _, field := range specialJunkIpcFields { sendf("%s=%s", field.Key, field.Value) } - controlledJunkIpcFields := device.awg.HandshakeHandler.ControlledJunk.IpcGetFields() - for _, field := range controlledJunkIpcFields { - sendf("%s=%s", field.Key, field.Value) - } - if device.awg.HandshakeHandler.ITimeout != 0 { - sendf("itime=%d", device.awg.HandshakeHandler.ITimeout/time.Second) - } } for _, peer := range device.peers.keyMap { @@ -200,6 +191,8 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { deviceConfig := true tempAwg := awg.Protocol{} + tempAwg.Cfg.MagicHeaders.Values = make([]awg.MagicHeader, 4) + scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() @@ -312,8 +305,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_count %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_count") - tempAwg.ASecCfg.JunkPacketCount = junkPacketCount - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.JunkPacketCount = junkPacketCount + tempAwg.Cfg.IsSet = true case "jmin": junkPacketMinSize, err := strconv.Atoi(value) @@ -321,8 +314,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_min_size %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_min_size") - tempAwg.ASecCfg.JunkPacketMinSize = junkPacketMinSize - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.JunkPacketMinSize = junkPacketMinSize + tempAwg.Cfg.IsSet = true case "jmax": junkPacketMaxSize, err := strconv.Atoi(value) @@ -330,8 +323,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_max_size %w", err) } device.log.Verbosef("UAPI: Updating junk_packet_max_size") - tempAwg.ASecCfg.JunkPacketMaxSize = junkPacketMaxSize - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.JunkPacketMaxSize = junkPacketMaxSize + tempAwg.Cfg.IsSet = true case "s1": initPacketJunkSize, err := strconv.Atoi(value) @@ -339,8 +332,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse init_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating init_packet_junk_size") - tempAwg.ASecCfg.InitHeaderJunkSize = initPacketJunkSize - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.InitHeaderJunkSize = initPacketJunkSize + tempAwg.Cfg.IsSet = true case "s2": responsePacketJunkSize, err := strconv.Atoi(value) @@ -348,8 +341,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse response_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating response_packet_junk_size") - tempAwg.ASecCfg.ResponseHeaderJunkSize = responsePacketJunkSize - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.ResponseHeaderJunkSize = responsePacketJunkSize + tempAwg.Cfg.IsSet = true case "s3": cookieReplyPacketJunkSize, err := strconv.Atoi(value) @@ -357,8 +350,8 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse cookie_reply_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating cookie_reply_packet_junk_size") - tempAwg.ASecCfg.CookieReplyHeaderJunkSize = cookieReplyPacketJunkSize - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.CookieReplyHeaderJunkSize = cookieReplyPacketJunkSize + tempAwg.Cfg.IsSet = true case "s4": transportPacketJunkSize, err := strconv.Atoi(value) @@ -366,81 +359,53 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) return ipcErrorf(ipc.IpcErrorInvalid, "parse transport_packet_junk_size %w", err) } device.log.Verbosef("UAPI: Updating transport_packet_junk_size") - tempAwg.ASecCfg.TransportHeaderJunkSize = transportPacketJunkSize - tempAwg.ASecCfg.IsSet = true - + tempAwg.Cfg.TransportHeaderJunkSize = transportPacketJunkSize + tempAwg.Cfg.IsSet = true case "h1": - initPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + initMagicHeader, err := awg.ParseMagicHeader(key, value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse init_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) } - tempAwg.ASecCfg.InitPacketMagicHeader = uint32(initPacketMagicHeader) - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.MagicHeaders.Values[0] = initMagicHeader + tempAwg.Cfg.IsSet = true case "h2": - responsePacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + responseMagicHeader, err := awg.ParseMagicHeader(key, value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse response_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) } - tempAwg.ASecCfg.ResponsePacketMagicHeader = uint32(responsePacketMagicHeader) - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.MagicHeaders.Values[1] = responseMagicHeader + tempAwg.Cfg.IsSet = true case "h3": - underloadPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + cookieReplyMagicHeader, err := awg.ParseMagicHeader(key, value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse underload_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) } - tempAwg.ASecCfg.UnderloadPacketMagicHeader = uint32(underloadPacketMagicHeader) - tempAwg.ASecCfg.IsSet = true + tempAwg.Cfg.MagicHeaders.Values[2] = cookieReplyMagicHeader + tempAwg.Cfg.IsSet = true case "h4": - transportPacketMagicHeader, err := strconv.ParseUint(value, 10, 32) + transportMagicHeader, err := awg.ParseMagicHeader(key, value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse transport_packet_magic_header %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) } - tempAwg.ASecCfg.TransportPacketMagicHeader = uint32(transportPacketMagicHeader) - tempAwg.ASecCfg.IsSet = true + + tempAwg.Cfg.MagicHeaders.Values[3] = transportMagicHeader + tempAwg.Cfg.IsSet = true case "i1", "i2", "i3", "i4", "i5": if len(value) == 0 { device.log.Verbosef("UAPI: received empty %s", key) return nil } - generators, err := awg.Parse(key, value) + generators, err := awg.ParseTagJunkGenerator(key, value) if err != nil { return ipcErrorf(ipc.IpcErrorInvalid, "invalid %s: %w", key, err) } device.log.Verbosef("UAPI: Updating %s", key) tempAwg.HandshakeHandler.SpecialJunk.AppendGenerator(generators) tempAwg.HandshakeHandler.IsSet = true - case "j1", "j2", "j3": - if len(value) == 0 { - device.log.Verbosef("UAPI: received empty %s", key) - return nil - } - - generators, err := awg.Parse(key, value) - if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "invalid %s: %w", key, err) - } - device.log.Verbosef("UAPI: Updating %s", key) - - tempAwg.HandshakeHandler.ControlledJunk.AppendGenerator(generators) - tempAwg.HandshakeHandler.IsSet = true - case "itime": - if len(value) == 0 { - device.log.Verbosef("UAPI: received empty itime") - return nil - } - - itime, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse itime %w", err) - } - device.log.Verbosef("UAPI: Updating itime") - - tempAwg.HandshakeHandler.ITimeout = time.Duration(itime) * time.Second - tempAwg.HandshakeHandler.IsSet = true default: return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) } diff --git a/go.mod b/go.mod index 5e5f34d..8c4372d 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.24.4 require ( github.com/stretchr/testify v1.10.0 github.com/tevino/abool v1.2.0 - github.com/tevino/abool/v2 v2.1.0 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.39.0 + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 golang.org/x/net v0.41.0 golang.org/x/sys v0.33.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 diff --git a/go.sum b/go.sum index 6b8f36b..3d8b3c2 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,18 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= -github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= -github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= @@ -34,7 +28,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= -gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= -gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= -gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f h1:zmc4cHEcCudRt2O8VsCW7nYLfAsbVY2i910/DAop1TM= -gvisor.dev/gvisor v0.0.0-20250606233247-e3c4c4cad86f/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= From f8eafb781b99db38ef49be66bb4353e94cccd43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Nov 2022 15:29:08 +0800 Subject: [PATCH 107/173] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e460293..c7b1915 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ wireguard-go +/.idea/ +.DS_Store \ No newline at end of file From e4aedc6f6e59e1827c7146cc8dc6947b91aff55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 9 Dec 2023 16:59:55 +0800 Subject: [PATCH 108/173] Add remove unused script --- remove-unused.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 remove-unused.sh diff --git a/remove-unused.sh b/remove-unused.sh new file mode 100755 index 0000000..43f3f1b --- /dev/null +++ b/remove-unused.sh @@ -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" From 177dea98065e796f9b0a0c96cdf012e5082813bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 15 Sep 2025 18:12:04 +0800 Subject: [PATCH 109/173] Remove unused --- .github/workflows/test.yml | 68 -- README.md | 77 -- conn/bind_std_test.go | 250 ------ conn/bindtest/bindtest.go | 137 ---- conn/conn_test.go | 24 - conn/control_linux_test.go | 266 ------- device/allowedips_rand_test.go | 141 ---- device/allowedips_test.go | 247 ------ device/bind_test.go | 56 -- device/cookie_test.go | 190 ----- device/device_test.go | 476 ------------ device/endpoint_test.go | 49 -- device/kdf_test.go | 85 --- device/noise_test.go | 207 ----- device/pools_test.go | 140 ---- device/race_disabled_test.go | 10 - device/race_enabled_test.go | 10 - format_test.go | 51 -- go.mod | 6 - go.sum | 6 - ipc/namedpipe/namedpipe_test.go | 674 ---------------- main.go | 268 ------- main_windows.go | 99 --- ratelimiter/ratelimiter_test.go | 119 --- replay/replay_test.go | 119 --- tai64n/tai64n_test.go | 40 - tests/netns.sh | 425 ----------- tun/alignment_windows_test.go | 67 -- tun/checksum_amd64_test.go | 45 -- tun/checksum_generic_test.go | 26 - tun/checksum_test.go | 619 --------------- tun/netstack/examples/http_client.go | 54 -- tun/netstack/examples/http_server.go | 51 -- tun/netstack/examples/ping_client.go | 75 -- tun/netstack/tun.go | 1055 -------------------------- tun/offload_linux_test.go | 764 ------------------- tun/offload_test.go | 95 --- tun/tuntest/tuntest.go | 155 ---- 38 files changed, 7246 deletions(-) delete mode 100644 .github/workflows/test.yml delete mode 100644 README.md delete mode 100644 conn/bind_std_test.go delete mode 100644 conn/bindtest/bindtest.go delete mode 100644 conn/conn_test.go delete mode 100644 conn/control_linux_test.go delete mode 100644 device/allowedips_rand_test.go delete mode 100644 device/allowedips_test.go delete mode 100644 device/bind_test.go delete mode 100644 device/cookie_test.go delete mode 100644 device/device_test.go delete mode 100644 device/endpoint_test.go delete mode 100644 device/kdf_test.go delete mode 100644 device/noise_test.go delete mode 100644 device/pools_test.go delete mode 100644 device/race_disabled_test.go delete mode 100644 device/race_enabled_test.go delete mode 100644 format_test.go delete mode 100644 ipc/namedpipe/namedpipe_test.go delete mode 100644 main.go delete mode 100644 main_windows.go delete mode 100644 ratelimiter/ratelimiter_test.go delete mode 100644 replay/replay_test.go delete mode 100644 tai64n/tai64n_test.go delete mode 100755 tests/netns.sh delete mode 100644 tun/alignment_windows_test.go delete mode 100644 tun/checksum_amd64_test.go delete mode 100644 tun/checksum_generic_test.go delete mode 100644 tun/checksum_test.go delete mode 100644 tun/netstack/examples/http_client.go delete mode 100644 tun/netstack/examples/http_server.go delete mode 100644 tun/netstack/examples/ping_client.go delete mode 100644 tun/netstack/tun.go delete mode 100644 tun/offload_linux_test.go delete mode 100644 tun/offload_test.go delete mode 100644 tun/tuntest/tuntest.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index a370995..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: CI - -on: - push: - branches: ["tailscale"] - pull_request: - branches: ["tailscale"] - -jobs: - build: - runs-on: ubuntu-22.04 - strategy: - matrix: - include: - - goos: linux - goarch: amd64 - - goos: linux - goarch: arm64 - - goos: linux - goarch: "386" - - goos: linux - goarch: loong64 - - goos: linux - goarch: arm - goarm: "5" - - goos: linux - goarch: arm - goarm: "7" - # macOS - - goos: darwin - goarch: amd64 - - goos: darwin - goarch: arm64 - # Windows - - goos: windows - goarch: amd64 - - goos: windows - goarch: arm64 - # BSDs - - goos: freebsd - goarch: amd64 - - goos: openbsd - goarch: amd64 - steps: - - name: checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: setup go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version-file: go.mod - - name: build - run: go build ./... - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: "0" - - test: - runs-on: ubuntu-22.04 - steps: - - name: checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: setup go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version-file: go.mod - - name: test - run: go test -race -v ./... diff --git a/README.md b/README.md deleted file mode 100644 index 074f7ec..0000000 --- a/README.md +++ /dev/null @@ -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. diff --git a/conn/bind_std_test.go b/conn/bind_std_test.go deleted file mode 100644 index 77af0d9..0000000 --- a/conn/bind_std_test.go +++ /dev/null @@ -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, 0, 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]) - } - } - }) - } -} diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go deleted file mode 100644 index 741b776..0000000 --- a/conn/bindtest/bindtest.go +++ /dev/null @@ -1,137 +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" - - "github.com/tailscale/wireguard-go/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, offset int) error { - for _, b := range bufs { - b = b[offset:] - 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 -} diff --git a/conn/conn_test.go b/conn/conn_test.go deleted file mode 100644 index c6194ee..0000000 --- a/conn/conn_test.go +++ /dev/null @@ -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) - } - }) -} diff --git a/conn/control_linux_test.go b/conn/control_linux_test.go deleted file mode 100644 index 3ca7d37..0000000 --- a/conn/control_linux_test.go +++ /dev/null @@ -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, controlSize) - - 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, controlSize) - - 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, unix.CmsgLen(0)) - 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, 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 - - 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, unix.CmsgSpace(unix.SizeofInet6Pktinfo)) - 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) - } - }) -} diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go deleted file mode 100644 index 07065c3..0000000 --- a/device/allowedips_rand_test.go +++ /dev/null @@ -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") - } -} diff --git a/device/allowedips_test.go b/device/allowedips_test.go deleted file mode 100644 index cde068e..0000000 --- a/device/allowedips_test.go +++ /dev/null @@ -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) -} diff --git a/device/bind_test.go b/device/bind_test.go deleted file mode 100644 index d64ca09..0000000 --- a/device/bind_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package device - -import ( - "errors" - - "github.com/tailscale/wireguard-go/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 -} diff --git a/device/cookie_test.go b/device/cookie_test.go deleted file mode 100644 index 4f1e50a..0000000 --- a/device/cookie_test.go +++ /dev/null @@ -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, - }) -} diff --git a/device/device_test.go b/device/device_test.go deleted file mode 100644 index e443421..0000000 --- a/device/device_test.go +++ /dev/null @@ -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" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/conn/bindtest" - "github.com/tailscale/wireguard-go/tun" - "github.com/tailscale/wireguard-go/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, offset int) 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) - } -} diff --git a/device/endpoint_test.go b/device/endpoint_test.go deleted file mode 100644 index 93a4998..0000000 --- a/device/endpoint_test.go +++ /dev/null @@ -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 -} diff --git a/device/kdf_test.go b/device/kdf_test.go deleted file mode 100644 index f9c76d6..0000000 --- a/device/kdf_test.go +++ /dev/null @@ -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) - } -} diff --git a/device/noise_test.go b/device/noise_test.go deleted file mode 100644 index 160bee5..0000000 --- a/device/noise_test.go +++ /dev/null @@ -1,207 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package device - -import ( - "bytes" - "encoding/binary" - "net/netip" - "testing" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/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) - } -} - -type initAwareEP struct { - calledWith *[32]byte -} - -var _ conn.Endpoint = (*initAwareEP)(nil) -var _ conn.InitiationAwareEndpoint = (*initAwareEP)(nil) - -func (i *initAwareEP) ClearSrc() {} -func (i *initAwareEP) SrcToString() string { return "" } -func (i *initAwareEP) DstToString() string { return "" } -func (i *initAwareEP) DstToBytes() []byte { return nil } -func (i *initAwareEP) DstIP() netip.Addr { return netip.Addr{} } -func (i *initAwareEP) SrcIP() netip.Addr { return netip.Addr{} } - -func (i *initAwareEP) InitiationMessagePublicKey(peerPublicKey [32]byte) { - calledWith := [32]byte{} - copy(calledWith[:], peerPublicKey[:]) - i.calledWith = &calledWith -} - -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) - initEP := &initAwareEP{} - peer := dev2.ConsumeMessageInitiation(msg1, initEP) - if peer == nil { - t.Fatal("handshake failed at initiation message") - } - if initEP.calledWith == nil { - t.Fatal("initAwareEP never called") - } - if *initEP.calledWith != dev1.staticIdentity.publicKey { - t.Fatal("initAwareEP called with unexpected public key") - } - - 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) - }() -} diff --git a/device/pools_test.go b/device/pools_test.go deleted file mode 100644 index 2b16f39..0000000 --- a/device/pools_test.go +++ /dev/null @@ -1,140 +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) { - 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() { - p.lock.Lock() - count := p.count - p.lock.Unlock() - 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() -} diff --git a/device/race_disabled_test.go b/device/race_disabled_test.go deleted file mode 100644 index bb5c450..0000000 --- a/device/race_disabled_test.go +++ /dev/null @@ -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 diff --git a/device/race_enabled_test.go b/device/race_enabled_test.go deleted file mode 100644 index 4e9daea..0000000 --- a/device/race_enabled_test.go +++ /dev/null @@ -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 diff --git a/format_test.go b/format_test.go deleted file mode 100644 index 6f6cab7..0000000 --- a/format_test.go +++ /dev/null @@ -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() -} diff --git a/go.mod b/go.mod index 9c9b02a..7476734 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,4 @@ require ( golang.org/x/net v0.15.0 golang.org/x/sys v0.12.0 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 ) diff --git a/go.sum b/go.sum index 6bcecea..ec4169f 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,8 @@ -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/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= diff --git a/ipc/namedpipe/namedpipe_test.go b/ipc/namedpipe/namedpipe_test.go deleted file mode 100644 index de7d0f6..0000000 --- a/ipc/namedpipe/namedpipe_test.go +++ /dev/null @@ -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" - - "github.com/tailscale/wireguard-go/ipc/namedpipe" - "golang.org/x/sys/windows" -) - -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() - } -} diff --git a/main.go b/main.go deleted file mode 100644 index 55000e9..0000000 --- a/main.go +++ /dev/null @@ -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" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/ipc" - "github.com/tailscale/wireguard-go/tun" - "golang.org/x/sys/unix" -) - -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 .\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") -} diff --git a/main_windows.go b/main_windows.go deleted file mode 100644 index 689a9a7..0000000 --- a/main_windows.go +++ /dev/null @@ -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" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/ipc" - - "github.com/tailscale/wireguard-go/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 , 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") -} diff --git a/ratelimiter/ratelimiter_test.go b/ratelimiter/ratelimiter_test.go deleted file mode 100644 index 0bfa3af..0000000 --- a/ratelimiter/ratelimiter_test.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/replay/replay_test.go b/replay/replay_test.go deleted file mode 100644 index 9a9e4a8..0000000 --- a/replay/replay_test.go +++ /dev/null @@ -1,119 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package replay - -import ( - "testing" -) - -/* Ported from the linux kernel implementation - * - * - */ - -const RejectAfterMessages = 1<<64 - 1<<13 - 1 - -func TestReplay(t *testing.T) { - var filter Filter - - const T_LIM = windowSize + 1 - - testNumber := 0 - T := func(n uint64, expected bool) { - testNumber++ - if filter.ValidateCounter(n, RejectAfterMessages) != expected { - t.Fatal("Test", testNumber, "failed", n, expected) - } - } - - filter.Reset() - - T(0, true) /* 1 */ - T(1, true) /* 2 */ - T(1, false) /* 3 */ - T(9, true) /* 4 */ - T(8, true) /* 5 */ - T(7, true) /* 6 */ - T(7, false) /* 7 */ - T(T_LIM, true) /* 8 */ - T(T_LIM-1, true) /* 9 */ - T(T_LIM-1, false) /* 10 */ - T(T_LIM-2, true) /* 11 */ - T(2, true) /* 12 */ - T(2, false) /* 13 */ - T(T_LIM+16, true) /* 14 */ - T(3, false) /* 15 */ - T(T_LIM+16, false) /* 16 */ - T(T_LIM*4, true) /* 17 */ - T(T_LIM*4-(T_LIM-1), true) /* 18 */ - T(10, false) /* 19 */ - T(T_LIM*4-T_LIM, false) /* 20 */ - T(T_LIM*4-(T_LIM+1), false) /* 21 */ - T(T_LIM*4-(T_LIM-2), true) /* 22 */ - T(T_LIM*4+1-T_LIM, false) /* 23 */ - T(0, false) /* 24 */ - T(RejectAfterMessages, false) /* 25 */ - T(RejectAfterMessages-1, true) /* 26 */ - T(RejectAfterMessages, false) /* 27 */ - T(RejectAfterMessages-1, false) /* 28 */ - T(RejectAfterMessages-2, true) /* 29 */ - T(RejectAfterMessages+1, false) /* 30 */ - T(RejectAfterMessages+2, false) /* 31 */ - T(RejectAfterMessages-2, false) /* 32 */ - T(RejectAfterMessages-3, true) /* 33 */ - T(0, false) /* 34 */ - - t.Log("Bulk test 1") - filter.Reset() - testNumber = 0 - for i := uint64(1); i <= windowSize; i++ { - T(i, true) - } - T(0, true) - T(0, false) - - t.Log("Bulk test 2") - filter.Reset() - testNumber = 0 - for i := uint64(2); i <= windowSize+1; i++ { - T(i, true) - } - T(1, true) - T(0, false) - - t.Log("Bulk test 3") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize + 1); i > 0; i-- { - T(i, true) - } - - t.Log("Bulk test 4") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize + 2); i > 1; i-- { - T(i, true) - } - T(0, false) - - t.Log("Bulk test 5") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize); i > 0; i-- { - T(i, true) - } - T(windowSize+1, true) - T(0, false) - - t.Log("Bulk test 6") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize); i > 0; i-- { - T(i, true) - } - T(0, true) - T(windowSize+1, true) -} diff --git a/tai64n/tai64n_test.go b/tai64n/tai64n_test.go deleted file mode 100644 index c70fc1a..0000000 --- a/tai64n/tai64n_test.go +++ /dev/null @@ -1,40 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package tai64n - -import ( - "testing" - "time" -) - -// Test that timestamps are monotonic as required by Wireguard and that -// nanosecond-level information is whitened to prevent side channel attacks. -func TestMonotonic(t *testing.T) { - startTime := time.Unix(0, 123456789) // a nontrivial bit pattern - // Whitening should reduce timestamp granularity - // to more than 10 but fewer than 20 milliseconds. - tests := []struct { - name string - t1, t2 time.Time - wantAfter bool - }{ - {"after_10_ns", startTime, startTime.Add(10 * time.Nanosecond), false}, - {"after_10_us", startTime, startTime.Add(10 * time.Microsecond), false}, - {"after_1_ms", startTime, startTime.Add(time.Millisecond), false}, - {"after_10_ms", startTime, startTime.Add(10 * time.Millisecond), false}, - {"after_20_ms", startTime, startTime.Add(20 * time.Millisecond), true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ts1, ts2 := stamp(tt.t1), stamp(tt.t2) - got := ts2.After(ts1) - if got != tt.wantAfter { - t.Errorf("after = %v; want %v", got, tt.wantAfter) - } - }) - } -} diff --git a/tests/netns.sh b/tests/netns.sh deleted file mode 100755 index 2f2a2cd..0000000 --- a/tests/netns.sh +++ /dev/null @@ -1,425 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2015-2017 Jason A. Donenfeld . All Rights Reserved. - -# This script tests the below topology: -# -# ┌─────────────────────┐ ┌──────────────────────────────────┐ ┌─────────────────────┐ -# │ $ns1 namespace │ │ $ns0 namespace │ │ $ns2 namespace │ -# │ │ │ │ │ │ -# │┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐│ -# ││ wg1 │───────────┼───┼────────────│ lo │────────────┼───┼───────────│ wg2 ││ -# │├────────┴──────────┐│ │ ┌───────┴────────┴────────┐ │ │┌──────────┴────────┤│ -# ││192.168.241.1/24 ││ │ │(ns1) (ns2) │ │ ││192.168.241.2/24 ││ -# ││fd00::1/24 ││ │ │127.0.0.1:1 127.0.0.1:2│ │ ││fd00::2/24 ││ -# │└───────────────────┘│ │ │[::]:1 [::]:2 │ │ │└───────────────────┘│ -# └─────────────────────┘ │ └─────────────────────────┘ │ └─────────────────────┘ -# └──────────────────────────────────┘ -# -# After the topology is prepared we run a series of TCP/UDP iperf3 tests between the -# wireguard peers in $ns1 and $ns2. Note that $ns0 is the endpoint for the wg1 -# interfaces in $ns1 and $ns2. See https://www.wireguard.com/netns/ for further -# details on how this is accomplished. - -# This code is ported to the WireGuard-Go directly from the kernel project. -# -# Please ensure that you have installed the newest version of the WireGuard -# tools from the WireGuard project and before running these tests as: -# -# ./netns.sh - -set -e - -exec 3>&1 -export WG_HIDE_KEYS=never -netns0="wg-test-$$-0" -netns1="wg-test-$$-1" -netns2="wg-test-$$-2" -program=$1 -export LOG_LEVEL="verbose" - -pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } -pp() { pretty "" "$*"; "$@"; } -maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } -n0() { pretty 0 "$*"; maybe_exec ip netns exec $netns0 "$@"; } -n1() { pretty 1 "$*"; maybe_exec ip netns exec $netns1 "$@"; } -n2() { pretty 2 "$*"; maybe_exec ip netns exec $netns2 "$@"; } -ip0() { pretty 0 "ip $*"; ip -n $netns0 "$@"; } -ip1() { pretty 1 "ip $*"; ip -n $netns1 "$@"; } -ip2() { pretty 2 "ip $*"; ip -n $netns2 "$@"; } -sleep() { read -t "$1" -N 0 || true; } -waitiperf() { pretty "${1//*-}" "wait for iperf:5201"; while [[ $(ss -N "$1" -tlp 'sport = 5201') != *iperf3* ]]; do sleep 0.1; done; } -waitncatudp() { pretty "${1//*-}" "wait for udp:1111"; while [[ $(ss -N "$1" -ulp 'sport = 1111') != *ncat* ]]; do sleep 0.1; done; } -waitiface() { pretty "${1//*-}" "wait for $2 to come up"; ip netns exec "$1" bash -c "while [[ \$(< \"/sys/class/net/$2/operstate\") != up ]]; do read -t .1 -N 0 || true; done;"; } - -cleanup() { - set +e - exec 2>/dev/null - printf "$orig_message_cost" > /proc/sys/net/core/message_cost - ip0 link del dev wg1 - ip1 link del dev wg1 - ip2 link del dev wg1 - local to_kill="$(ip netns pids $netns0) $(ip netns pids $netns1) $(ip netns pids $netns2)" - [[ -n $to_kill ]] && kill $to_kill - pp ip netns del $netns1 - pp ip netns del $netns2 - pp ip netns del $netns0 - exit -} - -orig_message_cost="$(< /proc/sys/net/core/message_cost)" -trap cleanup EXIT -printf 0 > /proc/sys/net/core/message_cost - -ip netns del $netns0 2>/dev/null || true -ip netns del $netns1 2>/dev/null || true -ip netns del $netns2 2>/dev/null || true -pp ip netns add $netns0 -pp ip netns add $netns1 -pp ip netns add $netns2 -ip0 link set up dev lo - -# ip0 link add dev wg1 type wireguard -n0 $program wg1 -ip0 link set wg1 netns $netns1 - -# ip0 link add dev wg1 type wireguard -n0 $program wg2 -ip0 link set wg2 netns $netns2 - -key1="$(pp wg genkey)" -key2="$(pp wg genkey)" -pub1="$(pp wg pubkey <<<"$key1")" -pub2="$(pp wg pubkey <<<"$key2")" -psk="$(pp wg genpsk)" -[[ -n $key1 && -n $key2 && -n $psk ]] - -configure_peers() { - - ip1 addr add 192.168.241.1/24 dev wg1 - ip1 addr add fd00::1/24 dev wg1 - - ip2 addr add 192.168.241.2/24 dev wg2 - ip2 addr add fd00::2/24 dev wg2 - - n0 wg set wg1 \ - private-key <(echo "$key1") \ - listen-port 10000 \ - peer "$pub2" \ - preshared-key <(echo "$psk") \ - allowed-ips 192.168.241.2/32,fd00::2/128 - n0 wg set wg2 \ - private-key <(echo "$key2") \ - listen-port 20000 \ - peer "$pub1" \ - preshared-key <(echo "$psk") \ - allowed-ips 192.168.241.1/32,fd00::1/128 - - n0 wg showconf wg1 - n0 wg showconf wg2 - - ip1 link set up dev wg1 - ip2 link set up dev wg2 - sleep 1 -} -configure_peers - -tests() { - # Ping over IPv4 - n2 ping -c 10 -f -W 1 192.168.241.1 - n1 ping -c 10 -f -W 1 192.168.241.2 - - # Ping over IPv6 - n2 ping6 -c 10 -f -W 1 fd00::1 - n1 ping6 -c 10 -f -W 1 fd00::2 - - # TCP over IPv4 - n2 iperf3 -s -1 -B 192.168.241.2 & - waitiperf $netns2 - n1 iperf3 -Z -n 1G -c 192.168.241.2 - - # TCP over IPv6 - n1 iperf3 -s -1 -B fd00::1 & - waitiperf $netns1 - n2 iperf3 -Z -n 1G -c fd00::1 - - # UDP over IPv4 - n1 iperf3 -s -1 -B 192.168.241.1 & - waitiperf $netns1 - n2 iperf3 -Z -n 1G -b 0 -u -c 192.168.241.1 - - # UDP over IPv6 - n2 iperf3 -s -1 -B fd00::2 & - waitiperf $netns2 - n1 iperf3 -Z -n 1G -b 0 -u -c fd00::2 -} - -[[ $(ip1 link show dev wg1) =~ mtu\ ([0-9]+) ]] && orig_mtu="${BASH_REMATCH[1]}" -big_mtu=$(( 34816 - 1500 + $orig_mtu )) - -# Test using IPv4 as outer transport -n0 wg set wg1 peer "$pub2" endpoint 127.0.0.1:20000 -n0 wg set wg2 peer "$pub1" endpoint 127.0.0.1:10000 - -# Before calling tests, we first make sure that the stats counters are working -n2 ping -c 10 -f -W 1 192.168.241.1 -{ read _; read _; read _; read rx_bytes _; read _; read tx_bytes _; } < <(ip2 -stats link show dev wg2) -ip2 -stats link show dev wg2 -n0 wg show -[[ $rx_bytes -ge 840 && $tx_bytes -ge 880 && $rx_bytes -lt 2500 && $rx_bytes -lt 2500 ]] -echo "counters working" -tests -ip1 link set wg1 mtu $big_mtu -ip2 link set wg2 mtu $big_mtu -tests - -ip1 link set wg1 mtu $orig_mtu -ip2 link set wg2 mtu $orig_mtu - -# Test using IPv6 as outer transport -n0 wg set wg1 peer "$pub2" endpoint [::1]:20000 -n0 wg set wg2 peer "$pub1" endpoint [::1]:10000 -tests -ip1 link set wg1 mtu $big_mtu -ip2 link set wg2 mtu $big_mtu -tests - -ip1 link set wg1 mtu $orig_mtu -ip2 link set wg2 mtu $orig_mtu - -# Test using IPv4 that roaming works -ip0 -4 addr del 127.0.0.1/8 dev lo -ip0 -4 addr add 127.212.121.99/8 dev lo -n0 wg set wg1 listen-port 9999 -n0 wg set wg1 peer "$pub2" endpoint 127.0.0.1:20000 -n1 ping6 -W 1 -c 1 fd00::2 -[[ $(n2 wg show wg2 endpoints) == "$pub1 127.212.121.99:9999" ]] - -# Test using IPv6 that roaming works -n1 wg set wg1 listen-port 9998 -n1 wg set wg1 peer "$pub2" endpoint [::1]:20000 -n1 ping -W 1 -c 1 192.168.241.2 -[[ $(n2 wg show wg2 endpoints) == "$pub1 [::1]:9998" ]] - -# Test that crypto-RP filter works -n1 wg set wg1 peer "$pub2" allowed-ips 192.168.241.0/24 -exec 4< <(n1 ncat -l -u -p 1111) -nmap_pid=$! -waitncatudp $netns1 -n2 ncat -u 192.168.241.1 1111 <<<"X" -read -r -N 1 -t 1 out <&4 && [[ $out == "X" ]] -kill $nmap_pid -more_specific_key="$(pp wg genkey | pp wg pubkey)" -n0 wg set wg1 peer "$more_specific_key" allowed-ips 192.168.241.2/32 -n0 wg set wg2 listen-port 9997 -exec 4< <(n1 ncat -l -u -p 1111) -nmap_pid=$! -waitncatudp $netns1 -n2 ncat -u 192.168.241.1 1111 <<<"X" -! read -r -N 1 -t 1 out <&4 -kill $nmap_pid -n0 wg set wg1 peer "$more_specific_key" remove -[[ $(n1 wg show wg1 endpoints) == "$pub2 [::1]:9997" ]] - -ip1 link del wg1 -ip2 link del wg2 - -# Test using NAT. We now change the topology to this: -# ┌────────────────────────────────────────┐ ┌────────────────────────────────────────────────┐ ┌────────────────────────────────────────┐ -# │ $ns1 namespace │ │ $ns0 namespace │ │ $ns2 namespace │ -# │ │ │ │ │ │ -# │ ┌─────┐ ┌─────┐ │ │ ┌──────┐ ┌──────┐ │ │ ┌─────┐ ┌─────┐ │ -# │ │ wg1 │─────────────│vethc│───────────┼────┼────│vethrc│ │vethrs│──────────────┼─────┼──│veths│────────────│ wg2 │ │ -# │ ├─────┴──────────┐ ├─────┴──────────┐│ │ ├──────┴─────────┐ ├──────┴────────────┐ │ │ ├─────┴──────────┐ ├─────┴──────────┐ │ -# │ │192.168.241.1/24│ │192.168.1.100/24││ │ │192.168.1.100/24│ │10.0.0.1/24 │ │ │ │10.0.0.100/24 │ │192.168.241.2/24│ │ -# │ │fd00::1/24 │ │ ││ │ │ │ │SNAT:192.168.1.0/24│ │ │ │ │ │fd00::2/24 │ │ -# │ └────────────────┘ └────────────────┘│ │ └────────────────┘ └───────────────────┘ │ │ └────────────────┘ └────────────────┘ │ -# └────────────────────────────────────────┘ └────────────────────────────────────────────────┘ └────────────────────────────────────────┘ - -# ip1 link add dev wg1 type wireguard -# ip2 link add dev wg1 type wireguard - -n1 $program wg1 -n2 $program wg2 - -configure_peers - -ip0 link add vethrc type veth peer name vethc -ip0 link add vethrs type veth peer name veths -ip0 link set vethc netns $netns1 -ip0 link set veths netns $netns2 -ip0 link set vethrc up -ip0 link set vethrs up -ip0 addr add 192.168.1.1/24 dev vethrc -ip0 addr add 10.0.0.1/24 dev vethrs -ip1 addr add 192.168.1.100/24 dev vethc -ip1 link set vethc up -ip1 route add default via 192.168.1.1 -ip2 addr add 10.0.0.100/24 dev veths -ip2 link set veths up -waitiface $netns0 vethrc -waitiface $netns0 vethrs -waitiface $netns1 vethc -waitiface $netns2 veths - -n0 bash -c 'printf 1 > /proc/sys/net/ipv4/ip_forward' -n0 bash -c 'printf 2 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout' -n0 bash -c 'printf 2 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream' -n0 iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 10.0.0.0/24 -j SNAT --to 10.0.0.1 - -n0 wg set wg1 peer "$pub2" endpoint 10.0.0.100:20000 persistent-keepalive 1 -n1 ping -W 1 -c 1 192.168.241.2 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n2 wg show wg2 endpoints) == "$pub1 10.0.0.1:10000" ]] -# Demonstrate n2 can still send packets to n1, since persistent-keepalive will prevent connection tracking entry from expiring (to see entries: `n0 conntrack -L`). -pp sleep 3 -n2 ping -W 1 -c 1 192.168.241.1 - -n0 iptables -t nat -F -ip0 link del vethrc -ip0 link del vethrs -ip1 link del wg1 -ip2 link del wg2 - -# Test that saddr routing is sticky but not too sticky, changing to this topology: -# ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ -# │ $ns1 namespace │ │ $ns2 namespace │ -# │ │ │ │ -# │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ -# │ │ wg1 │─────────────│veth1│───────────┼────┼──│veth2│────────────│ wg2 │ │ -# │ ├─────┴──────────┐ ├─────┴──────────┐│ │ ├─────┴──────────┐ ├─────┴──────────┐ │ -# │ │192.168.241.1/24│ │10.0.0.1/24 ││ │ │10.0.0.2/24 │ │192.168.241.2/24│ │ -# │ │fd00::1/24 │ │fd00:aa::1/96 ││ │ │fd00:aa::2/96 │ │fd00::2/24 │ │ -# │ └────────────────┘ └────────────────┘│ │ └────────────────┘ └────────────────┘ │ -# └────────────────────────────────────────┘ └────────────────────────────────────────┘ - -# ip1 link add dev wg1 type wireguard -# ip2 link add dev wg1 type wireguard -n1 $program wg1 -n2 $program wg2 - -configure_peers - -ip1 link add veth1 type veth peer name veth2 -ip1 link set veth2 netns $netns2 -n1 bash -c 'printf 0 > /proc/sys/net/ipv6/conf/veth1/accept_dad' -n2 bash -c 'printf 0 > /proc/sys/net/ipv6/conf/veth2/accept_dad' -n1 bash -c 'printf 1 > /proc/sys/net/ipv4/conf/veth1/promote_secondaries' - -# First we check that we aren't overly sticky and can fall over to new IPs when old ones are removed -ip1 addr add 10.0.0.1/24 dev veth1 -ip1 addr add fd00:aa::1/96 dev veth1 -ip2 addr add 10.0.0.2/24 dev veth2 -ip2 addr add fd00:aa::2/96 dev veth2 -ip1 link set veth1 up -ip2 link set veth2 up -waitiface $netns1 veth1 -waitiface $netns2 veth2 -n0 wg set wg1 peer "$pub2" endpoint 10.0.0.2:20000 -n1 ping -W 1 -c 1 192.168.241.2 -ip1 addr add 10.0.0.10/24 dev veth1 -ip1 addr del 10.0.0.1/24 dev veth1 -n1 ping -W 1 -c 1 192.168.241.2 -n0 wg set wg1 peer "$pub2" endpoint [fd00:aa::2]:20000 -n1 ping -W 1 -c 1 192.168.241.2 -ip1 addr add fd00:aa::10/96 dev veth1 -ip1 addr del fd00:aa::1/96 dev veth1 -n1 ping -W 1 -c 1 192.168.241.2 - -# Now we show that we can successfully do reply to sender routing -ip1 link set veth1 down -ip2 link set veth2 down -ip1 addr flush dev veth1 -ip2 addr flush dev veth2 -ip1 addr add 10.0.0.1/24 dev veth1 -ip1 addr add 10.0.0.2/24 dev veth1 -ip1 addr add fd00:aa::1/96 dev veth1 -ip1 addr add fd00:aa::2/96 dev veth1 -ip2 addr add 10.0.0.3/24 dev veth2 -ip2 addr add fd00:aa::3/96 dev veth2 -ip1 link set veth1 up -ip2 link set veth2 up -waitiface $netns1 veth1 -waitiface $netns2 veth2 -n0 wg set wg2 peer "$pub1" endpoint 10.0.0.1:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 10.0.0.1:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint [fd00:aa::1]:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 [fd00:aa::1]:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint 10.0.0.2:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 10.0.0.2:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint [fd00:aa::2]:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 [fd00:aa::2]:10000" ]] - -ip1 link del veth1 -ip1 link del wg1 -ip2 link del wg2 - -# Test that Netlink/IPC is working properly by doing things that usually cause split responses - -n0 $program wg0 -sleep 5 -config=( "[Interface]" "PrivateKey=$(wg genkey)" "[Peer]" "PublicKey=$(wg genkey)" ) -for a in {1..255}; do - for b in {0..255}; do - config+=( "AllowedIPs=$a.$b.0.0/16,$a::$b/128" ) - done -done -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -i=0 -for ip in $(n0 wg show wg0 allowed-ips); do - ((++i)) -done -((i == 255*256*2+1)) -ip0 link del wg0 - -n0 $program wg0 -config=( "[Interface]" "PrivateKey=$(wg genkey)" ) -for a in {1..40}; do - config+=( "[Peer]" "PublicKey=$(wg genkey)" ) - for b in {1..52}; do - config+=( "AllowedIPs=$a.$b.0.0/16" ) - done -done -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -i=0 -while read -r line; do - j=0 - for ip in $line; do - ((++j)) - done - ((j == 53)) - ((++i)) -done < <(n0 wg show wg0 allowed-ips) -((i == 40)) -ip0 link del wg0 - -n0 $program wg0 -config=( ) -for i in {1..29}; do - config+=( "[Peer]" "PublicKey=$(wg genkey)" ) -done -config+=( "[Peer]" "PublicKey=$(wg genkey)" "AllowedIPs=255.2.3.4/32,abcd::255/128" ) -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -n0 wg showconf wg0 > /dev/null -ip0 link del wg0 - -! n0 wg show doesnotexist || false - -declare -A objects -while read -t 0.1 -r line 2>/dev/null || [[ $? -ne 142 ]]; do - [[ $line =~ .*(wg[0-9]+:\ [A-Z][a-z]+\ [0-9]+)\ .*(created|destroyed).* ]] || continue - objects["${BASH_REMATCH[1]}"]+="${BASH_REMATCH[2]}" -done < /dev/kmsg -alldeleted=1 -for object in "${!objects[@]}"; do - if [[ ${objects["$object"]} != *createddestroyed ]]; then - echo "Error: $object: merely ${objects["$object"]}" >&3 - alldeleted=0 - fi -done -[[ $alldeleted -eq 1 ]] -pretty "" "Objects that were created were also destroyed." diff --git a/tun/alignment_windows_test.go b/tun/alignment_windows_test.go deleted file mode 100644 index 67a785e..0000000 --- a/tun/alignment_windows_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package tun - -import ( - "reflect" - "testing" - "unsafe" -) - -func checkAlignment(t *testing.T, name string, offset uintptr) { - t.Helper() - if offset%8 != 0 { - t.Errorf("offset of %q within struct is %d bytes, which does not align to 64-bit word boundaries (missing %d bytes). Atomic operations will crash on 32-bit systems.", name, offset, 8-(offset%8)) - } -} - -// TestRateJugglerAlignment checks that atomically-accessed fields are -// aligned to 64-bit boundaries, as required by the atomic package. -// -// Unfortunately, violating this rule on 32-bit platforms results in a -// hard segfault at runtime. -func TestRateJugglerAlignment(t *testing.T) { - var r rateJuggler - - typ := reflect.TypeOf(&r).Elem() - t.Logf("Peer type size: %d, with fields:", typ.Size()) - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - t.Logf("\t%30s\toffset=%3v\t(type size=%3d, align=%d)", - field.Name, - field.Offset, - field.Type.Size(), - field.Type.Align(), - ) - } - - checkAlignment(t, "rateJuggler.current", unsafe.Offsetof(r.current)) - checkAlignment(t, "rateJuggler.nextByteCount", unsafe.Offsetof(r.nextByteCount)) - checkAlignment(t, "rateJuggler.nextStartTime", unsafe.Offsetof(r.nextStartTime)) -} - -// TestNativeTunAlignment checks that atomically-accessed fields are -// aligned to 64-bit boundaries, as required by the atomic package. -// -// Unfortunately, violating this rule on 32-bit platforms results in a -// hard segfault at runtime. -func TestNativeTunAlignment(t *testing.T) { - var tun NativeTun - - typ := reflect.TypeOf(&tun).Elem() - t.Logf("Peer type size: %d, with fields:", typ.Size()) - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - t.Logf("\t%30s\toffset=%3v\t(type size=%3d, align=%d)", - field.Name, - field.Offset, - field.Type.Size(), - field.Type.Align(), - ) - } - - checkAlignment(t, "NativeTun.rate", unsafe.Offsetof(tun.rate)) -} diff --git a/tun/checksum_amd64_test.go b/tun/checksum_amd64_test.go deleted file mode 100644 index 7a0b681..0000000 --- a/tun/checksum_amd64_test.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build amd64 - -package tun - -import ( - "golang.org/x/sys/cpu" -) - -var archChecksumFuncs = []archChecksumDetails{ - { - name: "generic32", - available: true, - f: checksumGeneric32, - }, - { - name: "generic64", - available: true, - f: checksumGeneric64, - }, - { - name: "generic32Alternate", - available: true, - f: checksumGeneric32Alternate, - }, - { - name: "generic64Alternate", - available: true, - f: checksumGeneric64Alternate, - }, - { - name: "AMD64", - available: true, - f: checksumAMD64, - }, - { - name: "SSE2", - available: cpu.X86.HasSSE2, - f: checksumSSE2, - }, - { - name: "AVX2", - available: cpu.X86.HasAVX && cpu.X86.HasAVX2 && cpu.X86.HasBMI2, - f: checksumAVX2, - }, -} diff --git a/tun/checksum_generic_test.go b/tun/checksum_generic_test.go deleted file mode 100644 index 401a7bb..0000000 --- a/tun/checksum_generic_test.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !amd64 - -package tun - -var archChecksumFuncs = []archChecksumDetails{ - { - name: "generic32", - available: true, - f: checksumGeneric32, - }, - { - name: "generic32Alternate", - available: true, - f: checksumGeneric32Alternate, - }, - { - name: "generic64", - available: true, - f: checksumGeneric64, - }, - { - name: "generic64Alternate", - available: true, - f: checksumGeneric64Alternate, - }, -} diff --git a/tun/checksum_test.go b/tun/checksum_test.go deleted file mode 100644 index f5b8f18..0000000 --- a/tun/checksum_test.go +++ /dev/null @@ -1,619 +0,0 @@ -package tun - -import ( - "fmt" - "math" - "math/rand" - "net/netip" - "sort" - "syscall" - "testing" - "unsafe" - - "gvisor.dev/gvisor/pkg/tcpip" - gvisorChecksum "gvisor.dev/gvisor/pkg/tcpip/checksum" - "gvisor.dev/gvisor/pkg/tcpip/header" -) - -type archChecksumDetails struct { - name string - available bool - f func([]byte, uint16) uint16 -} - -func fillRandomBuffer(seed int64, buf []byte) { - rng := rand.New(rand.NewSource(seed)) - n, err := rng.Read(buf) - if err != nil { - panic(err) - } - if n != len(buf) { - panic("incomplete random buffer") - } -} - -func deterministicRandomBytes(seed int64, length int) []byte { - buf := make([]byte, length) - fillRandomBuffer(seed, buf) - return buf -} - -func getPageAlignedRandomBytes(seed int64, length int) []byte { - alignment := syscall.Getpagesize() - buf := make([]byte, length+(alignment-1)) - bufPtr := uintptr(unsafe.Pointer(&buf[0])) - alignedBufPtr := (bufPtr + uintptr(alignment-1)) & ^uintptr(alignment-1) - alignedStart := int(alignedBufPtr - bufPtr) - - buf = buf[alignedStart : alignedStart+length] - fillRandomBuffer(seed, buf) - return buf -} - -func TestChecksum(t *testing.T) { - alignedBuf := getPageAlignedRandomBytes(10, 8192) - allOnes := make([]byte, 65535) - for i := range allOnes { - allOnes[i] = 0xff - } - allFE := make([]byte, 65535) - for i := range allFE { - allFE[i] = 0xfe - } - - tests := []struct { - name string - data []byte - initial uint16 - want uint16 - }{ - { - name: "empty", - data: []byte{}, - initial: 0, - want: 0, - }, - { - name: "max initial", - data: []byte{}, - initial: math.MaxUint16, - want: 0xffff, - }, - { - name: "odd length", - data: []byte{0x01, 0x02, 0x01}, - initial: 0, - want: 0x0202, - }, - { - name: "tiny", - data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02}, - initial: 0, - want: 0x0306, - }, - { - name: "initial", - data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02}, - initial: 0x1000, - want: 0x1306, - }, - // cleanup0 through cleanup15 is 1024 (handled by large SIMD loops) + - // 32 (handled by small SIMD loops) + n, where n ranges from 0 to 15 - // to cover all of the leftover byte sizes that are possible after small - // SIMD loops that handle 16 bytes. - { - name: "cleanup0", - data: deterministicRandomBytes(1, 1056), - initial: 0, - want: 0x11ec, - }, - { - name: "cleanup1", - data: deterministicRandomBytes(1, 1057), - initial: 0, - want: 0xc5ec, - }, - { - name: "cleanup2", - data: deterministicRandomBytes(1, 1058), - initial: 0, - want: 0xc6ad, - }, - { - name: "cleanup3", - data: deterministicRandomBytes(1, 1059), - initial: 0, - want: 0x86ae, - }, - { - name: "cleanup4", - data: deterministicRandomBytes(1, 1060), - initial: 0, - want: 0x878e, - }, - { - name: "cleanup5", - data: deterministicRandomBytes(1, 1061), - initial: 0, - want: 0xdb8e, - }, - { - name: "cleanup6", - data: deterministicRandomBytes(1, 1062), - initial: 0, - want: 0xdbd5, - }, - { - name: "cleanup7", - data: deterministicRandomBytes(1, 1063), - initial: 0, - want: 0xcfd6, - }, - { - name: "cleanup8", - data: deterministicRandomBytes(1, 1064), - initial: 0, - want: 0xd090, - }, - { - name: "cleanup9", - data: deterministicRandomBytes(1, 1065), - initial: 0, - want: 0x0791, - }, - { - name: "cleanup10", - data: deterministicRandomBytes(1, 1066), - initial: 0, - want: 0x079f, - }, - { - name: "cleanup11", - data: deterministicRandomBytes(1, 1067), - initial: 0, - want: 0xba9f, - }, - { - name: "cleanup12", - data: deterministicRandomBytes(1, 1068), - initial: 0, - want: 0xbb0c, - }, - { - name: "cleanup13", - data: deterministicRandomBytes(1, 1069), - initial: 0, - want: 0x770d, - }, - { - name: "cleanup14", - data: deterministicRandomBytes(1, 1070), - initial: 0, - want: 0x780a, - }, - { - name: "cleanup15", - data: deterministicRandomBytes(1, 1071), - initial: 0, - want: 0x640b, - }, - // small1 through small15 covers small sizes that are not large enough - // to do overlapped reads. - { - name: "small1", - data: deterministicRandomBytes(2, 1), - initial: 0x1122, - want: 0x4022, - }, - { - name: "small2", - data: deterministicRandomBytes(2, 2), - initial: 0x1122, - want: 0x40a4, - }, - { - name: "small3", - data: deterministicRandomBytes(2, 3), - initial: 0x1122, - want: 0xc2a4, - }, - { - name: "small4", - data: deterministicRandomBytes(2, 4), - initial: 0x1122, - want: 0xc36f, - }, - { - name: "small5", - data: deterministicRandomBytes(2, 5), - initial: 0x1122, - want: 0xa570, - }, - { - name: "small6", - data: deterministicRandomBytes(2, 6), - initial: 0x1122, - want: 0xa669, - }, - { - name: "small7", - data: deterministicRandomBytes(2, 7), - initial: 0x1122, - want: 0x0f6a, - }, - { - name: "small8", - data: deterministicRandomBytes(2, 8), - initial: 0x1122, - want: 0x0fd9, - }, - { - name: "small9", - data: deterministicRandomBytes(2, 9), - initial: 0x1122, - want: 0x40d9, - }, - { - name: "small10", - data: deterministicRandomBytes(2, 10), - initial: 0x1122, - want: 0x411d, - }, - { - name: "small11", - data: deterministicRandomBytes(2, 11), - initial: 0x1122, - want: 0x011e, - }, - { - name: "small12", - data: deterministicRandomBytes(2, 12), - initial: 0x1122, - want: 0x01c8, - }, - { - name: "small13", - data: deterministicRandomBytes(2, 13), - initial: 0x1122, - want: 0x4dc8, - }, - { - name: "small14", - data: deterministicRandomBytes(2, 14), - initial: 0x1122, - want: 0x4eb5, - }, - { - name: "small15", - data: deterministicRandomBytes(2, 15), - initial: 0x1122, - want: 0xa4b5, - }, - // other small-ish sizes - { - name: "small16", - data: deterministicRandomBytes(1, 16), - initial: 0, - want: 0x02fa, - }, - { - name: "small32", - data: deterministicRandomBytes(1, 32), - initial: 0, - want: 0x03ee, - }, - { - name: "small64", - data: deterministicRandomBytes(1, 64), - initial: 0, - want: 0x3f85, - }, - { - name: "medium", - data: deterministicRandomBytes(1, 1400), - initial: 0, - want: 0xbea5, - }, - { - name: "big", - data: deterministicRandomBytes(2, 65000), - initial: 0, - want: 0x3ba7, - }, - { - name: "big-initial", - data: deterministicRandomBytes(2, 65000), - initial: 0x1234, - want: 0x4ddb, - }, - { - // big-small-loop is intended to exercise a few iterations of a big - // initial loop of 128 bytes or larger + a smaller loop of 16 bytes - // + some leftover - name: "big-small-loop", - data: deterministicRandomBytes(3, 1094), - initial: 0x9999, - want: 0xe65b, - }, - { - name: "page-aligned", - data: alignedBuf[:4096], - initial: 0, - want: 0x963b, - }, - { - name: "32-aligned", - data: alignedBuf[32:4128], - initial: 0, - want: 0x30c4, - }, - { - name: "16-aligned", - data: alignedBuf[16:4112], - initial: 0, - want: 0xaeff, - }, - { - name: "8-aligned", - data: alignedBuf[8:4104], - initial: 0, - want: 0x6c3b, - }, - { - name: "4-aligned", - data: alignedBuf[4:4100], - initial: 0, - want: 0x2e4a, - }, - { - name: "2-aligned", - data: alignedBuf[2:4098], - initial: 0, - want: 0xc702, - }, - { - name: "unaligned", - data: alignedBuf[1:4097], - initial: 0, - want: 0x3bc7, - }, - { - name: "unalignedAndOdd", - data: alignedBuf[1:4096], - initial: 0, - want: 0x3b13, - }, - { - name: "fe1282", - data: allFE[:1282], - initial: 0, - want: 0x7c7c, - }, - { - name: "fe", - data: allFE, - initial: 0, - want: 0x7e81, - }, - { - name: "maximum", - data: allOnes, - initial: 0, - want: 0xff00, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for _, fd := range archChecksumFuncs { - t.Run(fd.name, func(t *testing.T) { - if !fd.available { - t.Skip("can not run on this system") - } - if got := fd.f(tt.data, tt.initial); got != tt.want { - t.Errorf("%s checksum = %04x, want %04x", fd.name, got, tt.want) - } - }) - } - t.Run("reference", func(t *testing.T) { - if got := gvisorChecksum.Checksum(tt.data, tt.initial); got != tt.want { - t.Errorf("reference checksum = %04x, want %04x", got, tt.want) - } - }) - }) - } -} - -func TestPseudoHeaderChecksumNoFold(t *testing.T) { - tests := []struct { - name string - protocol uint8 - srcAddr []byte - dstAddr []byte - totalLen uint16 - want uint16 - }{ - { - name: "ipv4", - protocol: syscall.IPPROTO_TCP, - srcAddr: netip.MustParseAddr("192.168.1.1").AsSlice(), - dstAddr: netip.MustParseAddr("192.168.1.2").AsSlice(), - totalLen: 1492, - want: 0x892e, - }, - { - name: "ipv6", - protocol: syscall.IPPROTO_TCP, - srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(), - dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(), - totalLen: 1492, - want: 0x947f, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Run("pseudoHeaderChecksum32", func(t *testing.T) { - got := pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - if got != tt.want { - t.Errorf("got %04x, want %04x", got, tt.want) - } - }) - t.Run("pseudoHeaderChecksum64", func(t *testing.T) { - got := pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - if got != tt.want { - t.Errorf("got %04x, want %04x", got, tt.want) - } - }) - t.Run("reference", func(t *testing.T) { - got := header.PseudoHeaderChecksum( - tcpip.TransportProtocolNumber(tt.protocol), - tcpip.AddrFromSlice(tt.srcAddr), - tcpip.AddrFromSlice(tt.dstAddr), - tt.totalLen) - if got != tt.want { - t.Errorf("got %04x, want %04x", got, tt.want) - } - }) - }) - } -} - -func FuzzChecksum(f *testing.F) { - buf := getPageAlignedRandomBytes(1234, 65536) - - f.Add([]byte{}, uint16(0)) - f.Add([]byte{}, uint16(0x1234)) - f.Add([]byte{}, uint16(0)) - f.Add(buf[:15], uint16(0x1234)) - f.Add(buf[:256], uint16(0x1234)) - f.Add(buf[:1280], uint16(0x1234)) - f.Add(buf[:1288], uint16(0x1234)) - f.Add(buf[1:1050], uint16(0x1234)) - - f.Fuzz(func(t *testing.T, data []byte, initial uint16) { - want := gvisorChecksum.Checksum(data, initial) - - for _, fd := range archChecksumFuncs { - t.Run(fd.name, func(t *testing.T) { - if !fd.available { - t.Skip("can not run on this system") - } - if got := fd.f(data, initial); got != want { - t.Errorf("%s checksum = %04x, want %04x", fd.name, got, want) - } - }) - } - }) -} - -var result uint16 - -func BenchmarkChecksum(b *testing.B) { - offsets := []int{ // offsets from page alignment - 0, - 1, - 2, - 4, - 8, - 16, - } - lengths := []int{ - 0, - 7, - 15, - 16, - 31, - 64, - 90, - 95, - 128, - 256, - 512, - 1024, - 1240, - 1500, - 2048, - 4096, - 8192, - 9000, - 9001, - 16384, - 65536, - } - if !sort.IntsAreSorted(offsets) { - b.Fatal("offsets are not sorted") - } - largestLength := lengths[len(lengths)-1] - if !sort.IntsAreSorted(lengths) { - b.Fatal("lengths are not sorted") - } - largestOffset := lengths[len(offsets)-1] - alignedBuf := getPageAlignedRandomBytes(1, largestOffset+largestLength) - var r uint16 - for _, offset := range offsets { - name := fmt.Sprintf("%vAligned", offset) - if offset == 0 { - name = "pageAligned" - } - offsetBuf := alignedBuf[offset:] - b.Run(name, func(b *testing.B) { - for _, length := range lengths { - b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { - for _, fd := range archChecksumFuncs { - b.Run(fd.name, func(b *testing.B) { - if !fd.available { - b.Skip("can not run on this system") - } - b.SetBytes(int64(length)) - for i := 0; i < b.N; i++ { - r += fd.f(offsetBuf[:length], 0) - } - }) - } - }) - } - }) - } - result = r -} - -func BenchmarkPseudoHeaderChecksum(b *testing.B) { - tests := []struct { - name string - protocol uint8 - srcAddr []byte - dstAddr []byte - totalLen uint16 - want uint16 - }{ - { - name: "ipv4", - protocol: syscall.IPPROTO_TCP, - srcAddr: []byte{192, 168, 1, 1}, - dstAddr: []byte{192, 168, 1, 2}, - totalLen: 1492, - want: 0x892e, - }, - { - name: "ipv6", - protocol: syscall.IPPROTO_TCP, - srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(), - dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(), - totalLen: 1492, - want: 0x892e, - }, - } - for _, tt := range tests { - b.Run(tt.name, func(b *testing.B) { - b.Run("pseudoHeaderChecksum32", func(b *testing.B) { - for i := 0; i < b.N; i++ { - result += pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - } - }) - b.Run("pseudoHeaderChecksum64", func(b *testing.B) { - for i := 0; i < b.N; i++ { - result += pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen) - } - }) - }) - } -} diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go deleted file mode 100644 index 81f4d31..0000000 --- a/tun/netstack/examples/http_client.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "io" - "log" - "net/http" - "net/netip" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.28")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8")}, - 1420) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - err = dev.IpcSet(`private_key=087ec6e14bbed210e7215cdc73468dfa23f080a1bfb8665b2fd809bd99d28379 -public_key=c4c8e984c5322c8184c72265b92b250fdb63688705f504ba003c88f03393cf28 -allowed_ip=0.0.0.0/0 -endpoint=127.0.0.1:58120 -`) - err = dev.Up() - if err != nil { - log.Panic(err) - } - - client := http.Client{ - Transport: &http.Transport{ - DialContext: tnet.DialContext, - }, - } - resp, err := client.Get("http://192.168.4.29/") - if err != nil { - log.Panic(err) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Panic(err) - } - log.Println(string(body)) -} diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go deleted file mode 100644 index 30f4544..0000000 --- a/tun/netstack/examples/http_server.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "io" - "log" - "net" - "net/http" - "net/netip" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.29")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")}, - 1420, - ) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - dev.IpcSet(`private_key=003ed5d73b55806c30de3f8a7bdab38af13539220533055e635690b8b87ad641 -listen_port=58120 -public_key=f928d4f6c1b86c12f2562c10b07c555c5c57fd00f59e90c8d8d88767271cbf7c -allowed_ip=192.168.4.28/32 -persistent_keepalive_interval=25 -`) - dev.Up() - listener, err := tnet.ListenTCP(&net.TCPAddr{Port: 80}) - if err != nil { - log.Panicln(err) - } - http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { - log.Printf("> %s - %s - %s", request.RemoteAddr, request.URL.String(), request.UserAgent()) - io.WriteString(writer, "Hello from userspace TCP!") - }) - err = http.Serve(listener, nil) - if err != nil { - log.Panicln(err) - } -} diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go deleted file mode 100644 index fc991b1..0000000 --- a/tun/netstack/examples/ping_client.go +++ /dev/null @@ -1,75 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "bytes" - "log" - "math/rand" - "net/netip" - "time" - - "golang.org/x/net/icmp" - "golang.org/x/net/ipv4" - - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.29")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8")}, - 1420) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - dev.IpcSet(`private_key=a8dac1d8a70a751f0f699fb14ba1cff7b79cf4fbd8f09f44c6e6a90d0369604f -public_key=25123c5dcd3328ff645e4f2a3fce0d754400d3887a0cb7c56f0267e20fbf3c5b -endpoint=163.172.161.0:12912 -allowed_ip=0.0.0.0/0 -`) - err = dev.Up() - if err != nil { - log.Panic(err) - } - - socket, err := tnet.Dial("ping4", "zx2c4.com") - if err != nil { - log.Panic(err) - } - requestPing := icmp.Echo{ - Seq: rand.Intn(1 << 16), - Data: []byte("gopher burrow"), - } - icmpBytes, _ := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) - socket.SetReadDeadline(time.Now().Add(time.Second * 10)) - start := time.Now() - _, err = socket.Write(icmpBytes) - if err != nil { - log.Panic(err) - } - n, err := socket.Read(icmpBytes[:]) - if err != nil { - log.Panic(err) - } - replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) - if err != nil { - log.Panic(err) - } - replyPing, ok := replyPacket.Body.(*icmp.Echo) - if !ok { - log.Panicf("invalid reply type: %v", replyPacket) - } - if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { - log.Panicf("invalid ping reply: %v", replyPing) - } - log.Printf("Ping latency: %v", time.Since(start)) -} diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go deleted file mode 100644 index d8e70bb..0000000 --- a/tun/netstack/tun.go +++ /dev/null @@ -1,1055 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package netstack - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "net/netip" - "os" - "regexp" - "strconv" - "strings" - "syscall" - "time" - - "github.com/tailscale/wireguard-go/tun" - - "golang.org/x/net/dns/dnsmessage" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/link/channel" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" - "gvisor.dev/gvisor/pkg/tcpip/stack" - "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" - "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" - "gvisor.dev/gvisor/pkg/tcpip/transport/udp" - "gvisor.dev/gvisor/pkg/waiter" -) - -type netTun struct { - ep *channel.Endpoint - stack *stack.Stack - events chan tun.Event - incomingPacket chan *buffer.View - mtu int - dnsServers []netip.Addr - hasV4, hasV6 bool -} - -type Net netTun - -func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { - opts := stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, - TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, - HandleLocal: true, - } - dev := &netTun{ - ep: channel.New(1024, uint32(mtu), ""), - stack: stack.New(opts), - events: make(chan tun.Event, 10), - incomingPacket: make(chan *buffer.View), - dnsServers: dnsServers, - mtu: mtu, - } - sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default - tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) - } - dev.ep.AddNotify(dev) - tcpipErr = dev.stack.CreateNIC(1, dev.ep) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr) - } - for _, ip := range localAddresses { - var protoNumber tcpip.NetworkProtocolNumber - if ip.Is4() { - protoNumber = ipv4.ProtocolNumber - } else if ip.Is6() { - protoNumber = ipv6.ProtocolNumber - } - protoAddr := tcpip.ProtocolAddress{ - Protocol: protoNumber, - AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), - } - tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr) - } - if ip.Is4() { - dev.hasV4 = true - } else if ip.Is6() { - dev.hasV6 = true - } - } - if dev.hasV4 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) - } - if dev.hasV6 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) - } - - dev.events <- tun.EventUp - return dev, (*Net)(dev), nil -} - -func (tun *netTun) Name() (string, error) { - return "go", nil -} - -func (tun *netTun) File() *os.File { - return nil -} - -func (tun *netTun) Events() <-chan tun.Event { - return tun.events -} - -func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { - view, ok := <-tun.incomingPacket - if !ok { - return 0, os.ErrClosed - } - - n, err := view.Read(buf[0][offset:]) - if err != nil { - return 0, err - } - sizes[0] = n - return 1, nil -} - -func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { - for _, buf := range buf { - packet := buf[offset:] - if len(packet) == 0 { - continue - } - - pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) - switch packet[0] >> 4 { - case 4: - tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) - case 6: - tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb) - default: - return 0, syscall.EAFNOSUPPORT - } - } - return len(buf), nil -} - -func (tun *netTun) WriteNotify() { - pkt := tun.ep.Read() - if pkt.IsNil() { - return - } - - view := pkt.ToView() - pkt.DecRef() - - tun.incomingPacket <- view -} - -func (tun *netTun) Close() error { - tun.stack.RemoveNIC(1) - - if tun.events != nil { - close(tun.events) - } - - tun.ep.Close() - - if tun.incomingPacket != nil { - close(tun.incomingPacket) - } - - return nil -} - -func (tun *netTun) MTU() (int, error) { - return tun.mtu, nil -} - -func (tun *netTun) BatchSize() int { - return 1 -} - -func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { - var protoNumber tcpip.NetworkProtocolNumber - if endpoint.Addr().Is4() { - protoNumber = ipv4.ProtocolNumber - } else { - protoNumber = ipv6.ProtocolNumber - } - return tcpip.FullAddress{ - NIC: 1, - Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), - Port: endpoint.Port(), - }, protoNumber -} - -func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) { - fa, pn := convertToFullAddr(addr) - return gonet.DialContextTCP(ctx, net.stack, fa, pn) -} - -func (net *Net) DialContextTCP(ctx context.Context, addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialContextTCPAddrPort(ctx, netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) DialTCPAddrPort(addr netip.AddrPort) (*gonet.TCPConn, error) { - fa, pn := convertToFullAddr(addr) - return gonet.DialTCP(net.stack, fa, pn) -} - -func (net *Net) DialTCP(addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialTCPAddrPort(netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) ListenTCPAddrPort(addr netip.AddrPort) (*gonet.TCPListener, error) { - fa, pn := convertToFullAddr(addr) - return gonet.ListenTCP(net.stack, fa, pn) -} - -func (net *Net) ListenTCP(addr *net.TCPAddr) (*gonet.TCPListener, error) { - if addr == nil { - return net.ListenTCPAddrPort(netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.ListenTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) { - var lfa, rfa *tcpip.FullAddress - var pn tcpip.NetworkProtocolNumber - if laddr.IsValid() || laddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(laddr) - lfa = &addr - } - if raddr.IsValid() || raddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(raddr) - rfa = &addr - } - return gonet.DialUDP(net.stack, lfa, rfa, pn) -} - -func (net *Net) ListenUDPAddrPort(laddr netip.AddrPort) (*gonet.UDPConn, error) { - return net.DialUDPAddrPort(laddr, netip.AddrPort{}) -} - -func (net *Net) DialUDP(laddr, raddr *net.UDPAddr) (*gonet.UDPConn, error) { - var la, ra netip.AddrPort - if laddr != nil { - ip, _ := netip.AddrFromSlice(laddr.IP) - la = netip.AddrPortFrom(ip, uint16(laddr.Port)) - } - if raddr != nil { - ip, _ := netip.AddrFromSlice(raddr.IP) - ra = netip.AddrPortFrom(ip, uint16(raddr.Port)) - } - return net.DialUDPAddrPort(la, ra) -} - -func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) { - return net.DialUDP(laddr, nil) -} - -type PingConn struct { - laddr PingAddr - raddr PingAddr - wq waiter.Queue - ep tcpip.Endpoint - deadline *time.Timer -} - -type PingAddr struct{ addr netip.Addr } - -func (ia PingAddr) String() string { - return ia.addr.String() -} - -func (ia PingAddr) Network() string { - if ia.addr.Is4() { - return "ping4" - } else if ia.addr.Is6() { - return "ping6" - } - return "ping" -} - -func (ia PingAddr) Addr() netip.Addr { - return ia.addr -} - -func PingAddrFromAddr(addr netip.Addr) *PingAddr { - return &PingAddr{addr} -} - -func (net *Net) DialPingAddr(laddr, raddr netip.Addr) (*PingConn, error) { - if !laddr.IsValid() && !raddr.IsValid() { - return nil, errors.New("ping dial: invalid address") - } - v6 := laddr.Is6() || raddr.Is6() - bind := laddr.IsValid() - if !bind { - if v6 { - laddr = netip.IPv6Unspecified() - } else { - laddr = netip.IPv4Unspecified() - } - } - - tn := icmp.ProtocolNumber4 - pn := ipv4.ProtocolNumber - if v6 { - tn = icmp.ProtocolNumber6 - pn = ipv6.ProtocolNumber - } - - pc := &PingConn{ - laddr: PingAddr{laddr}, - deadline: time.NewTimer(time.Hour << 10), - } - pc.deadline.Stop() - - ep, tcpipErr := net.stack.NewEndpoint(tn, pn, &pc.wq) - if tcpipErr != nil { - return nil, fmt.Errorf("ping socket: endpoint: %s", tcpipErr) - } - pc.ep = ep - - if bind { - fa, _ := convertToFullAddr(netip.AddrPortFrom(laddr, 0)) - if tcpipErr = pc.ep.Bind(fa); tcpipErr != nil { - return nil, fmt.Errorf("ping bind: %s", tcpipErr) - } - } - - if raddr.IsValid() { - pc.raddr = PingAddr{raddr} - fa, _ := convertToFullAddr(netip.AddrPortFrom(raddr, 0)) - if tcpipErr = pc.ep.Connect(fa); tcpipErr != nil { - return nil, fmt.Errorf("ping connect: %s", tcpipErr) - } - } - - return pc, nil -} - -func (net *Net) ListenPingAddr(laddr netip.Addr) (*PingConn, error) { - return net.DialPingAddr(laddr, netip.Addr{}) -} - -func (net *Net) DialPing(laddr, raddr *PingAddr) (*PingConn, error) { - var la, ra netip.Addr - if laddr != nil { - la = laddr.addr - } - if raddr != nil { - ra = raddr.addr - } - return net.DialPingAddr(la, ra) -} - -func (net *Net) ListenPing(laddr *PingAddr) (*PingConn, error) { - var la netip.Addr - if laddr != nil { - la = laddr.addr - } - return net.ListenPingAddr(la) -} - -func (pc *PingConn) LocalAddr() net.Addr { - return pc.laddr -} - -func (pc *PingConn) RemoteAddr() net.Addr { - return pc.raddr -} - -func (pc *PingConn) Close() error { - pc.deadline.Reset(0) - pc.ep.Close() - return nil -} - -func (pc *PingConn) SetWriteDeadline(t time.Time) error { - return errors.New("not implemented") -} - -func (pc *PingConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var na netip.Addr - switch v := addr.(type) { - case *PingAddr: - na = v.addr - case *net.IPAddr: - na, _ = netip.AddrFromSlice(v.IP) - default: - return 0, fmt.Errorf("ping write: wrong net.Addr type") - } - if !((na.Is4() && pc.laddr.addr.Is4()) || (na.Is6() && pc.laddr.addr.Is6())) { - return 0, fmt.Errorf("ping write: mismatched protocols") - } - - buf := bytes.NewReader(p) - rfa, _ := convertToFullAddr(netip.AddrPortFrom(na, 0)) - // won't block, no deadlines - n64, tcpipErr := pc.ep.Write(buf, tcpip.WriteOptions{ - To: &rfa, - }) - if tcpipErr != nil { - return int(n64), fmt.Errorf("ping write: %s", tcpipErr) - } - - return int(n64), nil -} - -func (pc *PingConn) Write(p []byte) (n int, err error) { - return pc.WriteTo(p, &pc.raddr) -} - -func (pc *PingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - e, notifyCh := waiter.NewChannelEntry(waiter.EventIn) - pc.wq.EventRegister(&e) - defer pc.wq.EventUnregister(&e) - - select { - case <-pc.deadline.C: - return 0, nil, os.ErrDeadlineExceeded - case <-notifyCh: - } - - w := tcpip.SliceWriter(p) - - res, tcpipErr := pc.ep.Read(&w, tcpip.ReadOptions{ - NeedRemoteAddr: true, - }) - if tcpipErr != nil { - return 0, nil, fmt.Errorf("ping read: %s", tcpipErr) - } - - remoteAddr, _ := netip.AddrFromSlice(res.RemoteAddr.Addr.AsSlice()) - return res.Count, &PingAddr{remoteAddr}, nil -} - -func (pc *PingConn) Read(p []byte) (n int, err error) { - n, _, err = pc.ReadFrom(p) - return -} - -func (pc *PingConn) SetDeadline(t time.Time) error { - // pc.SetWriteDeadline is unimplemented - - return pc.SetReadDeadline(t) -} - -func (pc *PingConn) SetReadDeadline(t time.Time) error { - pc.deadline.Reset(time.Until(t)) - return nil -} - -var ( - errNoSuchHost = errors.New("no such host") - errLameReferral = errors.New("lame referral") - errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message") - errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message") - errServerMisbehaving = errors.New("server misbehaving") - errInvalidDNSResponse = errors.New("invalid DNS response") - errNoAnswerFromDNSServer = errors.New("no answer from DNS server") - errServerTemporarilyMisbehaving = errors.New("server misbehaving") - errCanceled = errors.New("operation was canceled") - errTimeout = errors.New("i/o timeout") - errNumericPort = errors.New("port must be numeric") - errNoSuitableAddress = errors.New("no suitable address found") - errMissingAddress = errors.New("missing address") -) - -func (net *Net) LookupHost(host string) (addrs []string, err error) { - return net.LookupContextHost(context.Background(), host) -} - -func isDomainName(s string) bool { - l := len(s) - if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { - return false - } - last := byte('.') - nonNumeric := false - partlen := 0 - for i := 0; i < len(s); i++ { - c := s[i] - switch { - default: - return false - case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': - nonNumeric = true - partlen++ - case '0' <= c && c <= '9': - partlen++ - case c == '-': - if last == '.' { - return false - } - partlen++ - nonNumeric = true - case c == '.': - if last == '.' || last == '-' { - return false - } - if partlen > 63 || partlen == 0 { - return false - } - partlen = 0 - } - last = c - } - if last == '-' || partlen > 63 { - return false - } - return nonNumeric -} - -func randU16() uint16 { - var b [2]byte - _, err := rand.Read(b[:]) - if err != nil { - panic(err) - } - return binary.LittleEndian.Uint16(b[:]) -} - -func newRequest(q dnsmessage.Question) (id uint16, udpReq, tcpReq []byte, err error) { - id = randU16() - b := dnsmessage.NewBuilder(make([]byte, 2, 514), dnsmessage.Header{ID: id, RecursionDesired: true}) - b.EnableCompression() - if err := b.StartQuestions(); err != nil { - return 0, nil, nil, err - } - if err := b.Question(q); err != nil { - return 0, nil, nil, err - } - tcpReq, err = b.Finish() - udpReq = tcpReq[2:] - l := len(tcpReq) - 2 - tcpReq[0] = byte(l >> 8) - tcpReq[1] = byte(l) - return id, udpReq, tcpReq, err -} - -func equalASCIIName(x, y dnsmessage.Name) bool { - if x.Length != y.Length { - return false - } - for i := 0; i < int(x.Length); i++ { - a := x.Data[i] - b := y.Data[i] - if 'A' <= a && a <= 'Z' { - a += 0x20 - } - if 'A' <= b && b <= 'Z' { - b += 0x20 - } - if a != b { - return false - } - } - return true -} - -func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQues dnsmessage.Question) bool { - if !respHdr.Response { - return false - } - if reqID != respHdr.ID { - return false - } - if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) { - return false - } - return true -} - -func dnsPacketRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { - if _, err := c.Write(b); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - b = make([]byte, 512) - for { - n, err := c.Read(b) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - var p dnsmessage.Parser - h, err := p.Start(b[:n]) - if err != nil { - continue - } - q, err := p.Question() - if err != nil || !checkResponse(id, query, h, q) { - continue - } - return p, h, nil - } -} - -func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { - if _, err := c.Write(b); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - b = make([]byte, 1280) - if _, err := io.ReadFull(c, b[:2]); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - l := int(b[0])<<8 | int(b[1]) - if l > len(b) { - b = make([]byte, l) - } - n, err := io.ReadFull(c, b[:l]) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - var p dnsmessage.Parser - h, err := p.Start(b[:n]) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage - } - q, err := p.Question() - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage - } - if !checkResponse(id, query, h, q) { - return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse - } - return p, h, nil -} - -func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { - q.Class = dnsmessage.ClassINET - id, udpReq, tcpReq, err := newRequest(q) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotMarshalDNSMessage - } - - for _, useUDP := range []bool{true, false} { - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) - defer cancel() - - var c net.Conn - var err error - if useUDP { - c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, netip.AddrPortFrom(server, 53)) - } else { - c, err = tnet.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(server, 53)) - } - - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - if d, ok := ctx.Deadline(); ok && !d.IsZero() { - err := c.SetDeadline(d) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - } - var p dnsmessage.Parser - var h dnsmessage.Header - if useUDP { - p, h, err = dnsPacketRoundTrip(c, id, q, udpReq) - } else { - p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq) - } - c.Close() - if err != nil { - if err == context.Canceled { - err = errCanceled - } else if err == context.DeadlineExceeded { - err = errTimeout - } - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone { - return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse - } - if h.Truncated { - continue - } - return p, h, nil - } - return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer -} - -func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error { - if h.RCode == dnsmessage.RCodeNameError { - return errNoSuchHost - } - _, err := p.AnswerHeader() - if err != nil && err != dnsmessage.ErrSectionDone { - return errCannotUnmarshalDNSMessage - } - if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone { - return errLameReferral - } - if h.RCode != dnsmessage.RCodeSuccess && h.RCode != dnsmessage.RCodeNameError { - if h.RCode == dnsmessage.RCodeServerFailure { - return errServerTemporarilyMisbehaving - } - return errServerMisbehaving - } - return nil -} - -func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error { - for { - h, err := p.AnswerHeader() - if err == dnsmessage.ErrSectionDone { - return errNoSuchHost - } - if err != nil { - return errCannotUnmarshalDNSMessage - } - if h.Type == qtype { - return nil - } - if err := p.SkipAnswer(); err != nil { - return errCannotUnmarshalDNSMessage - } - } -} - -func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { - var lastErr error - - n, err := dnsmessage.NewName(name) - if err != nil { - return dnsmessage.Parser{}, "", errCannotMarshalDNSMessage - } - q := dnsmessage.Question{ - Name: n, - Type: qtype, - Class: dnsmessage.ClassINET, - } - - for i := 0; i < 2; i++ { - for _, server := range tnet.dnsServers { - p, h, err := tnet.exchange(ctx, server, q, time.Second*5) - if err != nil { - dnsErr := &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - dnsErr.IsTimeout = true - } - if _, ok := err.(*net.OpError); ok { - dnsErr.IsTemporary = true - } - lastErr = dnsErr - continue - } - - if err := checkHeader(&p, h); err != nil { - dnsErr := &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if err == errServerTemporarilyMisbehaving { - dnsErr.IsTemporary = true - } - if err == errNoSuchHost { - dnsErr.IsNotFound = true - return p, server.String(), dnsErr - } - lastErr = dnsErr - continue - } - - err = skipToAnswer(&p, qtype) - if err == nil { - return p, server.String(), nil - } - lastErr = &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if err == errNoSuchHost { - lastErr.(*net.DNSError).IsNotFound = true - return p, server.String(), lastErr - } - } - } - return dnsmessage.Parser{}, "", lastErr -} - -func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { - if host == "" || (!tnet.hasV6 && !tnet.hasV4) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} - } - zlen := len(host) - if strings.IndexByte(host, ':') != -1 { - if zidx := strings.LastIndexByte(host, '%'); zidx != -1 { - zlen = zidx - } - } - if ip, err := netip.ParseAddr(host[:zlen]); err == nil { - return []string{ip.String()}, nil - } - - if !isDomainName(host) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} - } - type result struct { - p dnsmessage.Parser - server string - error - } - var addrsV4, addrsV6 []netip.Addr - lanes := 0 - if tnet.hasV4 { - lanes++ - } - if tnet.hasV6 { - lanes++ - } - lane := make(chan result, lanes) - var lastErr error - if tnet.hasV4 { - go func() { - p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeA) - lane <- result{p, server, err} - }() - } - if tnet.hasV6 { - go func() { - p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeAAAA) - lane <- result{p, server, err} - }() - } - for l := 0; l < lanes; l++ { - result := <-lane - if result.error != nil { - if lastErr == nil { - lastErr = result.error - } - continue - } - - loop: - for { - h, err := result.p.AnswerHeader() - if err != nil && err != dnsmessage.ErrSectionDone { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - } - if err != nil { - break - } - switch h.Type { - case dnsmessage.TypeA: - a, err := result.p.AResource() - if err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - addrsV4 = append(addrsV4, netip.AddrFrom4(a.A)) - - case dnsmessage.TypeAAAA: - aaaa, err := result.p.AAAAResource() - if err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA)) - - default: - if err := result.p.SkipAnswer(); err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - continue - } - } - } - // We don't do RFC6724. Instead just put V6 addresses first if an IPv6 address is enabled - var addrs []netip.Addr - if tnet.hasV6 { - addrs = append(addrsV6, addrsV4...) - } else { - addrs = append(addrsV4, addrsV6...) - } - - if len(addrs) == 0 && lastErr != nil { - return nil, lastErr - } - saddrs := make([]string, 0, len(addrs)) - for _, ip := range addrs { - saddrs = append(saddrs, ip.String()) - } - return saddrs, nil -} - -func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) { - if deadline.IsZero() { - return deadline, nil - } - timeRemaining := deadline.Sub(now) - if timeRemaining <= 0 { - return time.Time{}, errTimeout - } - timeout := timeRemaining / time.Duration(addrsRemaining) - const saneMinimum = 2 * time.Second - if timeout < saneMinimum { - if timeRemaining < saneMinimum { - timeout = timeRemaining - } else { - timeout = saneMinimum - } - } - return now.Add(timeout), nil -} - -var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`) - -func (tnet *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if ctx == nil { - panic("nil context") - } - var acceptV4, acceptV6 bool - matches := protoSplitter.FindStringSubmatch(network) - if matches == nil { - return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)} - } else if len(matches[2]) == 0 { - acceptV4 = true - acceptV6 = true - } else { - acceptV4 = matches[2][0] == '4' - acceptV6 = !acceptV4 - } - var host string - var port int - if matches[1] == "ping" { - host = address - } else { - var sport string - var err error - host, sport, err = net.SplitHostPort(address) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - port, err = strconv.Atoi(sport) - if err != nil || port < 0 || port > 65535 { - return nil, &net.OpError{Op: "dial", Err: errNumericPort} - } - } - allAddr, err := tnet.LookupContextHost(ctx, host) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - var addrs []netip.AddrPort - for _, addr := range allAddr { - ip, err := netip.ParseAddr(addr) - if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) { - addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port))) - } - } - if len(addrs) == 0 && len(allAddr) != 0 { - return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress} - } - - var firstErr error - for i, addr := range addrs { - select { - case <-ctx.Done(): - err := ctx.Err() - if err == context.Canceled { - err = errCanceled - } else if err == context.DeadlineExceeded { - err = errTimeout - } - return nil, &net.OpError{Op: "dial", Err: err} - default: - } - - dialCtx := ctx - if deadline, hasDeadline := ctx.Deadline(); hasDeadline { - partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i) - if err != nil { - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: err} - } - break - } - if partialDeadline.Before(deadline) { - var cancel context.CancelFunc - dialCtx, cancel = context.WithDeadline(ctx, partialDeadline) - defer cancel() - } - } - - var c net.Conn - switch matches[1] { - case "tcp": - c, err = tnet.DialContextTCPAddrPort(dialCtx, addr) - case "udp": - c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, addr) - case "ping": - c, err = tnet.DialPingAddr(netip.Addr{}, addr.Addr()) - } - if err == nil { - return c, nil - } - if firstErr == nil { - firstErr = err - } - } - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: errMissingAddress} - } - return nil, firstErr -} - -func (tnet *Net) Dial(network, address string) (net.Conn, error) { - return tnet.DialContext(context.Background(), network, address) -} diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go deleted file mode 100644 index 4070378..0000000 --- a/tun/offload_linux_test.go +++ /dev/null @@ -1,764 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package tun - -import ( - "net/netip" - "testing" - - "github.com/tailscale/wireguard-go/conn" - "golang.org/x/sys/unix" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" -) - -const ( - offset = virtioNetHdrLen -) - -var ( - ip4PortA = netip.MustParseAddrPort("192.0.2.1:1") - ip4PortB = netip.MustParseAddrPort("192.0.2.2:1") - ip4PortC = netip.MustParseAddrPort("192.0.2.3:1") - ip6PortA = netip.MustParseAddrPort("[2001:db8::1]:1") - ip6PortB = netip.MustParseAddrPort("[2001:db8::2]:1") - ip6PortC = netip.MustParseAddrPort("[2001:db8::3]:1") -) - -func udp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv4Fields)) []byte { - totalLen := 28 + payloadLen - b := make([]byte, offset+int(totalLen), 65535) - ipv4H := header.IPv4(b[offset:]) - srcAs4 := srcIPPort.Addr().As4() - dstAs4 := dstIPPort.Addr().As4() - ipFields := &header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), - DstAddr: tcpip.AddrFromSlice(dstAs4[:]), - Protocol: unix.IPPROTO_UDP, - TTL: 64, - TotalLength: uint16(totalLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv4H.Encode(ipFields) - udpH := header.UDP(b[offset+20:]) - udpH.Encode(&header.UDPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - Length: uint16(payloadLen + udphLen), - }) - ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(udphLen+payloadLen)) - udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) - return b -} - -func udp6Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { - return udp6PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) -} - -func udp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv6Fields)) []byte { - totalLen := 48 + payloadLen - b := make([]byte, offset+int(totalLen), 65535) - ipv6H := header.IPv6(b[offset:]) - srcAs16 := srcIPPort.Addr().As16() - dstAs16 := dstIPPort.Addr().As16() - ipFields := &header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), - DstAddr: tcpip.AddrFromSlice(dstAs16[:]), - TransportProtocol: unix.IPPROTO_UDP, - HopLimit: 64, - PayloadLength: uint16(payloadLen + udphLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv6H.Encode(ipFields) - udpH := header.UDP(b[offset+40:]) - udpH.Encode(&header.UDPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - Length: uint16(payloadLen + udphLen), - }) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(udphLen+payloadLen)) - udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) - return b -} - -func udp4Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { - return udp4PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) -} - -func tcp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32, ipFn func(*header.IPv4Fields)) []byte { - totalLen := 40 + segmentSize - b := make([]byte, offset+int(totalLen), 65535) - ipv4H := header.IPv4(b[offset:]) - srcAs4 := srcIPPort.Addr().As4() - dstAs4 := dstIPPort.Addr().As4() - ipFields := &header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), - DstAddr: tcpip.AddrFromSlice(dstAs4[:]), - Protocol: unix.IPPROTO_TCP, - TTL: 64, - TotalLength: uint16(totalLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv4H.Encode(ipFields) - tcpH := header.TCP(b[offset+20:]) - tcpH.Encode(&header.TCPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - SeqNum: seq, - AckNum: 1, - DataOffset: 20, - Flags: flags, - WindowSize: 3000, - }) - ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(20+segmentSize)) - tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) - return b -} - -func tcp4Packet(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32) []byte { - return tcp4PacketMutateIPFields(srcIPPort, dstIPPort, flags, segmentSize, seq, nil) -} - -func tcp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32, ipFn func(*header.IPv6Fields)) []byte { - totalLen := 60 + segmentSize - b := make([]byte, offset+int(totalLen), 65535) - ipv6H := header.IPv6(b[offset:]) - srcAs16 := srcIPPort.Addr().As16() - dstAs16 := dstIPPort.Addr().As16() - ipFields := &header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), - DstAddr: tcpip.AddrFromSlice(dstAs16[:]), - TransportProtocol: unix.IPPROTO_TCP, - HopLimit: 64, - PayloadLength: uint16(segmentSize + 20), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv6H.Encode(ipFields) - tcpH := header.TCP(b[offset+40:]) - tcpH.Encode(&header.TCPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - SeqNum: seq, - AckNum: 1, - DataOffset: 20, - Flags: flags, - WindowSize: 3000, - }) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(20+segmentSize)) - tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) - return b -} - -func tcp6Packet(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32) []byte { - return tcp6PacketMutateIPFields(srcIPPort, dstIPPort, flags, segmentSize, seq, nil) -} - -func Test_handleVirtioRead(t *testing.T) { - tests := []struct { - name string - hdr virtioNetHdr - pktIn []byte - wantLens []int - wantErr bool - }{ - { - "tcp4", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_TCPV4, - gsoSize: 100, - hdrLen: 40, - csumStart: 20, - csumOffset: 16, - }, - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck|header.TCPFlagPsh, 200, 1), - []int{140, 140}, - false, - }, - { - "tcp6", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_TCPV6, - gsoSize: 100, - hdrLen: 60, - csumStart: 40, - csumOffset: 16, - }, - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck|header.TCPFlagPsh, 200, 1), - []int{160, 160}, - false, - }, - { - "udp4", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - gsoSize: 100, - hdrLen: 28, - csumStart: 20, - csumOffset: 6, - }, - udp4Packet(ip4PortA, ip4PortB, 200), - []int{128, 128}, - false, - }, - { - "udp6", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - gsoSize: 100, - hdrLen: 48, - csumStart: 40, - csumOffset: 6, - }, - udp6Packet(ip6PortA, ip6PortB, 200), - []int{148, 148}, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - out := make([][]byte, conn.IdealBatchSize) - sizes := make([]int, conn.IdealBatchSize) - for i := range out { - out[i] = make([]byte, 65535) - } - tt.hdr.encode(tt.pktIn) - n, err := handleVirtioRead(tt.pktIn, out, sizes, offset) - if err != nil { - if tt.wantErr { - return - } - t.Fatalf("got err: %v", err) - } - if n != len(tt.wantLens) { - t.Fatalf("got %d packets, wanted %d", n, len(tt.wantLens)) - } - for i := range tt.wantLens { - if tt.wantLens[i] != sizes[i] { - t.Fatalf("wantLens[%d]: %d != outSizes: %d", i, tt.wantLens[i], sizes[i]) - } - } - }) - } -} - -func flipTCP4Checksum(b []byte) []byte { - at := virtioNetHdrLen + 20 + 16 // 20 byte ipv4 header; tcp csum offset is 16 - b[at] ^= 0xFF - b[at+1] ^= 0xFF - return b -} - -func flipUDP4Checksum(b []byte) []byte { - at := virtioNetHdrLen + 20 + 6 // 20 byte ipv4 header; udp csum offset is 6 - b[at] ^= 0xFF - b[at+1] ^= 0xFF - return b -} - -func Fuzz_handleGRO(f *testing.F) { - pkt0 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1) - pkt1 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101) - pkt2 := tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201) - pkt3 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1) - pkt4 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101) - pkt5 := tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201) - pkt6 := udp4Packet(ip4PortA, ip4PortB, 100) - pkt7 := udp4Packet(ip4PortA, ip4PortB, 100) - pkt8 := udp4Packet(ip4PortA, ip4PortC, 100) - pkt9 := udp6Packet(ip6PortA, ip6PortB, 100) - pkt10 := udp6Packet(ip6PortA, ip6PortB, 100) - pkt11 := udp6Packet(ip6PortA, ip6PortC, 100) - f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, 0, offset) - f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, gro int, offset int) { - pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11} - toWrite := make([]int, 0, len(pkts)) - handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), groDisablementFlags(gro), &toWrite) - if len(toWrite) > len(pkts) { - t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts)) - } - seenWriteI := make(map[int]bool) - for _, writeI := range toWrite { - if writeI < 0 || writeI > len(pkts)-1 { - t.Errorf("toWrite value (%d) outside bounds of len(pkts): %d", writeI, len(pkts)) - } - if seenWriteI[writeI] { - t.Errorf("duplicate toWrite value: %d", writeI) - } - seenWriteI[writeI] = true - } - }) -} - -func Test_handleGRO(t *testing.T) { - tests := []struct { - name string - pktsIn [][]byte - gro groDisablementFlags - wantToWrite []int - wantLens []int - wantErr bool - }{ - { - "multiple protocols and flows", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 - tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - }, - 0, - []int{0, 1, 2, 4, 5, 7, 9}, - []int{240, 228, 128, 140, 260, 160, 248}, - false, - }, - { - "multiple protocols and flows no UDP GRO", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 - tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - }, - udpGRODisabled, - []int{0, 1, 2, 4, 5, 7, 8, 9, 10}, - []int{240, 128, 128, 140, 260, 160, 128, 148, 148}, - false, - }, - { - "PSH interleaved", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck|header.TCPFlagPsh, 100, 101), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 301), // v4 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck|header.TCPFlagPsh, 100, 101), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 201), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 301), // v6 flow 1 - }, - 0, - []int{0, 2, 4, 6}, - []int{240, 240, 260, 260}, - false, - }, - { - "coalesceItemInvalidCSum", - [][]byte{ - flipTCP4Checksum(tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)), // v4 flow 1 seq 1 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 seq 101 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 - flipUDP4Checksum(udp4Packet(ip4PortA, ip4PortB, 100)), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4Packet(ip4PortA, ip4PortB, 100), - }, - 0, - []int{0, 1, 3, 4}, - []int{140, 240, 128, 228}, - false, - }, - { - "out of order", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 seq 101 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 seq 1 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 - }, - 0, - []int{0}, - []int{340}, - false, - }, - { - "unequal TTL", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.TTL++ - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.TTL++ - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal ToS", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.TOS++ - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.TOS++ - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal flags more fragments set", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.Flags = 1 - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.Flags = 1 - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal flags DF set", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.Flags = 2 - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.Flags = 2 - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "ipv6 unequal hop limit", - [][]byte{ - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), - tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { - fields.HopLimit++ - }), - udp6Packet(ip6PortA, ip6PortB, 100), - udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { - fields.HopLimit++ - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{160, 160, 148, 148}, - false, - }, - { - "ipv6 unequal traffic class", - [][]byte{ - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), - tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { - fields.TrafficClass++ - }), - udp6Packet(ip6PortA, ip6PortB, 100), - udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { - fields.TrafficClass++ - }), - }, - 0, - []int{0, 1, 2, 3}, - []int{160, 160, 148, 148}, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - toWrite := make([]int, 0, len(tt.pktsIn)) - err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.gro, &toWrite) - if err != nil { - if tt.wantErr { - return - } - t.Fatalf("got err: %v", err) - } - if len(toWrite) != len(tt.wantToWrite) { - t.Fatalf("got %d packets, wanted %d", len(toWrite), len(tt.wantToWrite)) - } - for i, pktI := range tt.wantToWrite { - if tt.wantToWrite[i] != toWrite[i] { - t.Fatalf("wantToWrite[%d]: %d != toWrite: %d", i, tt.wantToWrite[i], toWrite[i]) - } - if tt.wantLens[i] != len(tt.pktsIn[pktI][offset:]) { - t.Errorf("wanted len %d packet at %d, got: %d", tt.wantLens[i], i, len(tt.pktsIn[pktI][offset:])) - } - } - }) - } -} - -func Test_packetIsGROCandidate(t *testing.T) { - tcp4 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] - tcp4TooShort := tcp4[:39] - ip4InvalidHeaderLen := make([]byte, len(tcp4)) - copy(ip4InvalidHeaderLen, tcp4) - ip4InvalidHeaderLen[0] = 0x46 - ip4InvalidProtocol := make([]byte, len(tcp4)) - copy(ip4InvalidProtocol, tcp4) - ip4InvalidProtocol[9] = unix.IPPROTO_GRE - - tcp6 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] - tcp6TooShort := tcp6[:59] - ip6InvalidProtocol := make([]byte, len(tcp6)) - copy(ip6InvalidProtocol, tcp6) - ip6InvalidProtocol[6] = unix.IPPROTO_GRE - - udp4 := udp4Packet(ip4PortA, ip4PortB, 100)[virtioNetHdrLen:] - udp4TooShort := udp4[:27] - - udp6 := udp6Packet(ip6PortA, ip6PortB, 100)[virtioNetHdrLen:] - udp6TooShort := udp6[:47] - - tests := []struct { - name string - b []byte - gro groDisablementFlags - want groCandidateType - }{ - { - "tcp4", - tcp4, - 0, - tcp4GROCandidate, - }, - { - "tcp4 no support", - tcp4, - tcpGRODisabled, - notGROCandidate, - }, - { - "tcp6", - tcp6, - 0, - tcp6GROCandidate, - }, - { - "tcp6 no support", - tcp6, - tcpGRODisabled, - notGROCandidate, - }, - { - "udp4", - udp4, - 0, - udp4GROCandidate, - }, - { - "udp4 no support", - udp4, - udpGRODisabled, - notGROCandidate, - }, - { - "udp6", - udp6, - 0, - udp6GROCandidate, - }, - { - "udp6 no support", - udp6, - udpGRODisabled, - notGROCandidate, - }, - { - "udp4 too short", - udp4TooShort, - 0, - notGROCandidate, - }, - { - "udp6 too short", - udp6TooShort, - 0, - notGROCandidate, - }, - { - "tcp4 too short", - tcp4TooShort, - 0, - notGROCandidate, - }, - { - "tcp6 too short", - tcp6TooShort, - 0, - notGROCandidate, - }, - { - "invalid IP version", - []byte{0x00}, - 0, - notGROCandidate, - }, - { - "invalid IP header len", - ip4InvalidHeaderLen, - 0, - notGROCandidate, - }, - { - "ip4 invalid protocol", - ip4InvalidProtocol, - 0, - notGROCandidate, - }, - { - "ip6 invalid protocol", - ip6InvalidProtocol, - 0, - notGROCandidate, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := packetIsGROCandidate(tt.b, tt.gro); got != tt.want { - t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_udpPacketsCanCoalesce(t *testing.T) { - udp4a := udp4Packet(ip4PortA, ip4PortB, 100) - udp4b := udp4Packet(ip4PortA, ip4PortB, 100) - udp4c := udp4Packet(ip4PortA, ip4PortB, 110) - - type args struct { - pkt []byte - iphLen uint8 - gsoSize uint16 - item udpGROItem - bufs [][]byte - bufsOffset int - } - tests := []struct { - name string - args args - want canCoalesce - }{ - { - "coalesceAppend equal gso", - args{ - pkt: udp4a[offset:], - iphLen: 20, - gsoSize: 100, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4b, - }, - bufsOffset: offset, - }, - coalesceAppend, - }, - { - "coalesceAppend smaller gso", - args{ - pkt: udp4a[offset : len(udp4a)-90], - iphLen: 20, - gsoSize: 10, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4b, - }, - bufsOffset: offset, - }, - coalesceAppend, - }, - { - "coalesceUnavailable smaller gso previously appended", - args{ - pkt: udp4a[offset:], - iphLen: 20, - gsoSize: 100, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4c, - udp4b, - }, - bufsOffset: offset, - }, - coalesceUnavailable, - }, - { - "coalesceUnavailable larger following smaller", - args{ - pkt: udp4c[offset:], - iphLen: 20, - gsoSize: 110, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4c, - }, - bufsOffset: offset, - }, - coalesceUnavailable, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := udpPacketsCanCoalesce(tt.args.pkt, tt.args.iphLen, tt.args.gsoSize, tt.args.item, tt.args.bufs, tt.args.bufsOffset); got != tt.want { - t.Errorf("udpPacketsCanCoalesce() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/tun/offload_test.go b/tun/offload_test.go deleted file mode 100644 index 82a37b9..0000000 --- a/tun/offload_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package tun - -import ( - "net/netip" - "testing" - - "github.com/tailscale/wireguard-go/conn" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" -) - -func Fuzz_GSOSplit(f *testing.F) { - const segmentSize = 100 - - tcpFields := &header.TCPFields{ - SrcPort: 1, - DstPort: 1, - SeqNum: 1, - AckNum: 1, - DataOffset: 20, - Flags: header.TCPFlagAck | header.TCPFlagPsh, - WindowSize: 3000, - } - udpFields := &header.UDPFields{ - SrcPort: 1, - DstPort: 1, - Length: 8 + segmentSize, - } - - gsoTCPv4 := make([]byte, 20+20+segmentSize) - header.IPv4(gsoTCPv4).Encode(&header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.1").AsSlice()), - DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.2").AsSlice()), - Protocol: ipProtoTCP, - TTL: 64, - TotalLength: uint16(len(gsoTCPv4)), - }) - header.TCP(gsoTCPv4[20:]).Encode(tcpFields) - - gsoUDPv4 := make([]byte, 20+8+segmentSize) - header.IPv4(gsoUDPv4).Encode(&header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.1").AsSlice()), - DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.2").AsSlice()), - Protocol: ipProtoUDP, - TTL: 64, - TotalLength: uint16(len(gsoUDPv4)), - }) - header.UDP(gsoTCPv4[20:]).Encode(udpFields) - - gsoTCPv6 := make([]byte, 40+20+segmentSize) - header.IPv6(gsoTCPv6).Encode(&header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::1").AsSlice()), - DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::2").AsSlice()), - TransportProtocol: ipProtoTCP, - HopLimit: 64, - PayloadLength: uint16(20 + segmentSize), - }) - header.TCP(gsoTCPv6[40:]).Encode(tcpFields) - - gsoUDPv6 := make([]byte, 40+8+segmentSize) - header.IPv6(gsoUDPv6).Encode(&header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::1").AsSlice()), - DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::2").AsSlice()), - TransportProtocol: ipProtoUDP, - HopLimit: 64, - PayloadLength: uint16(8 + segmentSize), - }) - header.UDP(gsoUDPv6[20:]).Encode(udpFields) - - out := make([][]byte, conn.IdealBatchSize) - for i := range out { - out[i] = make([]byte, 65535) - } - sizes := make([]int, conn.IdealBatchSize) - - f.Add(gsoTCPv4, int(GSOTCPv4), uint16(40), uint16(20), uint16(16), uint16(100), false) - f.Add(gsoUDPv4, int(GSOUDPL4), uint16(28), uint16(20), uint16(6), uint16(100), false) - f.Add(gsoTCPv6, int(GSOTCPv6), uint16(60), uint16(40), uint16(16), uint16(100), false) - f.Add(gsoUDPv6, int(GSOUDPL4), uint16(48), uint16(40), uint16(6), uint16(100), false) - - f.Fuzz(func(t *testing.T, pkt []byte, gsoType int, hdrLen, csumStart, csumOffset, gsoSize uint16, needsCsum bool) { - options := GSOOptions{ - GSOType: GSOType(gsoType), - HdrLen: hdrLen, - CsumStart: csumStart, - CsumOffset: csumOffset, - GSOSize: gsoSize, - NeedsCsum: needsCsum, - } - n, _ := GSOSplit(pkt, options, out, sizes, 0) - if n > len(sizes) { - t.Errorf("n (%d) > len(sizes): %d", n, len(sizes)) - } - }) -} diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go deleted file mode 100644 index e7507c2..0000000 --- a/tun/tuntest/tuntest.go +++ /dev/null @@ -1,155 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. - */ - -package tuntest - -import ( - "encoding/binary" - "io" - "net/netip" - "os" - - "github.com/tailscale/wireguard-go/tun" -) - -func Ping(dst, src netip.Addr) []byte { - localPort := uint16(1337) - seq := uint16(0) - - payload := make([]byte, 4) - binary.BigEndian.PutUint16(payload[0:], localPort) - binary.BigEndian.PutUint16(payload[2:], seq) - - return genICMPv4(payload, dst, src) -} - -// Checksum is the "internet checksum" from https://tools.ietf.org/html/rfc1071. -func checksum(buf []byte, initial uint16) uint16 { - v := uint32(initial) - for i := 0; i < len(buf)-1; i += 2 { - v += uint32(binary.BigEndian.Uint16(buf[i:])) - } - if len(buf)%2 == 1 { - v += uint32(buf[len(buf)-1]) << 8 - } - for v > 0xffff { - v = (v >> 16) + (v & 0xffff) - } - return ^uint16(v) -} - -func genICMPv4(payload []byte, dst, src netip.Addr) []byte { - const ( - icmpv4ProtocolNumber = 1 - icmpv4Echo = 8 - icmpv4ChecksumOffset = 2 - icmpv4Size = 8 - ipv4Size = 20 - ipv4TotalLenOffset = 2 - ipv4ChecksumOffset = 10 - ttl = 65 - headerSize = ipv4Size + icmpv4Size - ) - - pkt := make([]byte, headerSize+len(payload)) - - ip := pkt[0:ipv4Size] - icmpv4 := pkt[ipv4Size : ipv4Size+icmpv4Size] - - // https://tools.ietf.org/html/rfc792 - icmpv4[0] = icmpv4Echo // type - icmpv4[1] = 0 // code - chksum := ^checksum(icmpv4, checksum(payload, 0)) - binary.BigEndian.PutUint16(icmpv4[icmpv4ChecksumOffset:], chksum) - - // https://tools.ietf.org/html/rfc760 section 3.1 - length := uint16(len(pkt)) - ip[0] = (4 << 4) | (ipv4Size / 4) - binary.BigEndian.PutUint16(ip[ipv4TotalLenOffset:], length) - ip[8] = ttl - ip[9] = icmpv4ProtocolNumber - copy(ip[12:], src.AsSlice()) - copy(ip[16:], dst.AsSlice()) - chksum = ^checksum(ip[:], 0) - binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], chksum) - - copy(pkt[headerSize:], payload) - return pkt -} - -type ChannelTUN struct { - Inbound chan []byte // incoming packets, closed on TUN close - Outbound chan []byte // outbound packets, blocks forever on TUN close - - closed chan struct{} - events chan tun.Event - tun chTun -} - -func NewChannelTUN() *ChannelTUN { - c := &ChannelTUN{ - Inbound: make(chan []byte), - Outbound: make(chan []byte), - closed: make(chan struct{}), - events: make(chan tun.Event, 1), - } - c.tun.c = c - c.events <- tun.EventUp - return c -} - -func (c *ChannelTUN) TUN() tun.Device { - return &c.tun -} - -type chTun struct { - c *ChannelTUN -} - -func (t *chTun) File() *os.File { return nil } - -func (t *chTun) Read(packets [][]byte, sizes []int, offset int) (int, error) { - select { - case <-t.c.closed: - return 0, os.ErrClosed - case msg := <-t.c.Outbound: - n := copy(packets[0][offset:], msg) - sizes[0] = n - return 1, nil - } -} - -// Write is called by the wireguard device to deliver a packet for routing. -func (t *chTun) Write(packets [][]byte, offset int) (int, error) { - if offset == -1 { - close(t.c.closed) - close(t.c.events) - return 0, io.EOF - } - for i, data := range packets { - msg := make([]byte, len(data)-offset) - copy(msg, data[offset:]) - select { - case <-t.c.closed: - return i, os.ErrClosed - case t.c.Inbound <- msg: - } - } - return len(packets), nil -} - -func (t *chTun) BatchSize() int { - return 1 -} - -const DefaultMTU = 1420 - -func (t *chTun) MTU() (int, error) { return DefaultMTU, nil } -func (t *chTun) Name() (string, error) { return "loopbackTun1", nil } -func (t *chTun) Events() <-chan tun.Event { return t.c.events } -func (t *chTun) Close() error { - t.Write(nil, -1) - return nil -} From dfa4a8968f3d670a7d87bd5c58cacac90a38d82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 9 Dec 2023 17:27:04 +0800 Subject: [PATCH 110/173] Add module_rename.py --- module_rename.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 module_rename.py diff --git a/module_rename.py b/module_rename.py new file mode 100644 index 0000000..bf03152 --- /dev/null +++ b/module_rename.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import fileinput + + +PKG_ORIGINAL = "github.com/tailscale/wireguard-go" +PKG_NEW = "github.com/sagernet/wireguard-go" + +EXTENSIONS = [".go", ".md", ".mod", ".sh"] + +parser = argparse.ArgumentParser() +parser.add_argument("-r", "--reverse", action="store_true") +args = parser.parse_args() + + +def replace_line(line): + if args.reverse: + return line.replace(PKG_NEW, PKG_ORIGINAL) + return line.replace(PKG_ORIGINAL, PKG_NEW) + + +for dirpath, dirnames, filenames in os.walk("."): + # Skip hidden directories like .git + dirnames[:] = [d for d in dirnames if not d[0] == "."] + filenames = [f for f in filenames if os.path.splitext(f)[1] in EXTENSIONS] + for filename in filenames: + file_path = os.path.join(dirpath, filename) + with fileinput.FileInput(file_path, inplace=True) as file: + for line in file: + print(replace_line(line), end="") From 558e1e0493c5ce35f33f23527e4dd466d9a6d312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 15 Sep 2025 18:12:36 +0800 Subject: [PATCH 111/173] Rename module --- conn/bind_windows.go | 2 +- device/device.go | 8 ++++---- device/keypair.go | 2 +- device/noise-protocol.go | 4 ++-- device/peer.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/receive.go | 2 +- device/send.go | 4 ++-- device/sticky_default.go | 4 ++-- device/sticky_linux.go | 4 ++-- device/tun.go | 2 +- device/uapi.go | 2 +- go.mod | 2 +- ipc/uapi_linux.go | 2 +- ipc/uapi_windows.go | 2 +- tun/offload_linux.go | 2 +- tun/tun_linux.go | 4 ++-- 18 files changed, 26 insertions(+), 26 deletions(-) diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 737b475..d32ecd6 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -17,7 +17,7 @@ import ( "golang.org/x/sys/windows" - "github.com/tailscale/wireguard-go/conn/winrio" + "github.com/sagernet/wireguard-go/conn/winrio" ) const ( diff --git a/device/device.go b/device/device.go index 5b23485..7c70554 100644 --- a/device/device.go +++ b/device/device.go @@ -11,10 +11,10 @@ import ( "sync/atomic" "time" - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/ratelimiter" - "github.com/tailscale/wireguard-go/rwcancel" - "github.com/tailscale/wireguard-go/tun" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/ratelimiter" + "github.com/sagernet/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/tun" ) type Device struct { diff --git a/device/keypair.go b/device/keypair.go index 2689ee2..fb3d392 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -11,7 +11,7 @@ import ( "sync/atomic" "time" - "github.com/tailscale/wireguard-go/replay" + "github.com/sagernet/wireguard-go/replay" ) /* Due to limitations in Go and /x/crypto there is currently diff --git a/device/noise-protocol.go b/device/noise-protocol.go index ad5838e..50dcb60 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -16,8 +16,8 @@ import ( "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/tai64n" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/tai64n" ) type handshakeState int diff --git a/device/peer.go b/device/peer.go index 064feb2..1307300 100644 --- a/device/peer.go +++ b/device/peer.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - "github.com/tailscale/wireguard-go/conn" + "github.com/sagernet/wireguard-go/conn" ) type Peer struct { diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index bab9625..d0718d2 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -5,7 +5,7 @@ package device -import "github.com/tailscale/wireguard-go/conn" +import "github.com/sagernet/wireguard-go/conn" /* Reduce memory consumption for Android */ diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index 9749cb7..79b74b0 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -7,7 +7,7 @@ package device -import "github.com/tailscale/wireguard-go/conn" +import "github.com/sagernet/wireguard-go/conn" const ( QueueStagedSize = conn.IdealBatchSize diff --git a/device/receive.go b/device/receive.go index 02c8f21..01baad8 100644 --- a/device/receive.go +++ b/device/receive.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/tailscale/wireguard-go/conn" + "github.com/sagernet/wireguard-go/conn" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" diff --git a/device/send.go b/device/send.go index c8bb079..ff082a8 100644 --- a/device/send.go +++ b/device/send.go @@ -13,8 +13,8 @@ import ( "sync" "time" - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/tun" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" diff --git a/device/sticky_default.go b/device/sticky_default.go index 732f84c..0d02174 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -3,8 +3,8 @@ package device import ( - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 6eeced2..79ece63 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -20,8 +20,8 @@ import ( "golang.org/x/sys/unix" - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/tun.go b/device/tun.go index 960ecca..891575d 100644 --- a/device/tun.go +++ b/device/tun.go @@ -8,7 +8,7 @@ package device import ( "fmt" - "github.com/tailscale/wireguard-go/tun" + "github.com/sagernet/wireguard-go/tun" ) const DefaultMTU = 1420 diff --git a/device/uapi.go b/device/uapi.go index 4987cda..6bbccbd 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "github.com/tailscale/wireguard-go/ipc" + "github.com/sagernet/wireguard-go/ipc" ) type IPCError struct { diff --git a/go.mod b/go.mod index 7476734..2dfe133 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tailscale/wireguard-go +module github.com/sagernet/wireguard-go go 1.20 diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index bfdf1bf..6a662d3 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -9,7 +9,7 @@ import ( "net" "os" - "github.com/tailscale/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/rwcancel" "golang.org/x/sys/unix" ) diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index bc30ae0..5d236b3 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -8,7 +8,7 @@ package ipc import ( "net" - "github.com/tailscale/wireguard-go/ipc/namedpipe" + "github.com/sagernet/wireguard-go/ipc/namedpipe" "golang.org/x/sys/windows" ) diff --git a/tun/offload_linux.go b/tun/offload_linux.go index fb6ac5b..6ba8547 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -13,7 +13,7 @@ import ( "io" "unsafe" - "github.com/tailscale/wireguard-go/conn" + "github.com/sagernet/wireguard-go/conn" "golang.org/x/sys/unix" ) diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 7cdbf88..4e27c1b 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -17,8 +17,8 @@ import ( "time" "unsafe" - "github.com/tailscale/wireguard-go/conn" - "github.com/tailscale/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" "golang.org/x/sys/unix" ) From d79d3c88fd3f45d0a4d7b8a895eca6c8b76cdb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 15 Sep 2025 18:12:47 +0800 Subject: [PATCH 112/173] Reformat code --- conn/bind_std.go | 6 ++---- conn/bind_windows.go | 3 +-- device/noise-protocol.go | 5 ++--- device/receive.go | 1 - device/sticky_linux.go | 3 +-- ipc/uapi_linux.go | 1 - tun/errors.go | 10 ++++------ tun/tun_linux.go | 4 +--- 8 files changed, 11 insertions(+), 22 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index fc05634..249f53d 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -213,10 +213,8 @@ func (s *StdNetBind) getMessages() *[]ipv6.Message { return s.msgsPool.Get().(*[]ipv6.Message) } -var ( - // If compilation fails here these are no longer the same underlying type. - _ ipv6.Message = ipv4.Message{} -) +// If compilation fails here these are no longer the same underlying type. +var _ ipv6.Message = ipv4.Message{} type batchReader interface { ReadBatch([]ipv6.Message, int) (int, error) diff --git a/conn/bind_windows.go b/conn/bind_windows.go index d32ecd6..81a820b 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -15,9 +15,8 @@ import ( "sync/atomic" "unsafe" - "golang.org/x/sys/windows" - "github.com/sagernet/wireguard-go/conn/winrio" + "golang.org/x/sys/windows" ) const ( diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 50dcb60..6e67d04 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -12,12 +12,11 @@ import ( "sync" "time" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/tai64n" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - - "github.com/sagernet/wireguard-go/conn" - "github.com/sagernet/wireguard-go/tai64n" ) type handshakeState int diff --git a/device/receive.go b/device/receive.go index 01baad8..6ab2680 100644 --- a/device/receive.go +++ b/device/receive.go @@ -411,7 +411,6 @@ func (device *Device) RoutineHandshake(id int) { // derive keypair err = peer.BeginSymmetricSession() - if err != nil { device.log.Errorf("%v - Failed to derive keypair: %v", peer, err) goto skip diff --git a/device/sticky_linux.go b/device/sticky_linux.go index 79ece63..b5b0e51 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -18,10 +18,9 @@ import ( "sync" "unsafe" - "golang.org/x/sys/unix" - "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/rwcancel" + "golang.org/x/sys/unix" ) func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index 6a662d3..c14d5d0 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -84,7 +84,6 @@ func UAPIListen(name string, file *os.File) (net.Listener, error) { unix.IN_DELETE| unix.IN_DELETE_SELF, ) - if err != nil { return nil, err } diff --git a/tun/errors.go b/tun/errors.go index 75ae3a4..2c49fc7 100644 --- a/tun/errors.go +++ b/tun/errors.go @@ -4,9 +4,7 @@ import ( "errors" ) -var ( - // ErrTooManySegments is returned by Device.Read() when segmentation - // overflows the length of supplied buffers. This error should not cause - // reads to cease. - ErrTooManySegments = errors.New("too many segments") -) +// ErrTooManySegments is returned by Device.Read() when segmentation +// overflows the length of supplied buffers. This error should not cause +// reads to cease. +var ErrTooManySegments = errors.New("too many segments") diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 4e27c1b..8f4aed1 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -506,9 +506,7 @@ func (tun *NativeTun) initFromFlags(name string) error { return err } if e := sc.Control(func(fd uintptr) { - var ( - ifr *unix.Ifreq - ) + var ifr *unix.Ifreq ifr, err = unix.NewIfreq(name) if err != nil { return From 2835be44111d5edbcac44401dc0c6c9cbd4eb48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Nov 2022 15:41:48 +0800 Subject: [PATCH 113/173] Add custom worker size params (cherry picked from commit 7c2acadba17cadf8a1df957c49e1333130d460ad) (cherry picked from commit a7bac1754e7717e1d4009d1ffd2d13330067d631) (cherry picked from commit 7a2f11c693b49e784318bbf987173095c67b563d) --- device/device.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/device/device.go b/device/device.go index 7c70554..17619bd 100644 --- a/device/device.go +++ b/device/device.go @@ -281,7 +281,7 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error { return nil } -func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { +func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { device := new(Device) device.state.state.Store(uint32(deviceStateDown)) device.closed = make(chan struct{}) @@ -308,10 +308,12 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { // start workers - cpus := runtime.NumCPU() + if workers == 0 { + workers = runtime.NumCPU() + } device.state.stopping.Wait() - device.queue.encryption.wg.Add(cpus) // One for each RoutineHandshake - for i := 0; i < cpus; i++ { + device.queue.encryption.wg.Add(workers) // One for each RoutineHandshake + for i := 0; i < workers; i++ { go device.RoutineEncryption(i + 1) go device.RoutineDecryption(i + 1) go device.RoutineHandshake(i + 1) From 606102e010019d1dda58d932329f096e55d2f360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Aug 2023 20:57:31 +0800 Subject: [PATCH 114/173] Add pause support --- device/device.go | 14 ++++++++++---- device/timers.go | 3 +++ go.mod | 1 + go.sum | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/device/device.go b/device/device.go index 17619bd..e491e53 100644 --- a/device/device.go +++ b/device/device.go @@ -6,11 +6,15 @@ package device import ( + "context" "runtime" "sync" "sync/atomic" "time" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/ratelimiter" "github.com/sagernet/wireguard-go/rwcancel" @@ -86,9 +90,10 @@ type Device struct { mtu atomic.Int32 } - ipcMutex sync.RWMutex - closed chan struct{} - log *Logger + ipcMutex sync.RWMutex + closed chan struct{} + log *Logger + pauseManager pause.Manager } // deviceState represents the state of a Device. @@ -281,8 +286,9 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error { return nil } -func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { +func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { device := new(Device) + device.pauseManager = service.FromContext[pause.Manager](ctx) device.state.state.Store(uint32(deviceStateDown)) device.closed = make(chan struct{}) device.log = logger diff --git a/device/timers.go b/device/timers.go index d4a4ed4..5839557 100644 --- a/device/timers.go +++ b/device/timers.go @@ -39,6 +39,9 @@ func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer { timer.isPending = false timer.modifyingLock.Unlock() + if pauseManager := peer.device.pauseManager; pauseManager != nil { + pauseManager.WaitActive() + } expirationFunction(peer) }) timer.Stop() diff --git a/go.mod b/go.mod index 2dfe133..37359e6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/sagernet/wireguard-go go 1.20 require ( + github.com/sagernet/sing v0.7.10 golang.org/x/crypto v0.13.0 golang.org/x/net v0.15.0 golang.org/x/sys v0.12.0 diff --git a/go.sum b/go.sum index ec4169f..c405e0a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw= +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/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= From b2a20cdd7732f28fd182ff17e1cb77f86098697c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 20 Nov 2024 18:53:51 +0800 Subject: [PATCH 115/173] Add device.InputPacket --- device/send.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/device/send.go b/device/send.go index ff082a8..2c690dc 100644 --- a/device/send.go +++ b/device/send.go @@ -322,6 +322,30 @@ func (device *Device) RoutineReadFromTUN() { } } +func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { + peer := device.allowedips.Lookup(destination) + if peer == nil { + return + } + elem := device.NewOutboundElement() + packet := elem.buffer[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.PutMessageBuffer(elem.buffer) + device.PutOutboundElement(elem) + device.PutOutboundElementsContainer(elemsForPeer) + } +} + func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { for { select { From c63bc19bc949c3de3beab1eedd55ecf634f19d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Dec 2023 18:44:05 +0800 Subject: [PATCH 116/173] Export std net bind --- conn/bind_std.go | 58 +++++++++++++++++++++++++++++---- conn/bind_windows.go | 76 ++++++++++++++++++++++++++++++++++++++++---- conn/conn.go | 2 ++ conn/controlfns.go | 26 ++------------- conn/default.go | 6 +++- device/device.go | 1 - 6 files changed, 129 insertions(+), 40 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 249f53d..a1d4d9e 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -16,6 +16,9 @@ import ( "sync" "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/ipv6" ) @@ -31,6 +34,9 @@ var ( // methods for sending and receiving multiple datagrams per-syscall. See the // proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564. type StdNetBind struct { + externalControl control.Func + reservedForEndpoint map[netip.AddrPort][3]uint8 + mu sync.Mutex // protects all fields except as specified ipv4 *net.UDPConn ipv6 *net.UDPConn @@ -49,8 +55,11 @@ type StdNetBind struct { blackhole6 bool } -func NewStdNetBind() Bind { +func NewStdNetBind(externalControl control.Func) Bind { return &StdNetBind{ + externalControl: externalControl, + reservedForEndpoint: make(map[netip.AddrPort][3]uint8), + udpAddrPool: sync.Pool{ New: func() any { return &net.UDPAddr{ @@ -118,8 +127,29 @@ func (e *StdNetEndpoint) DstToString() string { return e.AddrPort.String() } -func listenNet(network string, port int) (*net.UDPConn, int, error) { - conn, err := listenConfig().ListenPacket(context.Background(), network, ":"+strconv.Itoa(port)) +func listenNet(externalControl control.Func, network string, port int) (*net.UDPConn, int, error) { + 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 { return nil, 0, err } @@ -160,13 +190,13 @@ again: var v4pc *ipv4.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) { return nil, 0, err } // 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, errEADDRINUSE) && tries < 100 { v4conn.Close() tries++ @@ -270,8 +300,10 @@ func (s *StdNetBind) receiveIP( if sizes[i] == 0 { continue } - addrPort := msg.Addr.(*net.UDPAddr).AddrPort() - ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation + if msg.N > 3 { + common.ClearArray(bufs[i][1:4]) + } + ep := &StdNetEndpoint{AddrPort: M.AddrPortFromNet(msg.Addr)} // TODO: remove allocation getSrcFromControl(msg.OOB[:msg.NN], ep) eps[i] = ep } @@ -380,6 +412,14 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { retried bool err error ) + for _, buf := range bufs { + if len(buf) > 3 { + reserved, loaded := s.reservedForEndpoint[endpoint.(*StdNetEndpoint).AddrPort] + if loaded { + copy(buf[1:4], reserved[:]) + } + } + } retry: if offload { n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, offset, *msgs, setGSOSize) @@ -410,6 +450,10 @@ retry: return err } +func (s *StdNetBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) { + s.reservedForEndpoint[destination] = reserved +} + func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error { var ( n int diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 81a820b..51c0974 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -15,6 +15,10 @@ import ( "sync/atomic" "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" ) @@ -71,18 +75,26 @@ type afWinRingBind struct { // WinRingBind uses Windows registered I/O for fast ring buffered networking. type WinRingBind struct { + externalControl control.Func + reservedForEndpoint map[WinRingEndpoint][3]uint8 + v4, v6 afWinRingBind mu sync.RWMutex 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() { - return NewStdNetBind() + return NewStdNetBind(externalControl) + } + return &WinRingBind{ + externalControl: externalControl, + reservedForEndpoint: make(map[WinRingEndpoint][3]uint8), } - return new(WinRingBind) } type WinRingEndpoint struct { @@ -238,7 +250,7 @@ func (ring *ringBuffer) Open() error { 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 bind.sock, err = winrio.Socket(family, windows.SOCK_DGRAM, windows.IPPROTO_UDP) if err != nil { @@ -256,6 +268,19 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock if err != nil { 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) if err != nil { return nil, err @@ -267,6 +292,23 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock 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) { bind.mu.Lock() defer bind.mu.Unlock() @@ -279,11 +321,11 @@ func (bind *WinRingBind) Open(port uint16) (recvFns []ReceiveFunc, selectedPort return nil, 0, ErrBindAlreadyOpen } 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 { 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 { return nil, 0, err } @@ -419,6 +461,9 @@ func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen) + if n > 3 { + common.ClearArray(bufs[0][1:4]) + } sizes[0] = n eps[0] = ep return 1, err @@ -428,6 +473,9 @@ func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen) + if n > 3 { + common.ClearArray(bufs[0][1:4]) + } sizes[0] = n eps[0] = ep return 1, err @@ -494,6 +542,12 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint, offset int) erro defer bind.mu.RUnlock() 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 { case windows.AF_INET: if bind.v4.blackhole { @@ -514,6 +568,14 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint, offset int) erro 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 +} + func (s *StdNetBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/conn/conn.go b/conn/conn.go index f178161..4c5c194 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -57,6 +57,8 @@ type Bind interface { // BatchSize is the number of buffers expected to be passed to // the ReceiveFuncs, and the maximum expected to be passed to SendBatch. BatchSize() int + + SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) } // BindSocketToInterface is implemented by Bind objects that support being diff --git a/conn/controlfns.go b/conn/controlfns.go index 4f7d90f..44b52fc 100644 --- a/conn/controlfns.go +++ b/conn/controlfns.go @@ -6,8 +6,7 @@ package conn import ( - "net" - "syscall" + "github.com/sagernet/sing/common/control" ) // UDP socket read/write buffer size (7MB). The value of 7MB is chosen as it is @@ -17,27 +16,6 @@ import ( // around this limitation) 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 // that can apply socket options. -var controlFns = []controlFn{} - -// 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 - }, - } -} +var controlFns []control.Func diff --git a/conn/default.go b/conn/default.go index b6f761b..1892c3f 100644 --- a/conn/default.go +++ b/conn/default.go @@ -7,4 +7,8 @@ package conn -func NewDefaultBind() Bind { return NewStdNetBind() } +import "github.com/sagernet/sing/common/control" + +func NewDefaultBind(externalControl control.Func) Bind { + return NewStdNetBind(externalControl) +} diff --git a/device/device.go b/device/device.go index e491e53..14cd080 100644 --- a/device/device.go +++ b/device/device.go @@ -14,7 +14,6 @@ import ( "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" - "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/ratelimiter" "github.com/sagernet/wireguard-go/rwcancel" From 16510ac47288a3f634f1b2807dab47a47de569b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Sep 2025 19:03:11 +0800 Subject: [PATCH 117/173] Fix input packet --- device/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device/send.go b/device/send.go index 2c690dc..cf0f693 100644 --- a/device/send.go +++ b/device/send.go @@ -328,7 +328,7 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { return } elem := device.NewOutboundElement() - packet := elem.buffer[MessageTransportHeaderSize:] + packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] var n int for _, packetSlice := range packetSlices { n += copy(packet[n:], packetSlice) From 0361c54dca92071463e9611796c04464386457ac Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov <31506978+ygurov@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:07:48 +0100 Subject: [PATCH 118/173] fix: refactor processing of junk packets (#103) - fix the bug that transport packet interprets as init/resp/cookie with the same size - cleanup error responses - reduce buffer allocations --- device/awg/awg.go | 90 ---- device/awg/internal/mock.go | 37 -- device/awg/junk_creator.go | 50 -- device/awg/junk_creator_test.go | 97 ---- device/awg/magic_header.go | 97 ---- device/awg/magic_header_test.go | 488 ------------------ device/awg/prng.go | 50 -- device/awg/special_handshake_handler.go | 36 -- device/awg/tag_generator.go | 229 -------- device/awg/tag_generator_test.go | 321 ------------ device/awg/tag_junk_packet_generator.go | 59 --- device/awg/tag_junk_packet_generator_test.go | 210 -------- device/awg/tag_junk_packet_generators.go | 66 --- device/awg/tag_junk_packet_generators_test.go | 149 ------ device/awg/tag_parser.go | 112 ---- device/awg/tag_parser_test.go | 77 --- device/cookie_test.go | 2 +- device/device.go | 425 +-------------- device/magic-header.go | 63 +++ device/noise-protocol.go | 55 +- device/obf.go | 140 +++++ device/obf_bytes.go | 47 ++ device/obf_data.go | 25 + device/obf_datasize.go | 38 ++ device/obf_datastring.go | 29 ++ device/obf_rand.go | 39 ++ device/obf_randchars.go | 48 ++ device/obf_randdigits.go | 48 ++ device/obf_timestamp.go | 31 ++ device/peer.go | 11 - device/receive.go | 78 ++- device/send.go | 138 ++--- device/uapi.go | 299 +++++++---- 33 files changed, 852 insertions(+), 2832 deletions(-) delete mode 100644 device/awg/awg.go delete mode 100644 device/awg/internal/mock.go delete mode 100644 device/awg/junk_creator.go delete mode 100644 device/awg/junk_creator_test.go delete mode 100644 device/awg/magic_header.go delete mode 100644 device/awg/magic_header_test.go delete mode 100644 device/awg/prng.go delete mode 100644 device/awg/special_handshake_handler.go delete mode 100644 device/awg/tag_generator.go delete mode 100644 device/awg/tag_generator_test.go delete mode 100644 device/awg/tag_junk_packet_generator.go delete mode 100644 device/awg/tag_junk_packet_generator_test.go delete mode 100644 device/awg/tag_junk_packet_generators.go delete mode 100644 device/awg/tag_junk_packet_generators_test.go delete mode 100644 device/awg/tag_parser.go delete mode 100644 device/awg/tag_parser_test.go create mode 100644 device/magic-header.go create mode 100644 device/obf.go create mode 100644 device/obf_bytes.go create mode 100644 device/obf_data.go create mode 100644 device/obf_datasize.go create mode 100644 device/obf_datastring.go create mode 100644 device/obf_rand.go create mode 100644 device/obf_randchars.go create mode 100644 device/obf_randdigits.go create mode 100644 device/obf_timestamp.go diff --git a/device/awg/awg.go b/device/awg/awg.go deleted file mode 100644 index 888a42e..0000000 --- a/device/awg/awg.go +++ /dev/null @@ -1,90 +0,0 @@ -package awg - -import ( - "bytes" - "fmt" - "sync" - - "github.com/tevino/abool" -) - -type Cfg struct { - IsSet bool - JunkPacketCount int - JunkPacketMinSize int - JunkPacketMaxSize int - InitHeaderJunkSize int - ResponseHeaderJunkSize int - CookieReplyHeaderJunkSize int - TransportHeaderJunkSize int - - MagicHeaders MagicHeaders -} - -type Protocol struct { - IsOn abool.AtomicBool - // TODO: revision the need of the mutex - Mux sync.RWMutex - Cfg Cfg - JunkCreator JunkCreator - - HandshakeHandler SpecialHandshakeHandler -} - -func (protocol *Protocol) CreateInitHeaderJunk() ([]byte, error) { - protocol.Mux.RLock() - defer protocol.Mux.RUnlock() - - return protocol.createHeaderJunk(protocol.Cfg.InitHeaderJunkSize, 0) -} - -func (protocol *Protocol) CreateResponseHeaderJunk() ([]byte, error) { - protocol.Mux.RLock() - defer protocol.Mux.RUnlock() - - return protocol.createHeaderJunk(protocol.Cfg.ResponseHeaderJunkSize, 0) -} - -func (protocol *Protocol) CreateCookieReplyHeaderJunk() ([]byte, error) { - protocol.Mux.RLock() - defer protocol.Mux.RUnlock() - - return protocol.createHeaderJunk(protocol.Cfg.CookieReplyHeaderJunkSize, 0) -} - -func (protocol *Protocol) CreateTransportHeaderJunk(packetSize int) ([]byte, error) { - protocol.Mux.RLock() - defer protocol.Mux.RUnlock() - - return protocol.createHeaderJunk(protocol.Cfg.TransportHeaderJunkSize, packetSize) -} - -func (protocol *Protocol) createHeaderJunk(junkSize int, extraSize int) ([]byte, error) { - if junkSize == 0 { - return nil, nil - } - - buf := make([]byte, 0, junkSize+extraSize) - writer := bytes.NewBuffer(buf[:0]) - - err := protocol.JunkCreator.AppendJunk(writer, junkSize) - if err != nil { - return nil, fmt.Errorf("append junk: %w", err) - } - - return writer.Bytes(), nil -} - -func (protocol *Protocol) GetMagicHeaderMinFor(msgType uint32) (uint32, error) { - for _, magicHeader := range protocol.Cfg.MagicHeaders.Values { - if magicHeader.Min <= msgType && msgType <= magicHeader.Max { - return magicHeader.Min, nil - } - } - - return 0, fmt.Errorf("no header for value: %d", msgType) -} - -func (protocol *Protocol) GetMsgType(defaultMsgType uint32) (uint32, error) { - return protocol.Cfg.MagicHeaders.Get(defaultMsgType) -} diff --git a/device/awg/internal/mock.go b/device/awg/internal/mock.go deleted file mode 100644 index a2e1c95..0000000 --- a/device/awg/internal/mock.go +++ /dev/null @@ -1,37 +0,0 @@ -package internal - -type mockGenerator struct { - size int -} - -func NewMockGenerator(size int) mockGenerator { - return mockGenerator{size: size} -} - -func (m mockGenerator) Generate() []byte { - return make([]byte, m.size) -} - -func (m mockGenerator) Size() int { - return m.size -} - -func (m mockGenerator) Name() string { - return "mock" -} - -type mockByteGenerator struct { - data []byte -} - -func NewMockByteGenerator(data []byte) mockByteGenerator { - return mockByteGenerator{data: data} -} - -func (bg mockByteGenerator) Generate() []byte { - return bg.data -} - -func (bg mockByteGenerator) Size() int { - return len(bg.data) -} diff --git a/device/awg/junk_creator.go b/device/awg/junk_creator.go deleted file mode 100644 index 8ba2918..0000000 --- a/device/awg/junk_creator.go +++ /dev/null @@ -1,50 +0,0 @@ -package awg - -import ( - "bytes" - "fmt" -) - -type JunkCreator struct { - cfg Cfg - randomGenerator PRNG[int] -} - -// TODO: refactor param to only pass the junk related params -func NewJunkCreator(cfg Cfg) JunkCreator { - return JunkCreator{cfg: cfg, randomGenerator: NewPRNG[int]()} -} - -// Should be called with awg mux RLocked -func (jc *JunkCreator) CreateJunkPackets(junks *[][]byte) { - if jc.cfg.JunkPacketCount == 0 { - return - } - - for range jc.cfg.JunkPacketCount { - packetSize := jc.randomPacketSize() - junk := jc.randomJunkWithSize(packetSize) - *junks = append(*junks, junk) - } - return -} - -// Should be called with awg mux RLocked -func (jc *JunkCreator) randomPacketSize() int { - return jc.randomGenerator.RandomSizeInRange(jc.cfg.JunkPacketMinSize, jc.cfg.JunkPacketMaxSize) -} - -// Should be called with awg mux RLocked -func (jc *JunkCreator) AppendJunk(writer *bytes.Buffer, size int) error { - headerJunk := jc.randomJunkWithSize(size) - _, err := writer.Write(headerJunk) - if err != nil { - return fmt.Errorf("write header junk: %v", err) - } - return nil -} - -// Should be called with awg mux RLocked -func (jc *JunkCreator) randomJunkWithSize(size int) []byte { - return jc.randomGenerator.ReadSize(size) -} diff --git a/device/awg/junk_creator_test.go b/device/awg/junk_creator_test.go deleted file mode 100644 index cdf752b..0000000 --- a/device/awg/junk_creator_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package awg - -import ( - "bytes" - "fmt" - "testing" -) - -func setUpJunkCreator() JunkCreator { - mh, _ := NewMagicHeaders( - []MagicHeader{ - NewMagicHeaderSameValue(123456), - NewMagicHeaderSameValue(67543), - NewMagicHeaderSameValue(32345), - NewMagicHeaderSameValue(123123), - }, - ) - - jc := NewJunkCreator(Cfg{ - IsSet: true, - JunkPacketCount: 5, - JunkPacketMinSize: 500, - JunkPacketMaxSize: 1000, - InitHeaderJunkSize: 30, - ResponseHeaderJunkSize: 40, - MagicHeaders: mh, - }) - - return jc -} - -func Test_junkCreator_createJunkPackets(t *testing.T) { - jc := setUpJunkCreator() - t.Run("valid", func(t *testing.T) { - got := make([][]byte, 0, jc.cfg.JunkPacketCount) - jc.CreateJunkPackets(&got) - seen := make(map[string]bool) - for _, junk := range got { - key := string(junk) - if seen[key] { - t.Errorf( - "junkCreator.createJunkPackets() = %v, duplicate key: %v", - got, - junk, - ) - return - } - seen[key] = true - } - }) -} - -func Test_junkCreator_randomJunkWithSize(t *testing.T) { - t.Run("valid", func(t *testing.T) { - jc := setUpJunkCreator() - r1 := jc.randomJunkWithSize(10) - r2 := jc.randomJunkWithSize(10) - fmt.Printf("%v\n%v\n", r1, r2) - if bytes.Equal(r1, r2) { - t.Errorf("same junks") - return - } - }) -} - -func Test_junkCreator_randomPacketSize(t *testing.T) { - jc := setUpJunkCreator() - for range [30]struct{}{} { - t.Run("valid", func(t *testing.T) { - if got := jc.randomPacketSize(); jc.cfg.JunkPacketMinSize > got || - got > jc.cfg.JunkPacketMaxSize { - t.Errorf( - "junkCreator.randomPacketSize() = %v, not between range [%v,%v]", - got, - jc.cfg.JunkPacketMinSize, - jc.cfg.JunkPacketMaxSize, - ) - } - }) - } -} - -func Test_junkCreator_appendJunk(t *testing.T) { - jc := setUpJunkCreator() - t.Run("valid", func(t *testing.T) { - s := "apple" - buffer := bytes.NewBuffer([]byte(s)) - err := jc.AppendJunk(buffer, 30) - if err != nil && - buffer.Len() != len(s)+30 { - t.Error("appendWithJunk() size don't match") - } - read := make([]byte, 50) - buffer.Read(read) - fmt.Println(string(read)) - }) -} diff --git a/device/awg/magic_header.go b/device/awg/magic_header.go deleted file mode 100644 index aaf4e97..0000000 --- a/device/awg/magic_header.go +++ /dev/null @@ -1,97 +0,0 @@ -package awg - -import ( - "cmp" - "fmt" - "slices" - "strconv" - "strings" -) - -type MagicHeader struct { - Min uint32 - Max uint32 -} - -func NewMagicHeaderSameValue(value uint32) MagicHeader { - return MagicHeader{Min: value, Max: value} -} - -func NewMagicHeader(min, max uint32) (MagicHeader, error) { - if min > max { - return MagicHeader{}, fmt.Errorf("min (%d) cannot be greater than max (%d)", min, max) - } - - return MagicHeader{Min: min, Max: max}, nil -} - -func ParseMagicHeader(key, value string) (MagicHeader, error) { - hyphenIdx := strings.Index(value, "-") - if hyphenIdx == -1 { - // if there is no hyphen, we treat it as single magic header value - magicHeader, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return MagicHeader{}, fmt.Errorf("parse key: %s; value: %s; %w", key, value, err) - } - - return NewMagicHeader(uint32(magicHeader), uint32(magicHeader)) - } - - minStr := value[:hyphenIdx] - maxStr := value[hyphenIdx+1:] - if len(minStr) == 0 || len(maxStr) == 0 { - return MagicHeader{}, fmt.Errorf("invalid value for key: %s; value: %s; expected format: min-max", key, value) - } - - min, err := strconv.ParseUint(minStr, 10, 32) - if err != nil { - return MagicHeader{}, fmt.Errorf("parse min key: %s; value: %s; %w", key, minStr, err) - } - - max, err := strconv.ParseUint(maxStr, 10, 32) - if err != nil { - return MagicHeader{}, fmt.Errorf("parse max key: %s; value: %s; %w", key, maxStr, err) - } - - magicHeader, err := NewMagicHeader(uint32(min), uint32(max)) - if err != nil { - return MagicHeader{}, fmt.Errorf("new magicHeader key: %s; value: %s-%s; %w", key, minStr, maxStr, err) - } - - return magicHeader, nil -} - -type MagicHeaders struct { - Values []MagicHeader - randomGenerator RandomNumberGenerator[uint32] -} - -func NewMagicHeaders(headerValues []MagicHeader) (MagicHeaders, error) { - if len(headerValues) != 4 { - return MagicHeaders{}, fmt.Errorf("all header types should be included: %v", headerValues) - } - - sortedMagicHeaders := slices.SortedFunc(slices.Values(headerValues), func(lhs MagicHeader, rhs MagicHeader) int { - return cmp.Compare(lhs.Min, rhs.Min) - }) - - for i := range 3 { - if sortedMagicHeaders[i].Max >= sortedMagicHeaders[i+1].Min { - return MagicHeaders{}, fmt.Errorf( - "magic headers shouldn't overlap; %v > %v", - sortedMagicHeaders[i].Max, - sortedMagicHeaders[i+1].Min, - ) - } - } - - return MagicHeaders{Values: headerValues, randomGenerator: NewPRNG[uint32]()}, nil -} - -func (mh *MagicHeaders) Get(defaultMsgType uint32) (uint32, error) { - if defaultMsgType == 0 || defaultMsgType > 4 { - return 0, fmt.Errorf("invalid msg type: %d", defaultMsgType) - } - - return mh.randomGenerator.RandomSizeInRange(mh.Values[defaultMsgType-1].Min, mh.Values[defaultMsgType-1].Max), nil -} diff --git a/device/awg/magic_header_test.go b/device/awg/magic_header_test.go deleted file mode 100644 index 72a823e..0000000 --- a/device/awg/magic_header_test.go +++ /dev/null @@ -1,488 +0,0 @@ -package awg - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewMagicHeaderSameValue(t *testing.T) { - tests := []struct { - name string - value uint32 - expected MagicHeader - }{ - { - name: "zero value", - value: 0, - expected: MagicHeader{Min: 0, Max: 0}, - }, - { - name: "small value", - value: 1, - expected: MagicHeader{Min: 1, Max: 1}, - }, - { - name: "large value", - value: 4294967295, // max uint32 - expected: MagicHeader{Min: 4294967295, Max: 4294967295}, - }, - { - name: "medium value", - value: 1000, - expected: MagicHeader{Min: 1000, Max: 1000}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - result := NewMagicHeaderSameValue(tt.value) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestNewMagicHeader(t *testing.T) { - tests := []struct { - name string - min uint32 - max uint32 - expected MagicHeader - errorMsg string - }{ - { - name: "valid range", - min: 1, - max: 10, - expected: MagicHeader{Min: 1, Max: 10}, - }, - { - name: "equal values", - min: 5, - max: 5, - expected: MagicHeader{Min: 5, Max: 5}, - }, - { - name: "zero range", - min: 0, - max: 0, - expected: MagicHeader{Min: 0, Max: 0}, - }, - { - name: "max uint32 range", - min: 4294967294, - max: 4294967295, - expected: MagicHeader{Min: 4294967294, Max: 4294967295}, - }, - { - name: "min greater than max", - min: 10, - max: 5, - expected: MagicHeader{}, - errorMsg: "min (10) cannot be greater than max (5)", - }, - { - name: "large min greater than max", - min: 4294967295, - max: 1, - expected: MagicHeader{}, - errorMsg: "min (4294967295) cannot be greater than max (1)", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - result, err := NewMagicHeader(tt.min, tt.max) - - if tt.errorMsg != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Equal(t, MagicHeader{}, result) - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, result) - } - }) - } -} - -func TestParseMagicHeader(t *testing.T) { - tests := []struct { - name string - key string - value string - expected MagicHeader - errorMsg string - }{ - { - name: "single value", - key: "header1", - value: "100", - expected: MagicHeader{Min: 100, Max: 100}, - }, - { - name: "valid range", - key: "header2", - value: "10-20", - expected: MagicHeader{Min: 10, Max: 20}, - }, - { - name: "zero single value", - key: "header3", - value: "0", - expected: MagicHeader{Min: 0, Max: 0}, - }, - { - name: "zero range", - key: "header4", - value: "0-0", - expected: MagicHeader{Min: 0, Max: 0}, - }, - { - name: "max uint32 single", - key: "header5", - value: "4294967295", - expected: MagicHeader{Min: 4294967295, Max: 4294967295}, - }, - { - name: "max uint32 range", - key: "header6", - value: "4294967294-4294967295", - expected: MagicHeader{Min: 4294967294, Max: 4294967295}, - }, - { - name: "invalid single value - not number", - key: "header7", - value: "abc", - expected: MagicHeader{}, - errorMsg: "parse key: header7; value: abc;", - }, - { - name: "invalid single value - negative", - key: "header8", - value: "-5", - expected: MagicHeader{}, - errorMsg: "invalid value for key: header8; value: -5;", - }, - { - name: "invalid single value - too large", - key: "header9", - value: "4294967296", - expected: MagicHeader{}, - errorMsg: "parse key: header9; value: 4294967296;", - }, - { - name: "invalid range - min not number", - key: "header10", - value: "abc-10", - expected: MagicHeader{}, - errorMsg: "parse min key: header10; value: abc;", - }, - { - name: "invalid range - max not number", - key: "header11", - value: "10-abc", - expected: MagicHeader{}, - errorMsg: "parse max key: header11; value: abc;", - }, - { - name: "invalid range - min greater than max", - key: "header12", - value: "20-10", - expected: MagicHeader{}, - errorMsg: "new magicHeader key: header12; value: 20-10;", - }, - { - name: "invalid range - too many parts", - key: "header13", - value: "10-20-30", - expected: MagicHeader{}, - errorMsg: "parse key: header13; value: 10-20-30;", - }, - { - name: "empty value", - key: "header14", - value: "", - expected: MagicHeader{}, - errorMsg: "parse key: header14; value: ;", - }, - { - name: "hyphen only", - key: "header15", - value: "-", - expected: MagicHeader{}, - errorMsg: "invalid value for key: header15; value: -;", - }, - { - name: "empty min", - key: "header16", - value: "-10", - expected: MagicHeader{}, - errorMsg: "invalid value for key: header16; value: -10;", - }, - { - name: "empty max", - key: "header17", - value: "10-", - expected: MagicHeader{}, - errorMsg: "invalid value for key: header17; value: 10-;", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - result, err := ParseMagicHeader(tt.key, tt.value) - - if tt.errorMsg != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Equal(t, MagicHeader{}, result) - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, result) - } - }) - } -} - -func TestNewMagicHeaders(t *testing.T) { - tests := []struct { - name string - magicHeaders []MagicHeader - errorMsg string - }{ - { - name: "valid non-overlapping headers", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 11, Max: 20}, - {Min: 21, Max: 30}, - {Min: 31, Max: 40}, - }, - }, - { - name: "valid adjacent headers", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 1}, - {Min: 2, Max: 2}, - {Min: 3, Max: 3}, - {Min: 4, Max: 4}, - }, - }, - { - name: "valid zero-based headers", - magicHeaders: []MagicHeader{ - {Min: 0, Max: 0}, - {Min: 1, Max: 1}, - {Min: 2, Max: 2}, - {Min: 3, Max: 3}, - }, - }, - { - name: "valid large value headers", - magicHeaders: []MagicHeader{ - {Min: 4294967290, Max: 4294967291}, - {Min: 4294967292, Max: 4294967293}, - {Min: 4294967294, Max: 4294967294}, - {Min: 4294967295, Max: 4294967295}, - }, - }, - { - name: "too few headers", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 11, Max: 20}, - {Min: 21, Max: 30}, - }, - errorMsg: "all header types should be included:", - }, - { - name: "too many headers", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 11, Max: 20}, - {Min: 21, Max: 30}, - {Min: 31, Max: 40}, - {Min: 41, Max: 50}, - }, - errorMsg: "all header types should be included:", - }, - { - name: "empty headers", - magicHeaders: []MagicHeader{}, - errorMsg: "all header types should be included:", - }, - { - name: "overlapping headers", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 15}, - {Min: 10, Max: 20}, - {Min: 25, Max: 30}, - {Min: 35, Max: 40}, - }, - errorMsg: "magic headers shouldn't overlap;", - }, - { - name: "overlapping headers at limit-first", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 10, Max: 20}, - {Min: 25, Max: 30}, - {Min: 35, Max: 40}, - }, - errorMsg: "magic headers shouldn't overlap;", - }, - { - name: "overlapping headers at limit-second", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 15, Max: 25}, - {Min: 25, Max: 30}, - {Min: 35, Max: 40}, - }, - errorMsg: "magic headers shouldn't overlap;", - }, - { - name: "overlapping headers at limit-third", - magicHeaders: []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 15, Max: 25}, - {Min: 30, Max: 35}, - {Min: 35, Max: 40}, - }, - errorMsg: "magic headers shouldn't overlap;", - }, - { - name: "identical ranges", - magicHeaders: []MagicHeader{ - {Min: 10, Max: 20}, - {Min: 10, Max: 20}, - {Min: 25, Max: 30}, - {Min: 35, Max: 40}, - }, - errorMsg: "magic headers shouldn't overlap;", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - result, err := NewMagicHeaders(tt.magicHeaders) - - if tt.errorMsg != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Equal(t, MagicHeaders{}, result) - } else { - require.NoError(t, err) - require.Equal(t, tt.magicHeaders, result.Values) - require.NotNil(t, result.randomGenerator) - } - }) - } -} - -// Mock PRNG for testing -type mockPRNG struct { - returnValue uint32 -} - -func (m *mockPRNG) RandomSizeInRange(min, max uint32) uint32 { - return m.returnValue -} - -func (m *mockPRNG) Get() uint64 { - return 0 -} -func (m *mockPRNG) ReadSize(size int) []byte { - return make([]byte, size) -} - -func TestMagicHeaders_Get(t *testing.T) { - // Create test headers - headers := []MagicHeader{ - {Min: 1, Max: 10}, - {Min: 11, Max: 20}, - {Min: 21, Max: 30}, - {Min: 31, Max: 40}, - } - - tests := []struct { - name string - defaultMsgType uint32 - mockValue uint32 - expectedValue uint32 - errorMsg string - }{ - { - name: "valid type 1", - defaultMsgType: 1, - mockValue: 5, - expectedValue: 5, - }, - { - name: "valid type 2", - defaultMsgType: 2, - mockValue: 15, - expectedValue: 15, - }, - { - name: "valid type 3", - defaultMsgType: 3, - mockValue: 25, - expectedValue: 25, - }, - { - name: "valid type 4", - defaultMsgType: 4, - mockValue: 35, - expectedValue: 35, - }, - { - name: "invalid type 0", - defaultMsgType: 0, - mockValue: 0, - expectedValue: 0, - errorMsg: "invalid msg type: 0", - }, - { - name: "invalid type 5", - defaultMsgType: 5, - mockValue: 0, - expectedValue: 0, - errorMsg: "invalid msg type: 5", - }, - { - name: "invalid type max uint32", - defaultMsgType: 4294967295, - mockValue: 0, - expectedValue: 0, - errorMsg: "invalid msg type: 4294967295", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - // Create a new instance with mock PRNG for each test - testMagicHeaders := MagicHeaders{ - Values: headers, - randomGenerator: &mockPRNG{returnValue: tt.mockValue}, - } - - result, err := testMagicHeaders.Get(tt.defaultMsgType) - - if tt.errorMsg != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Equal(t, uint32(0), result) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedValue, result) - } - }) - } -} diff --git a/device/awg/prng.go b/device/awg/prng.go deleted file mode 100644 index e7661d7..0000000 --- a/device/awg/prng.go +++ /dev/null @@ -1,50 +0,0 @@ -package awg - -import ( - crand "crypto/rand" - v2 "math/rand/v2" - - "golang.org/x/exp/constraints" -) - -type RandomNumberGenerator[T constraints.Integer] interface { - RandomSizeInRange(min, max T) T - Get() uint64 - ReadSize(size int) []byte -} - -type PRNG[T constraints.Integer] struct { - cha8Rand *v2.ChaCha8 -} - -func NewPRNG[T constraints.Integer]() PRNG[T] { - buf := make([]byte, 32) - _, _ = crand.Read(buf) - - return PRNG[T]{ - cha8Rand: v2.NewChaCha8([32]byte(buf)), - } -} - -func (p PRNG[T]) RandomSizeInRange(min, max T) T { - if min > max { - panic("min must be less than max") - } - - if min == max { - return min - } - - return T(p.Get()%uint64(max-min)) + min -} - -func (p PRNG[T]) Get() uint64 { - return p.cha8Rand.Uint64() -} - -func (p PRNG[T]) ReadSize(size int) []byte { - // TODO: use a memory pool to allocate - buf := make([]byte, size) - _, _ = p.cha8Rand.Read(buf) - return buf -} diff --git a/device/awg/special_handshake_handler.go b/device/awg/special_handshake_handler.go deleted file mode 100644 index d740879..0000000 --- a/device/awg/special_handshake_handler.go +++ /dev/null @@ -1,36 +0,0 @@ -package awg - -import ( - "github.com/tevino/abool" - "go.uber.org/atomic" -) - -// TODO: atomic?/ and better way to use this -var PacketCounter *atomic.Uint64 = atomic.NewUint64(0) - -// TODO -var WaitResponse = struct { - Channel chan struct{} - ShouldWait *abool.AtomicBool -}{ - make(chan struct{}, 1), - abool.New(), -} - -type SpecialHandshakeHandler struct { - SpecialJunk TagJunkPacketGenerators - - IsSet bool -} - -func (handler *SpecialHandshakeHandler) Validate() error { - return handler.SpecialJunk.Validate() -} - -func (handler *SpecialHandshakeHandler) GenerateSpecialJunk() [][]byte { - if !handler.SpecialJunk.IsDefined() { - return nil - } - - return handler.SpecialJunk.GeneratePackets() -} diff --git a/device/awg/tag_generator.go b/device/awg/tag_generator.go deleted file mode 100644 index 3a1d497..0000000 --- a/device/awg/tag_generator.go +++ /dev/null @@ -1,229 +0,0 @@ -package awg - -import ( - crand "crypto/rand" - "encoding/binary" - "encoding/hex" - "fmt" - "strconv" - "strings" - "time" - - v2 "math/rand/v2" - // "go.uber.org/atomic" -) - -type Generator interface { - Generate() []byte - Size() int -} - -type newGenerator func(string) (Generator, error) - -type BytesGenerator struct { - value []byte - size int -} - -func (bg *BytesGenerator) Generate() []byte { - return bg.value -} - -func (bg *BytesGenerator) Size() int { - return bg.size -} - -func newBytesGenerator(param string) (Generator, error) { - hasPrefix := strings.HasPrefix(param, "0x") || strings.HasPrefix(param, "0X") - if !hasPrefix { - return nil, fmt.Errorf("not correct hex: %s", param) - } - - hex, err := hexToBytes(param) - if err != nil { - return nil, fmt.Errorf("hexToBytes: %w", err) - } - - return &BytesGenerator{value: hex, size: len(hex)}, nil -} - -func hexToBytes(hexStr string) ([]byte, error) { - hexStr = strings.TrimPrefix(hexStr, "0x") - hexStr = strings.TrimPrefix(hexStr, "0X") - - // Ensure even length (pad with leading zero if needed) - if len(hexStr)%2 != 0 { - hexStr = "0" + hexStr - } - - return hex.DecodeString(hexStr) -} - -type randomGeneratorBase struct { - cha8Rand *v2.ChaCha8 - size int -} - -func newRandomGeneratorBase(param string) (*randomGeneratorBase, error) { - size, err := strconv.Atoi(param) - if err != nil { - return nil, fmt.Errorf("parse int: %w", err) - } - - if size > 1000 { - return nil, fmt.Errorf("size must be less than 1000") - } - - buf := make([]byte, 32) - _, err = crand.Read(buf) - if err != nil { - return nil, fmt.Errorf("crand read: %w", err) - } - - return &randomGeneratorBase{ - cha8Rand: v2.NewChaCha8([32]byte(buf)), - size: size, - }, nil -} - -func (rpg *randomGeneratorBase) generate() []byte { - junk := make([]byte, rpg.size) - rpg.cha8Rand.Read(junk) - return junk -} - -func (rpg *randomGeneratorBase) Size() int { - return rpg.size -} - -type RandomBytesGenerator struct { - *randomGeneratorBase -} - -func newRandomBytesGenerator(param string) (Generator, error) { - rpgBase, err := newRandomGeneratorBase(param) - if err != nil { - return nil, fmt.Errorf("new random bytes generator: %w", err) - } - - return &RandomBytesGenerator{randomGeneratorBase: rpgBase}, nil -} - -func (rpg *RandomBytesGenerator) Generate() []byte { - return rpg.generate() -} - -const alphanumericChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -type RandomASCIIGenerator struct { - *randomGeneratorBase -} - -func newRandomASCIIGenerator(param string) (Generator, error) { - rpgBase, err := newRandomGeneratorBase(param) - if err != nil { - return nil, fmt.Errorf("new random ascii generator: %w", err) - } - - return &RandomASCIIGenerator{randomGeneratorBase: rpgBase}, nil -} - -func (rpg *RandomASCIIGenerator) Generate() []byte { - junk := rpg.generate() - - result := make([]byte, rpg.size) - for i, b := range junk { - result[i] = alphanumericChars[b%byte(len(alphanumericChars))] - } - - return result -} - -type RandomDigitGenerator struct { - *randomGeneratorBase -} - -func newRandomDigitGenerator(param string) (Generator, error) { - rpgBase, err := newRandomGeneratorBase(param) - if err != nil { - return nil, fmt.Errorf("new random digit generator: %w", err) - } - - return &RandomDigitGenerator{randomGeneratorBase: rpgBase}, nil -} - -func (rpg *RandomDigitGenerator) Generate() []byte { - junk := rpg.generate() - - result := make([]byte, rpg.size) - for i, b := range junk { - result[i] = '0' + (b % 10) // Convert to digit character - } - - return result -} - -type TimestampGenerator struct { -} - -func (tg *TimestampGenerator) Generate() []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, uint64(time.Now().Unix())) - return buf -} - -func (tg *TimestampGenerator) Size() int { - return 8 -} - -func newTimestampGenerator(param string) (Generator, error) { - if len(param) != 0 { - return nil, fmt.Errorf("timestamp param needs to be empty: %s", param) - } - - return &TimestampGenerator{}, nil -} - -type PacketCounterGenerator struct { -} - -func (c *PacketCounterGenerator) Generate() []byte { - buf := make([]byte, 8) - // TODO: better way to handle counter tag - binary.BigEndian.PutUint64(buf, PacketCounter.Load()) - return buf -} - -func (c *PacketCounterGenerator) Size() int { - return 8 -} - -func newPacketCounterGenerator(param string) (Generator, error) { - if len(param) != 0 { - return nil, fmt.Errorf("packet counter param needs to be empty: %s", param) - } - - return &PacketCounterGenerator{}, nil -} - -type WaitResponseGenerator struct { -} - -func (c *WaitResponseGenerator) Generate() []byte { - WaitResponse.ShouldWait.Set() - <-WaitResponse.Channel - WaitResponse.ShouldWait.UnSet() - return []byte{} -} - -func (c *WaitResponseGenerator) Size() int { - return 0 -} - -func newWaitResponseGenerator(param string) (Generator, error) { - if len(param) != 0 { - return nil, fmt.Errorf("wait response param needs to be empty: %s", param) - } - - return &WaitResponseGenerator{}, nil -} diff --git a/device/awg/tag_generator_test.go b/device/awg/tag_generator_test.go deleted file mode 100644 index 43efa67..0000000 --- a/device/awg/tag_generator_test.go +++ /dev/null @@ -1,321 +0,0 @@ -package awg - -import ( - "encoding/binary" - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewBytesGenerator(t *testing.T) { - t.Parallel() - - type args struct { - param string - } - tests := []struct { - name string - args args - want []byte - wantErr error - }{ - { - name: "empty", - args: args{ - param: "", - }, - wantErr: fmt.Errorf("not correct hex"), - }, - { - name: "wrong start", - args: args{ - param: "123456", - }, - wantErr: fmt.Errorf("not correct hex"), - }, - { - name: "not only hex value with X", - args: args{ - param: "0X12345q", - }, - wantErr: fmt.Errorf("not correct hex"), - }, - { - name: "not only hex value with x", - args: args{ - param: "0x12345q", - }, - wantErr: fmt.Errorf("not correct hex"), - }, - { - name: "valid hex", - args: args{ - param: "0xf6ab3267fa", - }, - want: []byte{0xf6, 0xab, 0x32, 0x67, 0xfa}, - }, - { - name: "valid hex with odd length", - args: args{ - param: "0xfab3267fa", - }, - want: []byte{0xf, 0xab, 0x32, 0x67, 0xfa}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := newBytesGenerator(tt.args.param) - - if tt.wantErr != nil { - require.ErrorAs(t, err, &tt.wantErr) - require.Nil(t, got) - return - } - - require.Nil(t, err) - require.NotNil(t, got) - - gotValues := got.Generate() - require.Equal(t, tt.want, gotValues) - }) - } -} - -func TestNewRandomBytesGenerator(t *testing.T) { - t.Parallel() - - type args struct { - param string - } - tests := []struct { - name string - args args - wantErr error - }{ - { - name: "empty", - args: args{ - param: "", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "not an int", - args: args{ - param: "x", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "too large", - args: args{ - param: "1001", - }, - wantErr: fmt.Errorf("random packet size must be less than 1000"), - }, - { - name: "valid", - args: args{ - param: "12", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := newRandomBytesGenerator(tt.args.param) - if tt.wantErr != nil { - require.ErrorAs(t, err, &tt.wantErr) - require.Nil(t, got) - return - } - - require.Nil(t, err) - require.NotNil(t, got) - first := got.Generate() - - second := got.Generate() - require.NotEqual(t, first, second) - }) - } -} - -func TestNewRandomASCIIGenerator(t *testing.T) { - t.Parallel() - - type args struct { - param string - } - tests := []struct { - name string - args args - wantErr error - }{ - { - name: "empty", - args: args{ - param: "", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "not an int", - args: args{ - param: "x", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "too large", - args: args{ - param: "1001", - }, - wantErr: fmt.Errorf("random packet size must be less than 1000"), - }, - { - name: "valid", - args: args{ - param: "12", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := newRandomASCIIGenerator(tt.args.param) - if tt.wantErr != nil { - require.ErrorAs(t, err, &tt.wantErr) - require.Nil(t, got) - return - } - - require.Nil(t, err) - require.NotNil(t, got) - first := got.Generate() - - second := got.Generate() - require.NotEqual(t, first, second) - }) - } -} - -func TestNewRandomDigitGenerator(t *testing.T) { - t.Parallel() - - type args struct { - param string - } - tests := []struct { - name string - args args - wantErr error - }{ - { - name: "empty", - args: args{ - param: "", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "not an int", - args: args{ - param: "x", - }, - wantErr: fmt.Errorf("parse int"), - }, - { - name: "too large", - args: args{ - param: "1001", - }, - wantErr: fmt.Errorf("random packet size must be less than 1000"), - }, - { - name: "valid", - args: args{ - param: "12", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := newRandomDigitGenerator(tt.args.param) - if tt.wantErr != nil { - require.ErrorAs(t, err, &tt.wantErr) - require.Nil(t, got) - return - } - - require.Nil(t, err) - require.NotNil(t, got) - first := got.Generate() - - second := got.Generate() - require.NotEqual(t, first, second) - }) - } -} - -func TestPacketCounterGenerator(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - param string - wantErr bool - }{ - { - name: "Valid empty param", - param: "", - wantErr: false, - }, - { - name: "Invalid non-empty param", - param: "anything", - wantErr: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - gen, err := newPacketCounterGenerator(tc.param) - if tc.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.Equal(t, 8, gen.Size()) - - // Reset counter to known value for test - initialCount := uint64(42) - PacketCounter.Store(initialCount) - - output := gen.Generate() - require.Equal(t, 8, len(output)) - - // Verify counter value in output - counterValue := binary.BigEndian.Uint64(output) - require.Equal(t, initialCount, counterValue) - - // Increment counter and verify change - PacketCounter.Add(1) - output = gen.Generate() - counterValue = binary.BigEndian.Uint64(output) - require.Equal(t, initialCount+1, counterValue) - }) - } -} diff --git a/device/awg/tag_junk_packet_generator.go b/device/awg/tag_junk_packet_generator.go deleted file mode 100644 index fdbebc8..0000000 --- a/device/awg/tag_junk_packet_generator.go +++ /dev/null @@ -1,59 +0,0 @@ -package awg - -import ( - "fmt" - "strconv" -) - -type TagJunkPacketGenerator struct { - name string - tagValue string - - packetSize int - generators []Generator -} - -func newTagJunkPacketGenerator(name, tagValue string, size int) TagJunkPacketGenerator { - return TagJunkPacketGenerator{ - name: name, - tagValue: tagValue, - generators: make([]Generator, 0, size), - } -} - -func (tg *TagJunkPacketGenerator) append(generator Generator) { - tg.generators = append(tg.generators, generator) - tg.packetSize += generator.Size() -} - -func (tg *TagJunkPacketGenerator) generatePacket() []byte { - packet := make([]byte, 0, tg.packetSize) - for _, generator := range tg.generators { - packet = append(packet, generator.Generate()...) - } - - return packet -} - -func (tg *TagJunkPacketGenerator) Name() string { - return tg.name -} - -func (tg *TagJunkPacketGenerator) nameIndex() (int, error) { - if len(tg.name) != 2 { - return 0, fmt.Errorf("name must be 2 character long: %s", tg.name) - } - - index, err := strconv.Atoi(tg.name[1:2]) - if err != nil { - return 0, fmt.Errorf("name 2 char should be an int %w", err) - } - return index, nil -} - -func (tg *TagJunkPacketGenerator) IpcGetFields() IpcFields { - return IpcFields{ - Key: tg.name, - Value: tg.tagValue, - } -} diff --git a/device/awg/tag_junk_packet_generator_test.go b/device/awg/tag_junk_packet_generator_test.go deleted file mode 100644 index 309d425..0000000 --- a/device/awg/tag_junk_packet_generator_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package awg - -import ( - "testing" - - "github.com/amnezia-vpn/amneziawg-go/device/awg/internal" - "github.com/stretchr/testify/require" -) - -func TestNewTagJunkGenerator(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - genName string - size int - expected TagJunkPacketGenerator - }{ - { - name: "Create new generator with empty name", - genName: "", - size: 0, - expected: TagJunkPacketGenerator{ - name: "", - packetSize: 0, - generators: make([]Generator, 0), - }, - }, - { - name: "Create new generator with valid name", - genName: "T1", - size: 0, - expected: TagJunkPacketGenerator{ - name: "T1", - packetSize: 0, - generators: make([]Generator, 0), - }, - }, - { - name: "Create new generator with non-zero size", - genName: "T2", - size: 5, - expected: TagJunkPacketGenerator{ - name: "T2", - packetSize: 0, - generators: make([]Generator, 5), - }, - }, - } - - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - result := newTagJunkPacketGenerator(tc.genName, "", tc.size) - require.Equal(t, tc.expected.name, result.name) - require.Equal(t, tc.expected.packetSize, result.packetSize) - require.Equal(t, cap(result.generators), len(tc.expected.generators)) - }) - } -} - -func TestTagJunkGeneratorAppend(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - initialState TagJunkPacketGenerator - mockSize int - expectedLength int - expectedSize int - }{ - { - name: "Append to empty generator", - initialState: newTagJunkPacketGenerator("T1", "", 0), - mockSize: 5, - expectedLength: 1, - expectedSize: 5, - }, - { - name: "Append to non-empty generator", - initialState: TagJunkPacketGenerator{ - name: "T2", - packetSize: 10, - generators: make([]Generator, 2), - }, - mockSize: 7, - expectedLength: 3, // 2 existing + 1 new - expectedSize: 17, // 10 + 7 - }, - } - - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - tg := tc.initialState - mockGen := internal.NewMockGenerator(tc.mockSize) - - tg.append(mockGen) - - require.Equal(t, tc.expectedLength, len(tg.generators)) - require.Equal(t, tc.expectedSize, tg.packetSize) - }) - } -} - -func TestTagJunkGeneratorGenerate(t *testing.T) { - t.Parallel() - - // Create mock generators for testing - mockGen1 := internal.NewMockByteGenerator([]byte{0x01, 0x02}) - mockGen2 := internal.NewMockByteGenerator([]byte{0x03, 0x04, 0x05}) - - testCases := []struct { - name string - setupGenerator func() TagJunkPacketGenerator - expected []byte - }{ - { - name: "Generate with empty generators", - setupGenerator: func() TagJunkPacketGenerator { - return newTagJunkPacketGenerator("T1", "", 0) - }, - expected: []byte{}, - }, - { - name: "Generate with single generator", - setupGenerator: func() TagJunkPacketGenerator { - tg := newTagJunkPacketGenerator("T2", "", 0) - tg.append(mockGen1) - return tg - }, - expected: []byte{0x01, 0x02}, - }, - { - name: "Generate with multiple generators", - setupGenerator: func() TagJunkPacketGenerator { - tg := newTagJunkPacketGenerator("T3", "", 0) - tg.append(mockGen1) - tg.append(mockGen2) - return tg - }, - expected: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, - }, - } - - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - tg := tc.setupGenerator() - result := tg.generatePacket() - - require.Equal(t, tc.expected, result) - }) - } -} - -func TestTagJunkGeneratorNameIndex(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - generatorName string - expectedIndex int - expectError bool - }{ - { - name: "Valid name with digit", - generatorName: "T5", - expectedIndex: 5, - expectError: false, - }, - { - name: "Invalid name - too short", - generatorName: "T", - expectError: true, - }, - { - name: "Invalid name - too long", - generatorName: "T55", - expectError: true, - }, - { - name: "Invalid name - non-digit second character", - generatorName: "TX", - expectError: true, - }, - } - - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - tg := TagJunkPacketGenerator{name: tc.generatorName} - index, err := tg.nameIndex() - - if tc.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tc.expectedIndex, index) - } - }) - } -} diff --git a/device/awg/tag_junk_packet_generators.go b/device/awg/tag_junk_packet_generators.go deleted file mode 100644 index 9921eb0..0000000 --- a/device/awg/tag_junk_packet_generators.go +++ /dev/null @@ -1,66 +0,0 @@ -package awg - -import "fmt" - -type TagJunkPacketGenerators struct { - tagGenerators []TagJunkPacketGenerator - length int - DefaultJunkCount int // Jc -} - -func (generators *TagJunkPacketGenerators) AppendGenerator( - generator TagJunkPacketGenerator, -) { - generators.tagGenerators = append(generators.tagGenerators, generator) - generators.length++ -} - -func (generators *TagJunkPacketGenerators) IsDefined() bool { - return len(generators.tagGenerators) > 0 -} - -// validate that packets were defined consecutively -func (generators *TagJunkPacketGenerators) Validate() error { - seen := make([]bool, len(generators.tagGenerators)) - for _, generator := range generators.tagGenerators { - index, err := generator.nameIndex() - if index > len(generators.tagGenerators) { - return fmt.Errorf("junk packet index should be consecutive") - } - if err != nil { - return fmt.Errorf("name index: %w", err) - } else { - seen[index-1] = true - } - } - - for _, found := range seen { - if !found { - return fmt.Errorf("junk packet index should be consecutive") - } - } - - return nil -} - -func (generators *TagJunkPacketGenerators) GeneratePackets() [][]byte { - var rv = make([][]byte, 0, generators.length+generators.DefaultJunkCount) - - for i, tagGenerator := range generators.tagGenerators { - rv = append(rv, make([]byte, tagGenerator.packetSize)) - copy(rv[i], tagGenerator.generatePacket()) - PacketCounter.Inc() - } - PacketCounter.Add(uint64(generators.DefaultJunkCount)) - - return rv -} - -func (tg *TagJunkPacketGenerators) IpcGetFields() []IpcFields { - rv := make([]IpcFields, 0, len(tg.tagGenerators)) - for _, generator := range tg.tagGenerators { - rv = append(rv, generator.IpcGetFields()) - } - - return rv -} diff --git a/device/awg/tag_junk_packet_generators_test.go b/device/awg/tag_junk_packet_generators_test.go deleted file mode 100644 index 6b1fd47..0000000 --- a/device/awg/tag_junk_packet_generators_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package awg - -import ( - "testing" - - "github.com/amnezia-vpn/amneziawg-go/device/awg/internal" - "github.com/stretchr/testify/require" -) - -func TestTagJunkGeneratorHandlerAppendGenerator(t *testing.T) { - tests := []struct { - name string - generator TagJunkPacketGenerator - }{ - { - name: "append single generator", - generator: newTagJunkPacketGenerator("t1", "", 10), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - generators := &TagJunkPacketGenerators{} - - // Initial length should be 0 - require.Equal(t, 0, generators.length) - require.Empty(t, generators.tagGenerators) - - // After append, length should be 1 and generator should be added - generators.AppendGenerator(tt.generator) - require.Equal(t, 1, generators.length) - require.Len(t, generators.tagGenerators, 1) - require.Equal(t, tt.generator, generators.tagGenerators[0]) - }) - } -} - -func TestTagJunkGeneratorHandlerValidate(t *testing.T) { - tests := []struct { - name string - generators []TagJunkPacketGenerator - wantErr bool - errMsg string - }{ - { - name: "bad start", - generators: []TagJunkPacketGenerator{ - newTagJunkPacketGenerator("t3", "", 10), - newTagJunkPacketGenerator("t4", "", 10), - }, - wantErr: true, - errMsg: "junk packet index should be consecutive", - }, - { - name: "non-consecutive indices", - generators: []TagJunkPacketGenerator{ - newTagJunkPacketGenerator("t1", "", 10), - newTagJunkPacketGenerator("t3", "", 10), // Missing t2 - }, - wantErr: true, - errMsg: "junk packet index should be consecutive", - }, - { - name: "consecutive indices", - generators: []TagJunkPacketGenerator{ - newTagJunkPacketGenerator("t1", "", 10), - newTagJunkPacketGenerator("t2", "", 10), - newTagJunkPacketGenerator("t3", "", 10), - newTagJunkPacketGenerator("t4", "", 10), - newTagJunkPacketGenerator("t5", "", 10), - }, - }, - { - name: "nameIndex error", - generators: []TagJunkPacketGenerator{ - newTagJunkPacketGenerator("error", "", 10), - }, - wantErr: true, - errMsg: "name must be 2 character long", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - generators := &TagJunkPacketGenerators{} - for _, gen := range tt.generators { - generators.AppendGenerator(gen) - } - - err := generators.Validate() - if tt.wantErr { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errMsg) - return - } - require.NoError(t, err) - }) - } -} - -func TestTagJunkGeneratorHandlerGenerate(t *testing.T) { - mockByte1 := []byte{0x01, 0x02} - mockByte2 := []byte{0x03, 0x04, 0x05} - mockGen1 := internal.NewMockByteGenerator(mockByte1) - mockGen2 := internal.NewMockByteGenerator(mockByte2) - - tests := []struct { - name string - setupGenerator func() []TagJunkPacketGenerator - expected [][]byte - }{ - { - name: "generate with no default junk", - setupGenerator: func() []TagJunkPacketGenerator { - tg1 := newTagJunkPacketGenerator("t1", "", 0) - tg1.append(mockGen1) - tg1.append(mockGen2) - tg2 := newTagJunkPacketGenerator("t2", "", 0) - tg2.append(mockGen2) - tg2.append(mockGen1) - - return []TagJunkPacketGenerator{tg1, tg2} - }, - expected: [][]byte{ - append(mockByte1, mockByte2...), - append(mockByte2, mockByte1...), - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - generators := &TagJunkPacketGenerators{} - tagGenerators := tt.setupGenerator() - for _, gen := range tagGenerators { - generators.AppendGenerator(gen) - } - - result := generators.GeneratePackets() - require.Equal(t, result, tt.expected) - }) - } -} diff --git a/device/awg/tag_parser.go b/device/awg/tag_parser.go deleted file mode 100644 index 06ba49b..0000000 --- a/device/awg/tag_parser.go +++ /dev/null @@ -1,112 +0,0 @@ -package awg - -import ( - "fmt" - "maps" - "regexp" - "strings" -) - -type IpcFields struct{ Key, Value string } - -type EnumTag string - -const ( - BytesEnumTag EnumTag = "b" - CounterEnumTag EnumTag = "c" - TimestampEnumTag EnumTag = "t" - RandomBytesEnumTag EnumTag = "r" - RandomASCIIEnumTag EnumTag = "rc" - RandomDigitEnumTag EnumTag = "rd" -) - -var generatorCreator = map[EnumTag]newGenerator{ - BytesEnumTag: newBytesGenerator, - CounterEnumTag: newPacketCounterGenerator, - TimestampEnumTag: newTimestampGenerator, - RandomBytesEnumTag: newRandomBytesGenerator, - RandomASCIIEnumTag: newRandomASCIIGenerator, - RandomDigitEnumTag: newRandomDigitGenerator, -} - -// helper map to determine enumTags are unique -var uniqueTags = map[EnumTag]bool{ - CounterEnumTag: false, - TimestampEnumTag: false, -} - -type Tag struct { - Name EnumTag - Param string -} - -func parseTag(input string) (Tag, error) { - // Regular expression to match - re := regexp.MustCompile(`([a-zA-Z]+)(?:\s+([^>]+))?>`) - - match := re.FindStringSubmatch(input) - tag := Tag{ - Name: EnumTag(match[1]), - } - if len(match) > 2 && match[2] != "" { - tag.Param = strings.TrimSpace(match[2]) - } - - return tag, nil -} - -func ParseTagJunkGenerator(name, input string) (TagJunkPacketGenerator, error) { - inputSlice := strings.Split(input, "<") - if len(inputSlice) <= 1 { - return TagJunkPacketGenerator{}, fmt.Errorf("empty input: %s", input) - } - - uniqueTagCheck := make(map[EnumTag]bool, len(uniqueTags)) - maps.Copy(uniqueTagCheck, uniqueTags) - - // skip byproduct of split - inputSlice = inputSlice[1:] - rv := newTagJunkPacketGenerator(name, input, len(inputSlice)) - for _, inputParam := range inputSlice { - if len(inputParam) <= 1 { - return TagJunkPacketGenerator{}, fmt.Errorf( - "empty tag in input: %s", - inputSlice, - ) - } else if strings.Count(inputParam, ">") != 1 { - return TagJunkPacketGenerator{}, fmt.Errorf("ill formated input: %s", input) - } - - tag, _ := parseTag(inputParam) - creator, ok := generatorCreator[tag.Name] - if !ok { - return TagJunkPacketGenerator{}, fmt.Errorf("invalid tag: %s", tag.Name) - } - if present, ok := uniqueTagCheck[tag.Name]; ok { - if present { - return TagJunkPacketGenerator{}, fmt.Errorf( - "tag %s needs to be unique", - tag.Name, - ) - } - uniqueTagCheck[tag.Name] = true - } - generator, err := creator(tag.Param) - if err != nil { - return TagJunkPacketGenerator{}, fmt.Errorf("gen: %w", err) - } - - // TODO: handle counter tag - // if tag.Name == CounterEnumTag { - // packetCounter, ok := generator.(*PacketCounterGenerator) - // if !ok { - // log.Fatalf("packet counter generator expected, got %T", generator) - // } - // PacketCounter = packetCounter.counter - // } - - rv.append(generator) - } - - return rv, nil -} diff --git a/device/awg/tag_parser_test.go b/device/awg/tag_parser_test.go deleted file mode 100644 index 3229cee..0000000 --- a/device/awg/tag_parser_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package awg - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParse(t *testing.T) { - type args struct { - name string - input string - } - tests := []struct { - name string - args args - wantErr error - }{ - { - name: "invalid name", - args: args{name: "apple", input: ""}, - wantErr: fmt.Errorf("ill formated input"), - }, - { - name: "empty", - args: args{name: "i1", input: ""}, - wantErr: fmt.Errorf("ill formated input"), - }, - { - name: "extra >", - args: args{name: "i1", input: ">"}, - wantErr: fmt.Errorf("ill formated input"), - }, - { - name: "extra <", - args: args{name: "i1", input: "<"}, - wantErr: fmt.Errorf("empty tag in input"), - }, - { - name: "empty <>", - args: args{name: "i1", input: "<>"}, - wantErr: fmt.Errorf("empty tag in input"), - }, - { - name: "invalid tag", - args: args{name: "i1", input: ""}, - wantErr: fmt.Errorf("invalid tag"), - }, - { - name: "counter uniqueness violation", - args: args{name: "i1", input: ""}, - wantErr: fmt.Errorf("parse tag needs to be unique"), - }, - { - name: "timestamp uniqueness violation", - args: args{name: "i1", input: ""}, - wantErr: fmt.Errorf("parse tag needs to be unique"), - }, - { - name: "valid", - args: args{input: ""}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := ParseTagJunkGenerator(tt.args.name, tt.args.input) - - // TODO: ErrorAs doesn't work as you think - if tt.wantErr != nil { - require.ErrorAs(t, err, &tt.wantErr) - return - } - require.Nil(t, err) - }) - } -} diff --git a/device/cookie_test.go b/device/cookie_test.go index e5a2bd4..9df3049 100644 --- a/device/cookie_test.go +++ b/device/cookie_test.go @@ -99,7 +99,7 @@ func TestCookieMAC1(t *testing.T) { 0x8c, 0xe1, 0xe8, 0xfa, 0x67, 0x20, 0x80, 0x6d, } generator.AddMacs(msg) - reply, err := checker.CreateReply(msg, 1377, src, DefaultMessageCookieReplyType) + reply, err := checker.CreateReply(msg, 1377, src, MessageCookieReplyType) if err != nil { t.Fatal("Failed to create cookie reply:", err) } diff --git a/device/device.go b/device/device.go index 46cf04e..2fdf85c 100644 --- a/device/device.go +++ b/device/device.go @@ -6,57 +6,17 @@ package device import ( - "encoding/binary" - "errors" - "fmt" "runtime" "sync" "sync/atomic" "time" "github.com/amnezia-vpn/amneziawg-go/conn" - "github.com/amnezia-vpn/amneziawg-go/device/awg" - "github.com/amnezia-vpn/amneziawg-go/ipc" "github.com/amnezia-vpn/amneziawg-go/ratelimiter" "github.com/amnezia-vpn/amneziawg-go/rwcancel" "github.com/amnezia-vpn/amneziawg-go/tun" ) -type Version uint8 - -const ( - VersionDefault Version = iota - VersionAwg - VersionAwgSpecialHandshake -) - -// TODO: -type AtomicVersion struct { - value atomic.Uint32 -} - -func NewAtomicVersion(v Version) *AtomicVersion { - av := &AtomicVersion{} - av.Store(v) - return av -} - -func (av *AtomicVersion) Load() Version { - return Version(av.value.Load()) -} - -func (av *AtomicVersion) Store(v Version) { - av.value.Store(uint32(v)) -} - -func (av *AtomicVersion) CompareAndSwap(old, new Version) bool { - return av.value.CompareAndSwap(uint32(old), uint32(new)) -} - -func (av *AtomicVersion) Swap(new Version) Version { - return Version(av.value.Swap(uint32(new))) -} - type Device struct { state struct { // state holds the device's state. It is accessed atomically. @@ -130,8 +90,27 @@ type Device struct { closed chan struct{} log *Logger - version Version - awg awg.Protocol + 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 } // deviceState represents the state of a Device. @@ -342,6 +321,11 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { device.rate.limiter.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() // create queues @@ -439,8 +423,6 @@ func (device *Device) Close() { device.rate.limiter.Close() - device.resetProtocol() - device.log.Verbosef("Device closed") close(device.closed) } @@ -580,358 +562,3 @@ func (device *Device) BindClose() error { device.net.Unlock() return err } - -func (device *Device) isAWG() bool { - return device.version >= VersionAwg -} - -func (device *Device) resetProtocol() { - // restore default message type values - MessageInitiationType = DefaultMessageInitiationType - MessageResponseType = DefaultMessageResponseType - MessageCookieReplyType = DefaultMessageCookieReplyType - MessageTransportType = DefaultMessageTransportType -} - -func (device *Device) handlePostConfig(tempAwg *awg.Protocol) error { - if !tempAwg.Cfg.IsSet && !tempAwg.HandshakeHandler.IsSet { - return nil - } - - var errs []error - - isAwgOn := false - device.awg.Mux.Lock() - if tempAwg.Cfg.JunkPacketCount < 0 { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - "JunkPacketCount should be non negative", - ), - ) - } - device.awg.Cfg.JunkPacketCount = tempAwg.Cfg.JunkPacketCount - if tempAwg.Cfg.JunkPacketCount != 0 { - isAwgOn = true - } - - device.awg.Cfg.JunkPacketMinSize = tempAwg.Cfg.JunkPacketMinSize - if tempAwg.Cfg.JunkPacketMinSize != 0 { - isAwgOn = true - } - - if device.awg.Cfg.JunkPacketCount > 0 && - tempAwg.Cfg.JunkPacketMaxSize == tempAwg.Cfg.JunkPacketMinSize { - - tempAwg.Cfg.JunkPacketMaxSize++ // to make rand gen work - } - - if tempAwg.Cfg.JunkPacketMaxSize >= MaxSegmentSize { - device.awg.Cfg.JunkPacketMinSize = 0 - device.awg.Cfg.JunkPacketMaxSize = 1 - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - "JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d", - tempAwg.Cfg.JunkPacketMaxSize, - MaxSegmentSize, - )) - } else if tempAwg.Cfg.JunkPacketMaxSize < tempAwg.Cfg.JunkPacketMinSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - "maxSize: %d; should be greater than minSize: %d", - tempAwg.Cfg.JunkPacketMaxSize, - tempAwg.Cfg.JunkPacketMinSize, - )) - } else { - device.awg.Cfg.JunkPacketMaxSize = tempAwg.Cfg.JunkPacketMaxSize - } - - if tempAwg.Cfg.JunkPacketMaxSize != 0 { - isAwgOn = true - } - - magicHeaders := make([]awg.MagicHeader, 4) - - if len(tempAwg.Cfg.MagicHeaders.Values) != 4 { - return ipcErrorf( - ipc.IpcErrorInvalid, - "magic headers should have 4 values; got: %d", - len(tempAwg.Cfg.MagicHeaders.Values), - ) - } - - if tempAwg.Cfg.MagicHeaders.Values[0].Min > 4 { - isAwgOn = true - device.log.Verbosef("UAPI: Updating init_packet_magic_header") - magicHeaders[0] = tempAwg.Cfg.MagicHeaders.Values[0] - - MessageInitiationType = magicHeaders[0].Min - } else { - device.log.Verbosef("UAPI: Using default init type") - MessageInitiationType = DefaultMessageInitiationType - magicHeaders[0] = awg.NewMagicHeaderSameValue(DefaultMessageInitiationType) - } - - if tempAwg.Cfg.MagicHeaders.Values[1].Min > 4 { - isAwgOn = true - - device.log.Verbosef("UAPI: Updating response_packet_magic_header") - magicHeaders[1] = tempAwg.Cfg.MagicHeaders.Values[1] - MessageResponseType = magicHeaders[1].Min - } else { - device.log.Verbosef("UAPI: Using default response type") - MessageResponseType = DefaultMessageResponseType - magicHeaders[1] = awg.NewMagicHeaderSameValue(DefaultMessageResponseType) - } - - if tempAwg.Cfg.MagicHeaders.Values[2].Min > 4 { - isAwgOn = true - - device.log.Verbosef("UAPI: Updating underload_packet_magic_header") - magicHeaders[2] = tempAwg.Cfg.MagicHeaders.Values[2] - MessageCookieReplyType = magicHeaders[2].Min - } else { - device.log.Verbosef("UAPI: Using default underload type") - MessageCookieReplyType = DefaultMessageCookieReplyType - magicHeaders[2] = awg.NewMagicHeaderSameValue(DefaultMessageCookieReplyType) - } - - if tempAwg.Cfg.MagicHeaders.Values[3].Min > 4 { - isAwgOn = true - - device.log.Verbosef("UAPI: Updating transport_packet_magic_header") - magicHeaders[3] = tempAwg.Cfg.MagicHeaders.Values[3] - MessageTransportType = magicHeaders[3].Min - } else { - device.log.Verbosef("UAPI: Using default transport type") - MessageTransportType = DefaultMessageTransportType - magicHeaders[3] = awg.NewMagicHeaderSameValue(DefaultMessageTransportType) - } - - var err error - device.awg.Cfg.MagicHeaders, err = awg.NewMagicHeaders(magicHeaders) - if err != nil { - errs = append(errs, ipcErrorf(ipc.IpcErrorInvalid, "new magic headers: %w", err)) - } - - isSameHeaderMap := map[uint32]struct{}{ - MessageInitiationType: {}, - MessageResponseType: {}, - MessageCookieReplyType: {}, - MessageTransportType: {}, - } - - // size will be different if same values - if len(isSameHeaderMap) != 4 { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d`, - MessageInitiationType, - MessageResponseType, - MessageCookieReplyType, - MessageTransportType, - ), - ) - } - - newInitSize := MessageInitiationSize + tempAwg.Cfg.InitHeaderJunkSize - - if newInitSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `init header size(148) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.Cfg.InitHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.Cfg.InitHeaderJunkSize = tempAwg.Cfg.InitHeaderJunkSize - } - - if tempAwg.Cfg.InitHeaderJunkSize != 0 { - isAwgOn = true - } - - newResponseSize := MessageResponseSize + tempAwg.Cfg.ResponseHeaderJunkSize - - if newResponseSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `response header size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.Cfg.ResponseHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.Cfg.ResponseHeaderJunkSize = tempAwg.Cfg.ResponseHeaderJunkSize - } - - if tempAwg.Cfg.ResponseHeaderJunkSize != 0 { - isAwgOn = true - } - - newCookieSize := MessageCookieReplySize + tempAwg.Cfg.CookieReplyHeaderJunkSize - - if newCookieSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `cookie reply size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.Cfg.CookieReplyHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.Cfg.CookieReplyHeaderJunkSize = tempAwg.Cfg.CookieReplyHeaderJunkSize - } - - if tempAwg.Cfg.CookieReplyHeaderJunkSize != 0 { - isAwgOn = true - } - - newTransportSize := MessageTransportSize + tempAwg.Cfg.TransportHeaderJunkSize - - if newTransportSize >= MaxSegmentSize { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `transport size(92) + junkSize:%d; should be smaller than maxSegmentSize: %d`, - tempAwg.Cfg.TransportHeaderJunkSize, - MaxSegmentSize, - ), - ) - } else { - device.awg.Cfg.TransportHeaderJunkSize = tempAwg.Cfg.TransportHeaderJunkSize - } - - if tempAwg.Cfg.TransportHeaderJunkSize != 0 { - isAwgOn = true - } - - isSameSizeMap := map[int]struct{}{ - newInitSize: {}, - newResponseSize: {}, - newCookieSize: {}, - newTransportSize: {}, - } - - if len(isSameSizeMap) != 4 { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, - `new sizes should differ; init: %d; response: %d; cookie: %d; trans: %d`, - newInitSize, - newResponseSize, - newCookieSize, - newTransportSize, - ), - ) - } else { - msgTypeToJunkSize = map[uint32]int{ - MessageInitiationType: device.awg.Cfg.InitHeaderJunkSize, - MessageResponseType: device.awg.Cfg.ResponseHeaderJunkSize, - MessageCookieReplyType: device.awg.Cfg.CookieReplyHeaderJunkSize, - MessageTransportType: device.awg.Cfg.TransportHeaderJunkSize, - } - - packetSizeToMsgType = map[int]uint32{ - newInitSize: MessageInitiationType, - newResponseSize: MessageResponseType, - newCookieSize: MessageCookieReplyType, - newTransportSize: MessageTransportType, - } - } - - device.awg.IsOn.SetTo(isAwgOn) - device.awg.JunkCreator = awg.NewJunkCreator(device.awg.Cfg) - - if tempAwg.HandshakeHandler.IsSet { - if err := tempAwg.HandshakeHandler.Validate(); err != nil { - errs = append(errs, ipcErrorf( - ipc.IpcErrorInvalid, "handshake handler validate: %w", err)) - } else { - device.awg.HandshakeHandler = tempAwg.HandshakeHandler - device.awg.HandshakeHandler.SpecialJunk.DefaultJunkCount = tempAwg.Cfg.JunkPacketCount - device.version = VersionAwgSpecialHandshake - } - } else { - device.version = VersionAwg - } - - device.awg.Mux.Unlock() - - return errors.Join(errs...) -} - -func (device *Device) ProcessAWGPacket(size int, packet *[]byte, buffer *[MaxMessageSize]byte) (uint32, error) { - // TODO: - // if awg.WaitResponse.ShouldWait.IsSet() { - // awg.WaitResponse.Channel <- struct{}{} - // } - - expectedMsgType, isKnownSize := packetSizeToMsgType[size] - if !isKnownSize { - msgType, err := device.handleTransport(size, packet, buffer) - - if err != nil { - return 0, fmt.Errorf("handle transport: %w", err) - } - - return msgType, nil - } - - junkSize := msgTypeToJunkSize[expectedMsgType] - - // transport size can align with other header types; - // making sure we have the right actualMsgType - actualMsgType, err := device.getMsgType(packet, junkSize) - if err != nil { - return 0, fmt.Errorf("get msg type: %w", err) - } - - if actualMsgType == expectedMsgType { - *packet = (*packet)[junkSize:] - return actualMsgType, nil - } - - device.log.Verbosef("awg: transport packet lined up with another msg type") - - msgType, err := device.handleTransport(size, packet, buffer) - if err != nil { - return 0, fmt.Errorf("handle transport: %w", err) - } - - return msgType, nil -} - -func (device *Device) getMsgType(packet *[]byte, junkSize int) (uint32, error) { - msgTypeValue := binary.LittleEndian.Uint32((*packet)[junkSize : junkSize+4]) - msgType, err := device.awg.GetMagicHeaderMinFor(msgTypeValue) - - if err != nil { - return 0, fmt.Errorf("get magic header min: %w", err) - } - - return msgType, nil -} - -func (device *Device) handleTransport(size int, packet *[]byte, buffer *[MaxMessageSize]byte) (uint32, error) { - junkSize := device.awg.Cfg.TransportHeaderJunkSize - - msgType, err := device.getMsgType(packet, junkSize) - if err != nil { - return 0, fmt.Errorf("get msg type: %w", err) - } - - if msgType != MessageTransportType { - // probably a junk packet - return 0, fmt.Errorf("Received message with unknown type: %d", msgType) - } - - if junkSize > 0 { - // remove junk from buffer by shifting the packet - // this buffer is also used for decryption, so it needs to be corrected - copy((*buffer)[:size], (*packet)[junkSize:]) - size -= junkSize - // need to reinitialize packet as well - (*packet) = (*packet)[:size] - } - - return msgType, nil -} diff --git a/device/magic-header.go b/device/magic-header.go new file mode 100644 index 0000000..78e59d6 --- /dev/null +++ b/device/magic-header.go @@ -0,0 +1,63 @@ +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 { + high := int64(h.end - h.start + 1) + r, _ := rand.Int(rand.Reader, big.NewInt(high)) + return h.start + uint32(r.Int64()) +} diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 6e6fe58..86346ac 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -53,17 +53,11 @@ const ( ) const ( - DefaultMessageInitiationType uint32 = 1 - DefaultMessageResponseType uint32 = 2 - DefaultMessageCookieReplyType uint32 = 3 - DefaultMessageTransportType uint32 = 4 -) - -var ( - MessageInitiationType uint32 = DefaultMessageInitiationType - MessageResponseType uint32 = DefaultMessageResponseType - MessageCookieReplyType uint32 = DefaultMessageCookieReplyType - MessageTransportType uint32 = DefaultMessageTransportType + MessageUnknownType uint32 = 0 + MessageInitiationType uint32 = 1 + MessageResponseType uint32 = 2 + MessageCookieReplyType uint32 = 3 + MessageTransportType uint32 = 4 ) const ( @@ -82,11 +76,6 @@ const ( MessageTransportOffsetContent = 16 ) -var ( - packetSizeToMsgType map[int]uint32 - msgTypeToJunkSize map[uint32]int -) - /* Type is an 8-bit field, followed by 3 nul bytes, * by marshalling the messages in little-endian byteorder * we can treat these as a 32-bit unsigned int (for now) @@ -205,17 +194,7 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e handshake.mixHash(handshake.remoteStatic[:]) - msgType := DefaultMessageInitiationType - if device.isAWG() { - device.awg.Mux.RLock() - msgType, err = device.awg.GetMsgType(DefaultMessageInitiationType) - if err != nil { - device.awg.Mux.RUnlock() - return nil, fmt.Errorf("get message type: %w", err) - } - - device.awg.Mux.RUnlock() - } + msgType := device.headers.init.Generate() msg := MessageInitiation{ Type: msgType, @@ -274,13 +253,9 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { chainKey [blake2s.Size]byte ) - device.awg.Mux.RLock() - if msg.Type != MessageInitiationType { - device.awg.Mux.RUnlock() return nil } - device.awg.Mux.RUnlock() device.staticIdentity.RLock() defer device.staticIdentity.RUnlock() @@ -395,19 +370,7 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } var msg MessageResponse - if device.isAWG() { - device.awg.Mux.RLock() - msg.Type, err = device.awg.GetMsgType(DefaultMessageResponseType) - if err != nil { - device.awg.Mux.RUnlock() - return nil, fmt.Errorf("get message type: %w", err) - } - - device.awg.Mux.RUnlock() - } else { - msg.Type = DefaultMessageResponseType - } - + msg.Type = device.headers.response.Generate() msg.Sender = handshake.localIndex msg.Receiver = handshake.remoteIndex @@ -457,13 +420,9 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { - device.awg.Mux.RLock() - if msg.Type != MessageResponseType { - device.awg.Mux.RUnlock() return nil } - device.awg.Mux.RUnlock() // lookup handshake by receiver diff --git a/device/obf.go b/device/obf.go new file mode 100644 index 0000000..53c55ff --- /dev/null +++ b/device/obf.go @@ -0,0 +1,140 @@ +package device + +import ( + "errors" + "fmt" + "strings" +) + +type obfBuilder func(val string) (obf, error) + +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 +} diff --git a/device/obf_bytes.go b/device/obf_bytes.go new file mode 100644 index 0000000..68d722b --- /dev/null +++ b/device/obf_bytes.go @@ -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 +} diff --git a/device/obf_data.go b/device/obf_data.go new file mode 100644 index 0000000..42d3f65 --- /dev/null +++ b/device/obf_data.go @@ -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 +} diff --git a/device/obf_datasize.go b/device/obf_datasize.go new file mode 100644 index 0000000..7267e2a --- /dev/null +++ b/device/obf_datasize.go @@ -0,0 +1,38 @@ +package device + +import "strconv" + +func newDataSizeObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_datastring.go b/device/obf_datastring.go new file mode 100644 index 0000000..2701e95 --- /dev/null +++ b/device/obf_datastring.go @@ -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) +} diff --git a/device/obf_rand.go b/device/obf_rand.go new file mode 100644 index 0000000..edf461e --- /dev/null +++ b/device/obf_rand.go @@ -0,0 +1,39 @@ +package device + +import ( + "crypto/rand" + "strconv" +) + +func newRandObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_randchars.go b/device/obf_randchars.go new file mode 100644 index 0000000..1d9968c --- /dev/null +++ b/device/obf_randchars.go @@ -0,0 +1,48 @@ +package device + +import ( + "crypto/rand" + "strconv" + "unicode" +) + +const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func newRandCharObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_randdigits.go b/device/obf_randdigits.go new file mode 100644 index 0000000..4794bb1 --- /dev/null +++ b/device/obf_randdigits.go @@ -0,0 +1,48 @@ +package device + +import ( + "crypto/rand" + "strconv" + "unicode" +) + +const digits10 = "0123456789" + +func newRandDigitsObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_timestamp.go b/device/obf_timestamp.go new file mode 100644 index 0000000..0a8180b --- /dev/null +++ b/device/obf_timestamp.go @@ -0,0 +1,31 @@ +package device + +import ( + "encoding/binary" + "time" +) + +func newTimestampObf(_ string) (obf, error) { + return ×tampObf{}, 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 +} diff --git a/device/peer.go b/device/peer.go index e8a5168..8f88b2a 100644 --- a/device/peer.go +++ b/device/peer.go @@ -13,7 +13,6 @@ import ( "time" "github.com/amnezia-vpn/amneziawg-go/conn" - "github.com/amnezia-vpn/amneziawg-go/device/awg" ) type Peer struct { @@ -114,16 +113,6 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { return peer, nil } -func (peer *Peer) SendAndCountBuffers(buffers [][]byte) error { - err := peer.SendBuffers(buffers) - if err == nil { - awg.PacketCounter.Add(uint64(len(buffers))) - return nil - } - - return err -} - func (peer *Peer) SendBuffers(buffers [][]byte) error { peer.device.net.RLock() defer peer.device.net.RUnlock() diff --git a/device/receive.go b/device/receive.go index 4c34799..b3b6105 100644 --- a/device/receive.go +++ b/device/receive.go @@ -97,13 +97,13 @@ func (device *Device) RoutineReceiveIncoming( elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize) ) - for i := range bufsArrs { + for i := range maxBatchSize { bufsArrs[i] = device.GetMessageBuffer() bufs[i] = bufsArrs[i][:] } defer func() { - for i := 0; i < maxBatchSize; i++ { + for i := range maxBatchSize { if bufsArrs[i] != nil { device.PutMessageBuffer(bufsArrs[i]) } @@ -129,7 +129,6 @@ func (device *Device) RoutineReceiveIncoming( } deathSpiral = 0 - device.awg.Mux.RLock() // handle each packet in the batch for i, size := range sizes[:count] { if size < MinMessageSize { @@ -138,16 +137,12 @@ func (device *Device) RoutineReceiveIncoming( // check size of packet packet := bufsArrs[i][:size] - var msgType uint32 - if device.isAWG() { - msgType, err = device.ProcessAWGPacket(size, &packet, bufsArrs[i]) - if err != nil { - device.log.Verbosef("awg: process packet: %v", err) - continue - } - } else { - 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 { @@ -233,7 +228,6 @@ func (device *Device) RoutineReceiveIncoming( default: } } - device.awg.Mux.RUnlock() for peer, elemsContainer := range elemsByPeer { if peer.isRunning.Load() { peer.queue.inbound.c <- elemsContainer @@ -291,9 +285,6 @@ func (device *Device) RoutineHandshake(id int) { device.log.Verbosef("Routine: handshake worker %d - started", id) for elem := range device.queue.handshake.c { - - device.awg.Mux.RLock() - // handle cookie fields and ratelimiting switch elem.msgType { @@ -450,7 +441,6 @@ func (device *Device) RoutineHandshake(id int) { peer.SendKeepalive() } skip: - device.awg.Mux.RUnlock() device.PutMessageBuffer(elem.buffer) } } @@ -569,3 +559,57 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { device.PutInboundElementsContainer(elemsContainer) } } + +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 +} diff --git a/device/send.go b/device/send.go index 0861a04..5e5cc1b 100644 --- a/device/send.go +++ b/device/send.go @@ -7,8 +7,10 @@ package device import ( "bytes" + "crypto/rand" "encoding/binary" "errors" + "math/big" "net" "os" "sync" @@ -123,41 +125,28 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { peer.device.log.Errorf("%v - Failed to create initiation message: %v", peer, err) return err } + var sendBuffer [][]byte - // so only packet processed for cookie generation - var junkedHeader []byte - if peer.device.version >= VersionAwg { - var junks [][]byte - if peer.device.version == VersionAwgSpecialHandshake { - peer.device.awg.Mux.RLock() - // set junks depending on packet type - junks = peer.device.awg.HandshakeHandler.GenerateSpecialJunk() - if junks != nil { - peer.device.log.Verbosef("%v - Special junks sent", peer) - } - peer.device.awg.Mux.RUnlock() - } else { - junks = make([][]byte, 0, peer.device.awg.Cfg.JunkPacketCount) + for _, ipacket := range peer.device.ipackets { + if ipacket != nil { + buf := make([]byte, ipacket.ObfuscatedLen(0)) + ipacket.Obfuscate(buf, nil) + sendBuffer = append(sendBuffer, buf) } - peer.device.awg.Mux.RLock() - peer.device.awg.JunkCreator.CreateJunkPackets(&junks) - peer.device.awg.Mux.RUnlock() + } - if len(junks) > 0 { - err = peer.SendBuffers(junks) + jc := peer.device.junk.count + jmin := peer.device.junk.min + jmax := peer.device.junk.max - if err != nil { - peer.device.log.Errorf("%v - Failed to send junk packets: %v", peer, err) - return err - } - } + for i := 0; i < jc; i++ { + nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(jmax-jmin+1))) + n := int(nBig.Int64()) + jmin - junkedHeader, err = peer.device.awg.CreateInitHeaderJunk() - if err != nil { - peer.device.log.Errorf("%v - %v", peer, err) - return err - } + buf := make([]byte, n) + rand.Read(buf) + sendBuffer = append(sendBuffer, buf) } var buf [MessageInitiationSize]byte @@ -165,14 +154,20 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { binary.Write(writer, binary.LittleEndian, msg) packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) - junkedHeader = append(junkedHeader, packet...) peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - sendBuffer = append(sendBuffer, junkedHeader) + if padding := peer.device.paddings.init; padding > 0 { + buf := make([]byte, padding+len(packet)) + rand.Read(buf[:padding]) + copy(buf[padding:], packet) + packet = buf + } - err = peer.SendAndCountBuffers(sendBuffer) + sendBuffer = append(sendBuffer, packet) + + err = peer.SendBuffers(sendBuffer) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -194,19 +189,12 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - junkedHeader, err := peer.device.awg.CreateResponseHeaderJunk() - if err != nil { - peer.device.log.Errorf("%v - %v", peer, err) - return err - } - var buf [MessageResponseSize]byte writer := bytes.NewBuffer(buf[:0]) binary.Write(writer, binary.LittleEndian, response) packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) - junkedHeader = append(junkedHeader, packet...) err = peer.BeginSymmetricSession() if err != nil { @@ -218,32 +206,26 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketTraversal() 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 - err = peer.SendAndCountBuffers([][]byte{junkedHeader}) + err = peer.SendBuffers([][]byte{packet}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } return err } -func (device *Device) SendHandshakeCookie( - initiatingElem *QueueHandshakeElement, -) error { +func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) error { device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8]) - msgType := DefaultMessageCookieReplyType - if device.isAWG() { - device.awg.Mux.RLock() - - var err error - msgType, err = device.awg.GetMsgType(DefaultMessageCookieReplyType) - device.awg.Mux.RUnlock() - if err != nil { - device.log.Errorf("Get message type for cookie reply: %v", err) - return err - } - } + msgType := device.headers.cookie.Generate() reply, err := device.cookieChecker.CreateReply( initiatingElem.packet, @@ -256,19 +238,20 @@ func (device *Device) SendHandshakeCookie( return err } - junkedHeader, err := device.awg.CreateCookieReplyHeaderJunk() - if err != nil { - device.log.Errorf("%v - %v", device, err) - return err - } - var buf [MessageCookieReplySize]byte writer := bytes.NewBuffer(buf[:0]) 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 + } - junkedHeader = append(junkedHeader, writer.Bytes()...) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{junkedHeader}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint) return nil } @@ -532,18 +515,7 @@ func (device *Device) RoutineEncryption(id int) { fieldReceiver := header[4:8] fieldNonce := header[8:16] - msgType := DefaultMessageTransportType - if device.isAWG() { - device.awg.Mux.RLock() - - var err error - msgType, err = device.awg.GetMsgType(DefaultMessageTransportType) - device.awg.Mux.RUnlock() - if err != nil { - device.log.Errorf("get message type for transport: %v", err) - continue - } - } + msgType := device.headers.transport.Generate() binary.LittleEndian.PutUint32(fieldType, msgType) binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) @@ -603,13 +575,15 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { if len(elem.packet) != MessageKeepaliveSize { dataSent = true - junkedHeader, err := device.awg.CreateTransportHeaderJunk(len(elem.packet)) - if err != nil { - device.log.Errorf("%v - %v", device, err) - continue + if padding := device.paddings.transport; padding > 0 { + // elem.packet is stored at the start of elem.buffer + // with zero padding + 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)] } - - elem.packet = append(junkedHeader, elem.packet...) } bufs = append(bufs, elem.packet) } @@ -617,7 +591,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - err := peer.SendAndCountBuffers(bufs) + err := peer.SendBuffers(bufs) if dataSent { peer.timersDataSent() } diff --git a/device/uapi.go b/device/uapi.go index 6c4be05..cff247e 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,6 @@ import ( "sync" "time" - "github.com/amnezia-vpn/amneziawg-go/device/awg" "github.com/amnezia-vpn/amneziawg-go/ipc" ) @@ -98,42 +97,53 @@ func (device *Device) IpcGetOperation(w io.Writer) error { sendf("fwmark=%d", device.net.fwmark) } - if device.isAWG() { - if device.awg.Cfg.JunkPacketCount != 0 { - sendf("jc=%d", device.awg.Cfg.JunkPacketCount) - } - if device.awg.Cfg.JunkPacketMinSize != 0 { - sendf("jmin=%d", device.awg.Cfg.JunkPacketMinSize) - } - if device.awg.Cfg.JunkPacketMaxSize != 0 { - sendf("jmax=%d", device.awg.Cfg.JunkPacketMaxSize) - } - if device.awg.Cfg.InitHeaderJunkSize != 0 { - sendf("s1=%d", device.awg.Cfg.InitHeaderJunkSize) - } - if device.awg.Cfg.ResponseHeaderJunkSize != 0 { - sendf("s2=%d", device.awg.Cfg.ResponseHeaderJunkSize) - } - if device.awg.Cfg.CookieReplyHeaderJunkSize != 0 { - sendf("s3=%d", device.awg.Cfg.CookieReplyHeaderJunkSize) - } - if device.awg.Cfg.TransportHeaderJunkSize != 0 { - sendf("s4=%d", device.awg.Cfg.TransportHeaderJunkSize) - } - for i, magicHeader := range device.awg.Cfg.MagicHeaders.Values { - if magicHeader.Min > 4 { - if magicHeader.Min == magicHeader.Max { - sendf("h%d=%d", i+1, magicHeader.Min) - continue - } + if device.junk.count != 0 { + sendf("jc=%d", device.junk.count) + } - sendf("h%d=%d-%d", i+1, magicHeader.Min, magicHeader.Max) - } - } + if device.junk.min != 0 { + sendf("jmin=%d", device.junk.min) + } - specialJunkIpcFields := device.awg.HandshakeHandler.SpecialJunk.IpcGetFields() - for _, field := range specialJunkIpcFields { - sendf("%s=%s", field.Key, field.Value) + 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) } } @@ -187,20 +197,18 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { } }() + ipcDev := new(ipcSetDevice) peer := new(ipcSetPeer) deviceConfig := true - tempAwg := awg.Protocol{} - tempAwg.Cfg.MagicHeaders.Values = make([]awg.MagicHeader, 4) - scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if line == "" { // Blank line means terminate operation. - err := device.handlePostConfig(&tempAwg) + err := ipcDev.mergeWithDevice(device) if err != nil { - return err + return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err) } peer.handlePostConfig() return nil @@ -229,7 +237,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { var err error if deviceConfig { - err = device.handleDeviceLine(key, value, &tempAwg) + err = device.handleDeviceLine(key, value) } else { err = device.handlePeerLine(peer, key, value) } @@ -237,9 +245,9 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return err } } - err = device.handlePostConfig(&tempAwg) + err = ipcDev.mergeWithDevice(device) if err != nil { - return err + return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err) } peer.handlePostConfig() @@ -249,7 +257,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return nil } -func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) error { +func (device *Device) handleDeviceLine(key, value string) error { switch key { case "private_key": var sk NoisePrivateKey @@ -300,112 +308,145 @@ func (device *Device) handleDeviceLine(key, value string, tempAwg *awg.Protocol) device.RemoveAllPeers() case "jc": - junkPacketCount, err := strconv.Atoi(value) + jc, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_count %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jc: %w", err) } - device.log.Verbosef("UAPI: Updating junk_packet_count") - tempAwg.Cfg.JunkPacketCount = junkPacketCount - tempAwg.Cfg.IsSet = true + 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": - junkPacketMinSize, err := strconv.Atoi(value) + jmin, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_min_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmin: %w", err) } - device.log.Verbosef("UAPI: Updating junk_packet_min_size") - tempAwg.Cfg.JunkPacketMinSize = junkPacketMinSize - tempAwg.Cfg.IsSet = true + 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": - junkPacketMaxSize, err := strconv.Atoi(value) + jmax, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse junk_packet_max_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmax: %w", err) } - device.log.Verbosef("UAPI: Updating junk_packet_max_size") - tempAwg.Cfg.JunkPacketMaxSize = junkPacketMaxSize - tempAwg.Cfg.IsSet = true + 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": - initPacketJunkSize, err := strconv.Atoi(value) + padding, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse init_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s1: %w", err) } - device.log.Verbosef("UAPI: Updating init_packet_junk_size") - tempAwg.Cfg.InitHeaderJunkSize = initPacketJunkSize - tempAwg.Cfg.IsSet = true + 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": - responsePacketJunkSize, err := strconv.Atoi(value) + padding, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse response_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s2: %w", err) } - device.log.Verbosef("UAPI: Updating response_packet_junk_size") - tempAwg.Cfg.ResponseHeaderJunkSize = responsePacketJunkSize - tempAwg.Cfg.IsSet = true + 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": - cookieReplyPacketJunkSize, err := strconv.Atoi(value) + padding, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse cookie_reply_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s3: %w", err) } - device.log.Verbosef("UAPI: Updating cookie_reply_packet_junk_size") - tempAwg.Cfg.CookieReplyHeaderJunkSize = cookieReplyPacketJunkSize - tempAwg.Cfg.IsSet = true + 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": - transportPacketJunkSize, err := strconv.Atoi(value) + padding, err := strconv.Atoi(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "parse transport_packet_junk_size %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s4: %w", err) } - device.log.Verbosef("UAPI: Updating transport_packet_junk_size") - tempAwg.Cfg.TransportHeaderJunkSize = transportPacketJunkSize - tempAwg.Cfg.IsSet = true + 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": - initMagicHeader, err := awg.ParseMagicHeader(key, value) + header, err := newMagicHeader(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H1: %w", err) } + device.headers.init = header - tempAwg.Cfg.MagicHeaders.Values[0] = initMagicHeader - tempAwg.Cfg.IsSet = true case "h2": - responseMagicHeader, err := awg.ParseMagicHeader(key, value) + header, err := newMagicHeader(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H2: %w", err) } + device.headers.response = header - tempAwg.Cfg.MagicHeaders.Values[1] = responseMagicHeader - tempAwg.Cfg.IsSet = true case "h3": - cookieReplyMagicHeader, err := awg.ParseMagicHeader(key, value) + header, err := newMagicHeader(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H3: %w", err) } + device.headers.cookie = header - tempAwg.Cfg.MagicHeaders.Values[2] = cookieReplyMagicHeader - tempAwg.Cfg.IsSet = true case "h4": - transportMagicHeader, err := awg.ParseMagicHeader(key, value) + header, err := newMagicHeader(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "uapi: %w", err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H4: %w", err) } + device.headers.transport = header - tempAwg.Cfg.MagicHeaders.Values[3] = transportMagicHeader - tempAwg.Cfg.IsSet = true - case "i1", "i2", "i3", "i4", "i5": - if len(value) == 0 { - device.log.Verbosef("UAPI: received empty %s", key) - return nil - } - - generators, err := awg.ParseTagJunkGenerator(key, value) + case "i1": + chain, err := newObfChain(value) if err != nil { - return ipcErrorf(ipc.IpcErrorInvalid, "invalid %s: %w", key, err) + return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I1: %w", err) } - device.log.Verbosef("UAPI: Updating %s", key) - tempAwg.HandshakeHandler.SpecialJunk.AppendGenerator(generators) - tempAwg.HandshakeHandler.IsSet = true + 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: return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key) } @@ -654,3 +695,49 @@ func (device *Device) IpcHandle(socket net.Conn) { 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 +} From 730d6c39d0c4e348a3d080bebe496664215e5c99 Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov Date: Sun, 30 Nov 2025 16:14:47 +0100 Subject: [PATCH 119/173] chore: add docs for the params from awg2 --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 428b752..f98db43 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,65 @@ $ git clone https://github.com/amnezia-vpn/amneziawg-go $ cd amneziawg-go $ make ``` - + +## Configuration + +> [!NOTE] +> If there is no value specified (for any param), AWG treats it as 0 + +### Junk packets + +The amount of junk packets specified in `Jc` with a random size between `Jmin` and `Jmax` would be generated and sent prior every handshake + +- `Jc: int`, recommended range is 4-12 +- `Jmin: int` <= `Jmax:int` + +> [!TIP] +> Junk packets do not carry any actual data, so there is no need to specify it on both sides. General recommendation is to use it on the client side only + +> [!IMPORTANT] +> If Jmax >= system MTU (not the one specified in AWG), then the system can fracture this packet into fragments, which looks suspicious from the censor side + +### Message paddings + +- `S1: int` - padding of handshake initial message +- `S2: int` - padding of handshake response message +- `S3: int` - padding of handshake cookie message +- `S4: int` - padding of transport messages + +### Message headers + +Every message in wireguard has `int32` type at the beginning of the packet. This field could be controlled by specifying the params below: + +- `H1: string` - header range of handshake initial message +- `H2: string` - header range of handshake initial message +- `H3: string` - header range of handshake cookie message +- `H4: string` - header range of transport message + +Values could be specified as: +- range: `x-y`, x <= y; e.g. `123-456` +- single value `1234` + +### Custom signature packets + +These packets are being send prior to every handshake, in the same way as Junk packets do. The sending order is `I1`, `I2`, `I3`, `I4`, `I5`. If there is no value specified, the packet is skipped. + +- `I1: string` +- `I2: string` +- `I3: string` +- `I4: string` +- `I5: string` + +Value is a sequence of tags specified below: +- `` - static bytes tag. Dumps `[seq]` as-is to the packet. `[seq]` is hex-encoded sequence which represents bytes sequence (2 hex numbers per byte) and is always even-sized +- `` - random bytes tag. Dumps `[size]` amount of randomly-generated bytes to the packet +- `` - random digits tag. Dumps `[size]` amount of randomly-generated bytes from `[0-9]` set to the packet +- `` - random chars tag. Dumps `[size]` amount of randomly-generated bytes from `[a-zA-Z] set to the packet +- `` - timestamp tag. Dumps 4-bytes long current system time in UNIX format +- `` - packet counter tag. Dumps 4-bytes long amount of packets sent by AWG + +> [!TIP] +> Custom signature packets does not carry any actual data, so there is no need to specify it on both sides. General recommendation is to use it on the client side only + +> [!IMPORTANT] +> If the final size of any packet exceeds system MTU, it would be fractured into fragments, which looks suspicious \ No newline at end of file From e796d477d89e6851b2bb4871bf75f1e621f94ace Mon Sep 17 00:00:00 2001 From: vkamn Date: Thu, 11 Dec 2025 18:56:42 +0800 Subject: [PATCH 120/173] chore: update license (#105) Signed-off-by: vkamn --- LICENSE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LICENSE b/LICENSE index f85e365..ab45fb3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +Copyright (C) 2017-2025 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 From 449d7cffd4adf86971bd679d0be5384b443e8be5 Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov <31506978+ygurov@users.noreply.github.com> Date: Fri, 19 Dec 2025 03:14:48 +0100 Subject: [PATCH 121/173] Feature/outline glue (#106) * feat: added outline integration layer * chore: make the function used in RegisterFallbackParser a standalone one * fix: check if domain has a dot prior trimming it * fix: use net.JoinHostPort instead of plain concat --- go.mod | 17 ++- go.sum | 34 ++++++ outline/dialer.go | 77 ++++++++++++++ outline/fallback.go | 224 +++++++++++++++++++++++++++++++++++++++ outline/fallback_test.go | 52 +++++++++ 5 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 outline/dialer.go create mode 100644 outline/fallback.go create mode 100644 outline/fallback_test.go diff --git a/go.mod b/go.mod index 8c4372d..a5f6548 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,27 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tevino/abool v1.2.0 go.uber.org/atomic v1.11.0 - golang.org/x/crypto v0.39.0 - golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 - golang.org/x/net v0.41.0 - golang.org/x/sys v0.33.0 + golang.org/x/crypto v0.42.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/net v0.44.0 + golang.org/x/sys v0.36.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 ) require ( + github.com/Jigsaw-Code/outline-sdk v0.0.20 // indirect + github.com/Jigsaw-Code/outline-sdk/x v0.0.8 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/goccy/go-yaml v1.17.1 // indirect github.com/google/btree v1.1.3 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect + golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3d8b3c2..8dde512 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,59 @@ +github.com/Jigsaw-Code/outline-sdk v0.0.20 h1:4ep7MK9lFmcyPIRIbn4xrP1VKdJNsqR6+iJEOHDKnNg= +github.com/Jigsaw-Code/outline-sdk v0.0.20/go.mod h1:CFDKyGZA4zatKE4vMLe8TyQpZCyINOeRFbMAmYHxodw= +github.com/Jigsaw-Code/outline-sdk/x v0.0.8 h1:fFHFXW7CKhRiegyNSdP25S/WIiVrRnMKysHDoO/N2Xg= +github.com/Jigsaw-Code/outline-sdk/x v0.0.8/go.mod h1:zqSH7yEYIQ0pYOhrr4QnodATVb5X/eZXV4AjUp9zhvs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= +github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= +github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b h1:WX7nnnLfCEXg+FmdYZPai2XuP3VqCP1HZVMST0n9DF0= +golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b/go.mod h1:EiXZlVfUTaAyySFVJb9rsODuiO+WXu8HrUuySb7nYFw= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/outline/dialer.go b/outline/dialer.go new file mode 100644 index 0000000..86aaff5 --- /dev/null +++ b/outline/dialer.go @@ -0,0 +1,77 @@ +package outline + +import ( + "context" + "fmt" + "net" + "net/netip" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" +) + +type DialerOptions struct { + Ipc string + Prefixes []netip.Prefix + Mtu int + Dns []netip.Addr +} + +func NewStreamDialer(opts DialerOptions) (*StreamDialer, error) { + var localAddresses []netip.Addr + for _, prefix := range opts.Prefixes { + localAddresses = append(localAddresses, prefix.Addr()) + } + + tun, tnet, err := netstack.CreateNetTUN(localAddresses, opts.Dns, opts.Mtu) + if err != nil { + return nil, fmt.Errorf("failed to create network tun: %v", err) + } + + awgLogger := device.Logger{ + Verbosef: func(format string, args ...any) { + }, + Errorf: func(format string, args ...any) { + }, + } + + dev := device.NewDevice(tun, conn.NewDefaultBind(), &awgLogger) + if err := dev.IpcSet(opts.Ipc); err != nil { + return nil, fmt.Errorf("failed to configure device: %v", err) + } + + if err := dev.Up(); err != nil { + return nil, fmt.Errorf("failed to start awg device: %v", err) + } + + return &StreamDialer{ + tnet: tnet, + }, nil +} + +var _ transport.StreamDialer = (*StreamDialer)(nil) + +type StreamDialer struct { + tnet *netstack.Net +} + +func (d *StreamDialer) DialStream(ctx context.Context, raddr string) (transport.StreamConn, error) { + host, port, err := net.SplitHostPort(raddr) + if err != nil { + return nil, fmt.Errorf("failed to parse raddr: %v", err) + } + if l := len(host); l > 0 && host[l-1] == '.' { + host = host[:l-1] + raddr = net.JoinHostPort(host, port) + } + + conn, err := d.tnet.DialContext(ctx, "tcp", raddr) + if err != nil { + return nil, err + } + + return conn.(*gonet.TCPConn), nil +} diff --git a/outline/fallback.go b/outline/fallback.go new file mode 100644 index 0000000..0f75dc9 --- /dev/null +++ b/outline/fallback.go @@ -0,0 +1,224 @@ +package outline + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "net/netip" + "strconv" + "strings" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/mobileproxy" + "github.com/Jigsaw-Code/outline-sdk/x/smart" + "github.com/goccy/go-yaml" +) + +type DeviceConfig struct { + PrivateKey string `yaml:"private_key"` + Address []string `yaml:"address"` + Dns []string `yaml:"dns"` + Mtu int `yaml:"mtu,omitempty"` + Jc int `yaml:"jc,omitempty"` + Jmin int `yaml:"jmin,omitempty"` + Jmax int `yaml:"jmax,omitempty"` + S1 int `yaml:"s1,omitempty"` + S2 int `yaml:"s2,omitempty"` + S3 int `yaml:"s3,omitempty"` + S4 int `yaml:"s4,omitempty"` + H1 string `yaml:"h1,omitempty"` + H2 string `yaml:"h2,omitempty"` + H3 string `yaml:"h3,omitempty"` + H4 string `yaml:"h4,omitempty"` + I1 string `yaml:"i1,omitempty"` + I2 string `yaml:"i2,omitempty"` + I3 string `yaml:"i3,omitempty"` + I4 string `yaml:"i4,omitempty"` + I5 string `yaml:"i5,omitempty"` + Peers []PeerConfig `yaml:"peers,omitempty"` +} + +type PeerConfig struct { + PublicKey string `yaml:"public_key"` + PresharedKey string `yaml:"preshared_key,omitempty"` + Endpoint string `yaml:"endpoint"` + AllowedIPs []string `yaml:"allowed_ips"` + PersistentKeepaliveInterval uint16 `yaml:"persistent_keepalive_interval,omitempty"` +} + +func mapYamlToConfig(y smart.YAMLNode) (*DeviceConfig, error) { + bytes, err := yaml.Marshal(y) + if err != nil { + return nil, fmt.Errorf("failed to marshal yaml: %v", err) + } + + var cfg DeviceConfig + if err = yaml.Unmarshal(bytes, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %v", err) + } + + return &cfg, nil +} + +func genIpcString(cfg *DeviceConfig) (string, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(cfg.PrivateKey) + if err != nil { + return "", fmt.Errorf("failed to decode private key: %v", err) + } + + var b strings.Builder + + b.WriteString("private_key=") + b.WriteString(hex.EncodeToString(privateKeyBytes)) + + if cfg.Jc != 0 { + b.WriteString("\njc=") + b.WriteString(strconv.Itoa(cfg.Jc)) + } + if cfg.Jmin != 0 { + b.WriteString("\njmin=") + b.WriteString(strconv.Itoa(cfg.Jmin)) + } + if cfg.Jmax != 0 { + b.WriteString("\njmax=") + b.WriteString(strconv.Itoa(cfg.Jmax)) + } + if cfg.S1 != 0 { + b.WriteString("\ns1=") + b.WriteString(strconv.Itoa(cfg.S1)) + } + if cfg.S2 != 0 { + b.WriteString("\ns2=") + b.WriteString(strconv.Itoa(cfg.S2)) + } + if cfg.S3 != 0 { + b.WriteString("\ns3=") + b.WriteString(strconv.Itoa(cfg.S3)) + } + if cfg.S4 != 0 { + b.WriteString("\ns4=") + b.WriteString(strconv.Itoa(cfg.S4)) + } + if cfg.H1 != "" { + b.WriteString("\nh1=") + b.WriteString(cfg.H1) + } + if cfg.H2 != "" { + b.WriteString("\nh2=") + b.WriteString(cfg.H2) + } + if cfg.H3 != "" { + b.WriteString("\nh3=") + b.WriteString(cfg.H3) + } + if cfg.H4 != "" { + b.WriteString("\nh4=") + b.WriteString(cfg.H4) + } + if cfg.I1 != "" { + b.WriteString("\ni1=") + b.WriteString(cfg.I1) + } + if cfg.I2 != "" { + b.WriteString("\ni2=") + b.WriteString(cfg.I2) + } + if cfg.I3 != "" { + b.WriteString("\ni3=") + b.WriteString(cfg.I3) + } + if cfg.I4 != "" { + b.WriteString("\ni4=") + b.WriteString(cfg.I4) + } + if cfg.I5 != "" { + b.WriteString("\ni5=") + b.WriteString(cfg.I5) + } + + for _, peer := range cfg.Peers { + publicKeyBytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) + if err != nil { + return "", fmt.Errorf("failed to decode public key: %v", err) + } + + b.WriteString("\npublic_key=") + b.WriteString(hex.EncodeToString(publicKeyBytes)) + + b.WriteString("\nendpoint=") + b.WriteString(peer.Endpoint) + + for _, allowedIp := range peer.AllowedIPs { + b.WriteString("\nallowed_ip=") + b.WriteString(allowedIp) + } + + if peer.PresharedKey != "" { + presharedKeyBytes, err := base64.StdEncoding.DecodeString(peer.PresharedKey) + if err != nil { + return "", fmt.Errorf("failed to decode preshared key: %v", err) + } + + b.WriteString("\npreshared_key=") + b.WriteString(hex.EncodeToString(presharedKeyBytes)) + } + + if peer.PersistentKeepaliveInterval != 0 { + b.WriteString("\npersistent_keepalive_interval=") + b.WriteString(strconv.Itoa(int(peer.PersistentKeepaliveInterval))) + } + } + + return b.String(), nil +} + +func FallbackParser(ctx context.Context, y smart.YAMLNode) (transport.StreamDialer, string, error) { + cfg, err := mapYamlToConfig(y) + if err != nil { + return nil, "", fmt.Errorf("failed to map yaml to config: %v", err) + } + + ipc, err := genIpcString(cfg) + if err != nil { + return nil, "", fmt.Errorf("faield to generate ipc config: %v", err) + } + + var prefixes []netip.Prefix + for _, address := range cfg.Address { + prefix, err := netip.ParsePrefix(address) + if err != nil { + return nil, "", fmt.Errorf("failed to parse address: %v", err) + } + prefixes = append(prefixes, prefix) + } + + var dns []netip.Addr + for _, saddr := range cfg.Dns { + addr, err := netip.ParseAddr(saddr) + if err != nil { + return nil, "", fmt.Errorf("failed to parse dns: %v", err) + } + dns = append(dns, addr) + } + + if cfg.Mtu == 0 { + cfg.Mtu = 1408 + } + + dialer, err := NewStreamDialer(DialerOptions{ + Ipc: ipc, + Prefixes: prefixes, + Mtu: cfg.Mtu, + Dns: dns, + }) + if err != nil { + return nil, "", fmt.Errorf("failed to create dialer: %v", err) + } + + return dialer, ipc, nil +} + +func RegisterFallbackParser(opt *mobileproxy.SmartDialerOptions, name string) { + opt.RegisterFallbackParser(name, FallbackParser) +} diff --git a/outline/fallback_test.go b/outline/fallback_test.go new file mode 100644 index 0000000..496f949 --- /dev/null +++ b/outline/fallback_test.go @@ -0,0 +1,52 @@ +package outline_test + +import ( + "testing" + + "github.com/Jigsaw-Code/outline-sdk/x/mobileproxy" + awg "github.com/amnezia-vpn/amneziawg-go/outline" +) + +const cfg = ` +dns: + - {system: {}} +tls: + - "" +fallback: + - awg: + address: [10.0.0.0/32] + dns: [8.8.8.8, 8.8.4.4] + private_key: +CdqlYvjqZ3OUr4mLWvGJo1h67CWpQwMIxA5OpyiJUM= + jc: 4 + jmin: 50 + jmax: 100 + s1: 87 + s2: 65 + s3: 43 + s4: 21 + h1: 1000000000-1000000001 + h2: 2000000000-2000000002 + h3: 3000000000-3000000003 + h4: 4000000000-4000000004 + peers: + - public_key: EGxNYihRLKQ9nvdOE5j5aZ7rtw3ttzJS1xxaJpgYYHI= + preshared_key: 2OiSh6rP3t/g39jgJNGK70B+nize821yIFNtUqi8/XU= + endpoint: 123.123.123.123:51820 + allowed_ips: [0.0.0.0/0, ::/0] + persistent_keepalive_interval: 25 +` + +var testDomains = mobileproxy.NewListFromLines("example.com") + +func Test_outlineIntegration(t *testing.T) { + opts := mobileproxy.NewSmartDialerOptions(testDomains, cfg) + opts.SetLogWriter(mobileproxy.NewStderrLogWriter()) + awg.RegisterFallbackParser(opts, "awg") + dialer, err := opts.NewStreamDialer() + if err != nil { + t.Fatal(err) + } + if _, err = mobileproxy.RunProxy("", dialer); err != nil { + t.Fatal(err) + } +} From 506b7631853cf3864335ef38f7f0b8453a285703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Feb 2026 15:47:47 +0800 Subject: [PATCH 122/173] Fix reserved bytes offset in StdNetBind.Send --- conn/bind_std.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index a1d4d9e..6bf5978 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -413,10 +413,10 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { err error ) for _, buf := range bufs { - if len(buf) > 3 { + if len(buf) > offset+3 { reserved, loaded := s.reservedForEndpoint[endpoint.(*StdNetEndpoint).AddrPort] if loaded { - copy(buf[1:4], reserved[:]) + copy(buf[offset+1:offset+4], reserved[:]) } } } From e7ef4339e718641fc7bc1b0ea41b538108de77cc Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov Date: Mon, 23 Mar 2026 11:01:42 +0000 Subject: [PATCH 123/173] readme: remove tag from tag reference --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f98db43..301b5bf 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ Value is a sequence of tags specified below: - `` - random digits tag. Dumps `[size]` amount of randomly-generated bytes from `[0-9]` set to the packet - `` - random chars tag. Dumps `[size]` amount of randomly-generated bytes from `[a-zA-Z] set to the packet - `` - timestamp tag. Dumps 4-bytes long current system time in UNIX format -- `` - packet counter tag. Dumps 4-bytes long amount of packets sent by AWG > [!TIP] > Custom signature packets does not carry any actual data, so there is no need to specify it on both sides. General recommendation is to use it on the client side only From 12a012205e3c444be02aba91a840455f74c127e1 Mon Sep 17 00:00:00 2001 From: Yaroslav Gurov Date: Tue, 31 Mar 2026 15:48:57 +0000 Subject: [PATCH 124/173] readme: actualize type for H1-H4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 301b5bf..e1c7309 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ The amount of junk packets specified in `Jc` with a random size between `Jmin` a ### Message headers -Every message in wireguard has `int32` type at the beginning of the packet. This field could be controlled by specifying the params below: +Every message in wireguard has `uint32` type at the beginning of the packet. This field could be controlled by specifying the params below: - `H1: string` - header range of handshake initial message - `H2: string` - header range of handshake initial message From f4f4c999267437c3eb909e8d0e5278fb4596d9a7 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 31 Mar 2026 16:37:57 +0300 Subject: [PATCH 125/173] fix: apply S4 transport padding to keepalive packets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keepalive packets were excluded from S4 padding because the padding logic was nested inside the dataSent guard. The receiving side (DeterminePacketTypeAndPadding) expects S4 padding on all transport packets, so unpadded keepalives fail H4 header validation and are silently dropped. This prevents the responder from completing key confirmation — lastHandshakeNano stays 0 until real data flows through the tunnel. --- device/send.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/device/send.go b/device/send.go index 5e5cc1b..0cc57da 100644 --- a/device/send.go +++ b/device/send.go @@ -574,16 +574,15 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { for _, elem := range elemsContainer.elems { if len(elem.packet) != MessageKeepaliveSize { dataSent = true - - if padding := device.paddings.transport; padding > 0 { - // elem.packet is stored at the start of elem.buffer - // with zero padding - 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)] + } + if padding := device.paddings.transport; padding > 0 { + // elem.packet is stored at the start of elem.buffer + // with zero padding + 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)] } bufs = append(bufs, elem.packet) } From 905a805bc6fc8741487faa7fbae498c1183740b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Nov 2022 15:29:08 +0800 Subject: [PATCH 126/173] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e460293..c7b1915 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ wireguard-go +/.idea/ +.DS_Store \ No newline at end of file From 680e63a4f80d2139b3ada2d831a5531d09a13774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 9 Dec 2023 16:59:55 +0800 Subject: [PATCH 127/173] Add remove unused script --- remove-unused.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 remove-unused.sh diff --git a/remove-unused.sh b/remove-unused.sh new file mode 100755 index 0000000..43f3f1b --- /dev/null +++ b/remove-unused.sh @@ -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" From e620c55272e3d2266f289e4d20d31d21a925ca28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 19:33:01 +0800 Subject: [PATCH 128/173] Add module rename script --- reformat.sh | 9 +++++++++ rename-module.sh | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 reformat.sh create mode 100755 rename-module.sh diff --git a/reformat.sh b/reformat.sh new file mode 100755 index 0000000..8d3ac05 --- /dev/null +++ b/reformat.sh @@ -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 diff --git a/rename-module.sh b/rename-module.sh new file mode 100755 index 0000000..a6bd016 --- /dev/null +++ b/rename-module.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +OLD_MODULE_NAME="github.com/tailscale/wireguard-go" +NEW_MODULE_NAME="github.com/sagernet/wireguard-go" + +rules=$(cat <.*) + by: $NEW_MODULE_NAME\$PATH +fix: \$NEW_IMPORT +EOF +) + +sg scan --inline-rules "$rules" -U + +sed -i "s|module $OLD_MODULE_NAME|module $NEW_MODULE_NAME|" go.mod + +go mod tidy + +./reformat.sh + +git commit -m "Rename module" -a From f853bfc5c50c7fffd11eeb79cdd7a454c164d337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 20:04:34 +0800 Subject: [PATCH 129/173] Remove unused --- README.md | 77 -- conn/bind_std_test.go | 250 ------ conn/bindtest/bindtest.go | 136 ---- conn/conn_test.go | 24 - conn/sticky_linux_test.go | 266 ------- device/allowedips_rand_test.go | 141 ---- device/allowedips_test.go | 304 -------- device/bind_test.go | 56 -- device/cookie_test.go | 190 ----- device/device_test.go | 476 ------------ device/endpoint_test.go | 49 -- device/kdf_test.go | 85 --- device/noise_test.go | 179 ----- device/pools_test.go | 141 ---- device/race_disabled_test.go | 10 - device/race_enabled_test.go | 10 - format_test.go | 51 -- go.mod | 6 - go.sum | 6 - ipc/namedpipe/namedpipe_test.go | 674 ---------------- main.go | 268 ------- main_windows.go | 99 --- ratelimiter/ratelimiter_test.go | 119 --- replay/replay_test.go | 119 --- tai64n/tai64n_test.go | 40 - tests/netns.sh | 425 ----------- tun/alignment_windows_test.go | 67 -- tun/checksum_test.go | 98 --- tun/netstack/examples/http_client.go | 54 -- tun/netstack/examples/http_server.go | 51 -- tun/netstack/examples/ping_client.go | 75 -- tun/netstack/tun.go | 1057 -------------------------- tun/offload_linux_test.go | 752 ------------------ tun/tuntest/tuntest.go | 155 ---- 34 files changed, 6510 deletions(-) delete mode 100644 README.md delete mode 100644 conn/bind_std_test.go delete mode 100644 conn/bindtest/bindtest.go delete mode 100644 conn/conn_test.go delete mode 100644 conn/sticky_linux_test.go delete mode 100644 device/allowedips_rand_test.go delete mode 100644 device/allowedips_test.go delete mode 100644 device/bind_test.go delete mode 100644 device/cookie_test.go delete mode 100644 device/device_test.go delete mode 100644 device/endpoint_test.go delete mode 100644 device/kdf_test.go delete mode 100644 device/noise_test.go delete mode 100644 device/pools_test.go delete mode 100644 device/race_disabled_test.go delete mode 100644 device/race_enabled_test.go delete mode 100644 format_test.go delete mode 100644 ipc/namedpipe/namedpipe_test.go delete mode 100644 main.go delete mode 100644 main_windows.go delete mode 100644 ratelimiter/ratelimiter_test.go delete mode 100644 replay/replay_test.go delete mode 100644 tai64n/tai64n_test.go delete mode 100755 tests/netns.sh delete mode 100644 tun/alignment_windows_test.go delete mode 100644 tun/checksum_test.go delete mode 100644 tun/netstack/examples/http_client.go delete mode 100644 tun/netstack/examples/http_server.go delete mode 100644 tun/netstack/examples/ping_client.go delete mode 100644 tun/netstack/tun.go delete mode 100644 tun/offload_linux_test.go delete mode 100644 tun/tuntest/tuntest.go diff --git a/README.md b/README.md deleted file mode 100644 index 709728d..0000000 --- a/README.md +++ /dev/null @@ -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-2025 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. diff --git a/conn/bind_std_test.go b/conn/bind_std_test.go deleted file mode 100644 index 34a3c9a..0000000 --- a/conn/bind_std_test.go +++ /dev/null @@ -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]) - } - } - }) - } -} diff --git a/conn/bindtest/bindtest.go b/conn/bindtest/bindtest.go deleted file mode 100644 index 46e20e6..0000000 --- a/conn/bindtest/bindtest.go +++ /dev/null @@ -1,136 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 -} diff --git a/conn/conn_test.go b/conn/conn_test.go deleted file mode 100644 index 618d02b..0000000 --- a/conn/conn_test.go +++ /dev/null @@ -1,24 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - } - }) -} diff --git a/conn/sticky_linux_test.go b/conn/sticky_linux_test.go deleted file mode 100644 index 1b1ee68..0000000 --- a/conn/sticky_linux_test.go +++ /dev/null @@ -1,266 +0,0 @@ -//go:build linux && !android - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - } - }) -} diff --git a/device/allowedips_rand_test.go b/device/allowedips_rand_test.go deleted file mode 100644 index b863696..0000000 --- a/device/allowedips_rand_test.go +++ /dev/null @@ -1,141 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 - - rng := rand.New(rand.NewSource(1)) - - for n := 0; n < NumberOfPeers; n++ { - peers = append(peers, &Peer{}) - } - - for n := 0; n < NumberOfAddresses; n++ { - var addr4 [4]byte - rng.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 - rng.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 - rng.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 - rng.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") - } -} diff --git a/device/allowedips_test.go b/device/allowedips_test.go deleted file mode 100644 index a4b08a3..0000000 --- a/device/allowedips_test.go +++ /dev/null @@ -1,304 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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, _ int, b *testing.B) { - var trie *trieEntry - var peers []*Peer - root := parentIndirection{&trie, 2} - - rng := rand.New(rand.NewSource(1)) - - const AddressLength = 4 - - for n := 0; n < peerNumber; n++ { - peers = append(peers, &Peer{}) - } - - for n := 0; n < addressNumber; n++ { - var addr [AddressLength]byte - rng.Read(addr[:]) - cidr := uint8(rng.Uint32() % (AddressLength * 8)) - index := rng.Int() % peerNumber - root.insert(addr[:], cidr, peers[index]) - } - - for n := 0; n < b.N; n++ { - var addr [AddressLength]byte - rng.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) - } - - remove := func(peer *Peer, a, b, c, d byte, cidr uint8) { - allowedIPs.Remove(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) - - insert(a, 1, 0, 0, 0, 32) - insert(a, 192, 0, 0, 0, 24) - assertEQ(a, 1, 0, 0, 0) - assertEQ(a, 192, 0, 0, 1) - remove(a, 192, 0, 0, 0, 32) - assertEQ(a, 192, 0, 0, 1) - remove(nil, 192, 0, 0, 0, 24) - assertEQ(a, 192, 0, 0, 1) - remove(b, 192, 0, 0, 0, 24) - assertEQ(a, 192, 0, 0, 1) - remove(a, 192, 0, 0, 0, 24) - assertNEQ(a, 192, 0, 0, 1) - remove(a, 1, 0, 0, 0, 32) - assertNEQ(a, 1, 0, 0, 0) -} - -/* 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) - } - - remove := 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.Remove(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") - } - } - - assertNEQ := 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 NEQ 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) - - insert(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) - insert(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) - assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) - assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) - remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 96) - assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) - remove(nil, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) - assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) - remove(b, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) - assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) - remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128) - assertNEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef) - remove(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) - assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) - remove(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98) - assertNEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010) -} diff --git a/device/bind_test.go b/device/bind_test.go deleted file mode 100644 index d3fa565..0000000 --- a/device/bind_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 -} diff --git a/device/cookie_test.go b/device/cookie_test.go deleted file mode 100644 index c937290..0000000 --- a/device/cookie_test.go +++ /dev/null @@ -1,190 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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, - }) -} diff --git a/device/device_test.go b/device/device_test.go deleted file mode 100644 index 0091e20..0000000 --- a/device/device_test.go +++ /dev/null @@ -1,476 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - } -} diff --git a/device/endpoint_test.go b/device/endpoint_test.go deleted file mode 100644 index 85482d8..0000000 --- a/device/endpoint_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 -} diff --git a/device/kdf_test.go b/device/kdf_test.go deleted file mode 100644 index 325db59..0000000 --- a/device/kdf_test.go +++ /dev/null @@ -1,85 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - } -} diff --git a/device/noise_test.go b/device/noise_test.go deleted file mode 100644 index f0928ac..0000000 --- a/device/noise_test.go +++ /dev/null @@ -1,179 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - }() -} diff --git a/device/pools_test.go b/device/pools_test.go deleted file mode 100644 index 8381d5a..0000000 --- a/device/pools_test.go +++ /dev/null @@ -1,141 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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() { - p.lock.Lock() - count := p.count - p.lock.Unlock() - 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() -} diff --git a/device/race_disabled_test.go b/device/race_disabled_test.go deleted file mode 100644 index 14b3284..0000000 --- a/device/race_disabled_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !race - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package device - -const raceEnabled = false diff --git a/device/race_enabled_test.go b/device/race_enabled_test.go deleted file mode 100644 index f1ea5cf..0000000 --- a/device/race_enabled_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build race - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package device - -const raceEnabled = true diff --git a/format_test.go b/format_test.go deleted file mode 100644 index 4d02c48..0000000 --- a/format_test.go +++ /dev/null @@ -1,51 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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() -} diff --git a/go.mod b/go.mod index 2a80e00..85766cf 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,4 @@ require ( golang.org/x/net v0.39.0 golang.org/x/sys v0.32.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 - gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c -) - -require ( - github.com/google/btree v1.1.2 // indirect - golang.org/x/time v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 61875c1..c9da2b3 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,8 @@ -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= -gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= diff --git a/ipc/namedpipe/namedpipe_test.go b/ipc/namedpipe/namedpipe_test.go deleted file mode 100644 index 998453b..0000000 --- a/ipc/namedpipe/namedpipe_test.go +++ /dev/null @@ -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() - } -} diff --git a/main.go b/main.go deleted file mode 100644 index b6989e2..0000000 --- a/main.go +++ /dev/null @@ -1,268 +0,0 @@ -//go:build !windows - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 .\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") -} diff --git a/main_windows.go b/main_windows.go deleted file mode 100644 index 67036cf..0000000 --- a/main_windows.go +++ /dev/null @@ -1,99 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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 , 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") -} diff --git a/ratelimiter/ratelimiter_test.go b/ratelimiter/ratelimiter_test.go deleted file mode 100644 index 71140da..0000000 --- a/ratelimiter/ratelimiter_test.go +++ /dev/null @@ -1,119 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 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) - } - } - } -} diff --git a/replay/replay_test.go b/replay/replay_test.go deleted file mode 100644 index 8378ec3..0000000 --- a/replay/replay_test.go +++ /dev/null @@ -1,119 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package replay - -import ( - "testing" -) - -/* Ported from the linux kernel implementation - * - * - */ - -const RejectAfterMessages = 1<<64 - 1<<13 - 1 - -func TestReplay(t *testing.T) { - var filter Filter - - const T_LIM = windowSize + 1 - - testNumber := 0 - T := func(n uint64, expected bool) { - testNumber++ - if filter.ValidateCounter(n, RejectAfterMessages) != expected { - t.Fatal("Test", testNumber, "failed", n, expected) - } - } - - filter.Reset() - - T(0, true) /* 1 */ - T(1, true) /* 2 */ - T(1, false) /* 3 */ - T(9, true) /* 4 */ - T(8, true) /* 5 */ - T(7, true) /* 6 */ - T(7, false) /* 7 */ - T(T_LIM, true) /* 8 */ - T(T_LIM-1, true) /* 9 */ - T(T_LIM-1, false) /* 10 */ - T(T_LIM-2, true) /* 11 */ - T(2, true) /* 12 */ - T(2, false) /* 13 */ - T(T_LIM+16, true) /* 14 */ - T(3, false) /* 15 */ - T(T_LIM+16, false) /* 16 */ - T(T_LIM*4, true) /* 17 */ - T(T_LIM*4-(T_LIM-1), true) /* 18 */ - T(10, false) /* 19 */ - T(T_LIM*4-T_LIM, false) /* 20 */ - T(T_LIM*4-(T_LIM+1), false) /* 21 */ - T(T_LIM*4-(T_LIM-2), true) /* 22 */ - T(T_LIM*4+1-T_LIM, false) /* 23 */ - T(0, false) /* 24 */ - T(RejectAfterMessages, false) /* 25 */ - T(RejectAfterMessages-1, true) /* 26 */ - T(RejectAfterMessages, false) /* 27 */ - T(RejectAfterMessages-1, false) /* 28 */ - T(RejectAfterMessages-2, true) /* 29 */ - T(RejectAfterMessages+1, false) /* 30 */ - T(RejectAfterMessages+2, false) /* 31 */ - T(RejectAfterMessages-2, false) /* 32 */ - T(RejectAfterMessages-3, true) /* 33 */ - T(0, false) /* 34 */ - - t.Log("Bulk test 1") - filter.Reset() - testNumber = 0 - for i := uint64(1); i <= windowSize; i++ { - T(i, true) - } - T(0, true) - T(0, false) - - t.Log("Bulk test 2") - filter.Reset() - testNumber = 0 - for i := uint64(2); i <= windowSize+1; i++ { - T(i, true) - } - T(1, true) - T(0, false) - - t.Log("Bulk test 3") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize + 1); i > 0; i-- { - T(i, true) - } - - t.Log("Bulk test 4") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize + 2); i > 1; i-- { - T(i, true) - } - T(0, false) - - t.Log("Bulk test 5") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize); i > 0; i-- { - T(i, true) - } - T(windowSize+1, true) - T(0, false) - - t.Log("Bulk test 6") - filter.Reset() - testNumber = 0 - for i := uint64(windowSize); i > 0; i-- { - T(i, true) - } - T(0, true) - T(windowSize+1, true) -} diff --git a/tai64n/tai64n_test.go b/tai64n/tai64n_test.go deleted file mode 100644 index d0b4425..0000000 --- a/tai64n/tai64n_test.go +++ /dev/null @@ -1,40 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package tai64n - -import ( - "testing" - "time" -) - -// Test that timestamps are monotonic as required by Wireguard and that -// nanosecond-level information is whitened to prevent side channel attacks. -func TestMonotonic(t *testing.T) { - startTime := time.Unix(0, 123456789) // a nontrivial bit pattern - // Whitening should reduce timestamp granularity - // to more than 10 but fewer than 20 milliseconds. - tests := []struct { - name string - t1, t2 time.Time - wantAfter bool - }{ - {"after_10_ns", startTime, startTime.Add(10 * time.Nanosecond), false}, - {"after_10_us", startTime, startTime.Add(10 * time.Microsecond), false}, - {"after_1_ms", startTime, startTime.Add(time.Millisecond), false}, - {"after_10_ms", startTime, startTime.Add(10 * time.Millisecond), false}, - {"after_20_ms", startTime, startTime.Add(20 * time.Millisecond), true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ts1, ts2 := stamp(tt.t1), stamp(tt.t2) - got := ts2.After(ts1) - if got != tt.wantAfter { - t.Errorf("after = %v; want %v", got, tt.wantAfter) - } - }) - } -} diff --git a/tests/netns.sh b/tests/netns.sh deleted file mode 100755 index 2f2a2cd..0000000 --- a/tests/netns.sh +++ /dev/null @@ -1,425 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2015-2017 Jason A. Donenfeld . All Rights Reserved. - -# This script tests the below topology: -# -# ┌─────────────────────┐ ┌──────────────────────────────────┐ ┌─────────────────────┐ -# │ $ns1 namespace │ │ $ns0 namespace │ │ $ns2 namespace │ -# │ │ │ │ │ │ -# │┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐│ -# ││ wg1 │───────────┼───┼────────────│ lo │────────────┼───┼───────────│ wg2 ││ -# │├────────┴──────────┐│ │ ┌───────┴────────┴────────┐ │ │┌──────────┴────────┤│ -# ││192.168.241.1/24 ││ │ │(ns1) (ns2) │ │ ││192.168.241.2/24 ││ -# ││fd00::1/24 ││ │ │127.0.0.1:1 127.0.0.1:2│ │ ││fd00::2/24 ││ -# │└───────────────────┘│ │ │[::]:1 [::]:2 │ │ │└───────────────────┘│ -# └─────────────────────┘ │ └─────────────────────────┘ │ └─────────────────────┘ -# └──────────────────────────────────┘ -# -# After the topology is prepared we run a series of TCP/UDP iperf3 tests between the -# wireguard peers in $ns1 and $ns2. Note that $ns0 is the endpoint for the wg1 -# interfaces in $ns1 and $ns2. See https://www.wireguard.com/netns/ for further -# details on how this is accomplished. - -# This code is ported to the WireGuard-Go directly from the kernel project. -# -# Please ensure that you have installed the newest version of the WireGuard -# tools from the WireGuard project and before running these tests as: -# -# ./netns.sh - -set -e - -exec 3>&1 -export WG_HIDE_KEYS=never -netns0="wg-test-$$-0" -netns1="wg-test-$$-1" -netns2="wg-test-$$-2" -program=$1 -export LOG_LEVEL="verbose" - -pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } -pp() { pretty "" "$*"; "$@"; } -maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } -n0() { pretty 0 "$*"; maybe_exec ip netns exec $netns0 "$@"; } -n1() { pretty 1 "$*"; maybe_exec ip netns exec $netns1 "$@"; } -n2() { pretty 2 "$*"; maybe_exec ip netns exec $netns2 "$@"; } -ip0() { pretty 0 "ip $*"; ip -n $netns0 "$@"; } -ip1() { pretty 1 "ip $*"; ip -n $netns1 "$@"; } -ip2() { pretty 2 "ip $*"; ip -n $netns2 "$@"; } -sleep() { read -t "$1" -N 0 || true; } -waitiperf() { pretty "${1//*-}" "wait for iperf:5201"; while [[ $(ss -N "$1" -tlp 'sport = 5201') != *iperf3* ]]; do sleep 0.1; done; } -waitncatudp() { pretty "${1//*-}" "wait for udp:1111"; while [[ $(ss -N "$1" -ulp 'sport = 1111') != *ncat* ]]; do sleep 0.1; done; } -waitiface() { pretty "${1//*-}" "wait for $2 to come up"; ip netns exec "$1" bash -c "while [[ \$(< \"/sys/class/net/$2/operstate\") != up ]]; do read -t .1 -N 0 || true; done;"; } - -cleanup() { - set +e - exec 2>/dev/null - printf "$orig_message_cost" > /proc/sys/net/core/message_cost - ip0 link del dev wg1 - ip1 link del dev wg1 - ip2 link del dev wg1 - local to_kill="$(ip netns pids $netns0) $(ip netns pids $netns1) $(ip netns pids $netns2)" - [[ -n $to_kill ]] && kill $to_kill - pp ip netns del $netns1 - pp ip netns del $netns2 - pp ip netns del $netns0 - exit -} - -orig_message_cost="$(< /proc/sys/net/core/message_cost)" -trap cleanup EXIT -printf 0 > /proc/sys/net/core/message_cost - -ip netns del $netns0 2>/dev/null || true -ip netns del $netns1 2>/dev/null || true -ip netns del $netns2 2>/dev/null || true -pp ip netns add $netns0 -pp ip netns add $netns1 -pp ip netns add $netns2 -ip0 link set up dev lo - -# ip0 link add dev wg1 type wireguard -n0 $program wg1 -ip0 link set wg1 netns $netns1 - -# ip0 link add dev wg1 type wireguard -n0 $program wg2 -ip0 link set wg2 netns $netns2 - -key1="$(pp wg genkey)" -key2="$(pp wg genkey)" -pub1="$(pp wg pubkey <<<"$key1")" -pub2="$(pp wg pubkey <<<"$key2")" -psk="$(pp wg genpsk)" -[[ -n $key1 && -n $key2 && -n $psk ]] - -configure_peers() { - - ip1 addr add 192.168.241.1/24 dev wg1 - ip1 addr add fd00::1/24 dev wg1 - - ip2 addr add 192.168.241.2/24 dev wg2 - ip2 addr add fd00::2/24 dev wg2 - - n0 wg set wg1 \ - private-key <(echo "$key1") \ - listen-port 10000 \ - peer "$pub2" \ - preshared-key <(echo "$psk") \ - allowed-ips 192.168.241.2/32,fd00::2/128 - n0 wg set wg2 \ - private-key <(echo "$key2") \ - listen-port 20000 \ - peer "$pub1" \ - preshared-key <(echo "$psk") \ - allowed-ips 192.168.241.1/32,fd00::1/128 - - n0 wg showconf wg1 - n0 wg showconf wg2 - - ip1 link set up dev wg1 - ip2 link set up dev wg2 - sleep 1 -} -configure_peers - -tests() { - # Ping over IPv4 - n2 ping -c 10 -f -W 1 192.168.241.1 - n1 ping -c 10 -f -W 1 192.168.241.2 - - # Ping over IPv6 - n2 ping6 -c 10 -f -W 1 fd00::1 - n1 ping6 -c 10 -f -W 1 fd00::2 - - # TCP over IPv4 - n2 iperf3 -s -1 -B 192.168.241.2 & - waitiperf $netns2 - n1 iperf3 -Z -n 1G -c 192.168.241.2 - - # TCP over IPv6 - n1 iperf3 -s -1 -B fd00::1 & - waitiperf $netns1 - n2 iperf3 -Z -n 1G -c fd00::1 - - # UDP over IPv4 - n1 iperf3 -s -1 -B 192.168.241.1 & - waitiperf $netns1 - n2 iperf3 -Z -n 1G -b 0 -u -c 192.168.241.1 - - # UDP over IPv6 - n2 iperf3 -s -1 -B fd00::2 & - waitiperf $netns2 - n1 iperf3 -Z -n 1G -b 0 -u -c fd00::2 -} - -[[ $(ip1 link show dev wg1) =~ mtu\ ([0-9]+) ]] && orig_mtu="${BASH_REMATCH[1]}" -big_mtu=$(( 34816 - 1500 + $orig_mtu )) - -# Test using IPv4 as outer transport -n0 wg set wg1 peer "$pub2" endpoint 127.0.0.1:20000 -n0 wg set wg2 peer "$pub1" endpoint 127.0.0.1:10000 - -# Before calling tests, we first make sure that the stats counters are working -n2 ping -c 10 -f -W 1 192.168.241.1 -{ read _; read _; read _; read rx_bytes _; read _; read tx_bytes _; } < <(ip2 -stats link show dev wg2) -ip2 -stats link show dev wg2 -n0 wg show -[[ $rx_bytes -ge 840 && $tx_bytes -ge 880 && $rx_bytes -lt 2500 && $rx_bytes -lt 2500 ]] -echo "counters working" -tests -ip1 link set wg1 mtu $big_mtu -ip2 link set wg2 mtu $big_mtu -tests - -ip1 link set wg1 mtu $orig_mtu -ip2 link set wg2 mtu $orig_mtu - -# Test using IPv6 as outer transport -n0 wg set wg1 peer "$pub2" endpoint [::1]:20000 -n0 wg set wg2 peer "$pub1" endpoint [::1]:10000 -tests -ip1 link set wg1 mtu $big_mtu -ip2 link set wg2 mtu $big_mtu -tests - -ip1 link set wg1 mtu $orig_mtu -ip2 link set wg2 mtu $orig_mtu - -# Test using IPv4 that roaming works -ip0 -4 addr del 127.0.0.1/8 dev lo -ip0 -4 addr add 127.212.121.99/8 dev lo -n0 wg set wg1 listen-port 9999 -n0 wg set wg1 peer "$pub2" endpoint 127.0.0.1:20000 -n1 ping6 -W 1 -c 1 fd00::2 -[[ $(n2 wg show wg2 endpoints) == "$pub1 127.212.121.99:9999" ]] - -# Test using IPv6 that roaming works -n1 wg set wg1 listen-port 9998 -n1 wg set wg1 peer "$pub2" endpoint [::1]:20000 -n1 ping -W 1 -c 1 192.168.241.2 -[[ $(n2 wg show wg2 endpoints) == "$pub1 [::1]:9998" ]] - -# Test that crypto-RP filter works -n1 wg set wg1 peer "$pub2" allowed-ips 192.168.241.0/24 -exec 4< <(n1 ncat -l -u -p 1111) -nmap_pid=$! -waitncatudp $netns1 -n2 ncat -u 192.168.241.1 1111 <<<"X" -read -r -N 1 -t 1 out <&4 && [[ $out == "X" ]] -kill $nmap_pid -more_specific_key="$(pp wg genkey | pp wg pubkey)" -n0 wg set wg1 peer "$more_specific_key" allowed-ips 192.168.241.2/32 -n0 wg set wg2 listen-port 9997 -exec 4< <(n1 ncat -l -u -p 1111) -nmap_pid=$! -waitncatudp $netns1 -n2 ncat -u 192.168.241.1 1111 <<<"X" -! read -r -N 1 -t 1 out <&4 -kill $nmap_pid -n0 wg set wg1 peer "$more_specific_key" remove -[[ $(n1 wg show wg1 endpoints) == "$pub2 [::1]:9997" ]] - -ip1 link del wg1 -ip2 link del wg2 - -# Test using NAT. We now change the topology to this: -# ┌────────────────────────────────────────┐ ┌────────────────────────────────────────────────┐ ┌────────────────────────────────────────┐ -# │ $ns1 namespace │ │ $ns0 namespace │ │ $ns2 namespace │ -# │ │ │ │ │ │ -# │ ┌─────┐ ┌─────┐ │ │ ┌──────┐ ┌──────┐ │ │ ┌─────┐ ┌─────┐ │ -# │ │ wg1 │─────────────│vethc│───────────┼────┼────│vethrc│ │vethrs│──────────────┼─────┼──│veths│────────────│ wg2 │ │ -# │ ├─────┴──────────┐ ├─────┴──────────┐│ │ ├──────┴─────────┐ ├──────┴────────────┐ │ │ ├─────┴──────────┐ ├─────┴──────────┐ │ -# │ │192.168.241.1/24│ │192.168.1.100/24││ │ │192.168.1.100/24│ │10.0.0.1/24 │ │ │ │10.0.0.100/24 │ │192.168.241.2/24│ │ -# │ │fd00::1/24 │ │ ││ │ │ │ │SNAT:192.168.1.0/24│ │ │ │ │ │fd00::2/24 │ │ -# │ └────────────────┘ └────────────────┘│ │ └────────────────┘ └───────────────────┘ │ │ └────────────────┘ └────────────────┘ │ -# └────────────────────────────────────────┘ └────────────────────────────────────────────────┘ └────────────────────────────────────────┘ - -# ip1 link add dev wg1 type wireguard -# ip2 link add dev wg1 type wireguard - -n1 $program wg1 -n2 $program wg2 - -configure_peers - -ip0 link add vethrc type veth peer name vethc -ip0 link add vethrs type veth peer name veths -ip0 link set vethc netns $netns1 -ip0 link set veths netns $netns2 -ip0 link set vethrc up -ip0 link set vethrs up -ip0 addr add 192.168.1.1/24 dev vethrc -ip0 addr add 10.0.0.1/24 dev vethrs -ip1 addr add 192.168.1.100/24 dev vethc -ip1 link set vethc up -ip1 route add default via 192.168.1.1 -ip2 addr add 10.0.0.100/24 dev veths -ip2 link set veths up -waitiface $netns0 vethrc -waitiface $netns0 vethrs -waitiface $netns1 vethc -waitiface $netns2 veths - -n0 bash -c 'printf 1 > /proc/sys/net/ipv4/ip_forward' -n0 bash -c 'printf 2 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout' -n0 bash -c 'printf 2 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream' -n0 iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 10.0.0.0/24 -j SNAT --to 10.0.0.1 - -n0 wg set wg1 peer "$pub2" endpoint 10.0.0.100:20000 persistent-keepalive 1 -n1 ping -W 1 -c 1 192.168.241.2 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n2 wg show wg2 endpoints) == "$pub1 10.0.0.1:10000" ]] -# Demonstrate n2 can still send packets to n1, since persistent-keepalive will prevent connection tracking entry from expiring (to see entries: `n0 conntrack -L`). -pp sleep 3 -n2 ping -W 1 -c 1 192.168.241.1 - -n0 iptables -t nat -F -ip0 link del vethrc -ip0 link del vethrs -ip1 link del wg1 -ip2 link del wg2 - -# Test that saddr routing is sticky but not too sticky, changing to this topology: -# ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ -# │ $ns1 namespace │ │ $ns2 namespace │ -# │ │ │ │ -# │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ -# │ │ wg1 │─────────────│veth1│───────────┼────┼──│veth2│────────────│ wg2 │ │ -# │ ├─────┴──────────┐ ├─────┴──────────┐│ │ ├─────┴──────────┐ ├─────┴──────────┐ │ -# │ │192.168.241.1/24│ │10.0.0.1/24 ││ │ │10.0.0.2/24 │ │192.168.241.2/24│ │ -# │ │fd00::1/24 │ │fd00:aa::1/96 ││ │ │fd00:aa::2/96 │ │fd00::2/24 │ │ -# │ └────────────────┘ └────────────────┘│ │ └────────────────┘ └────────────────┘ │ -# └────────────────────────────────────────┘ └────────────────────────────────────────┘ - -# ip1 link add dev wg1 type wireguard -# ip2 link add dev wg1 type wireguard -n1 $program wg1 -n2 $program wg2 - -configure_peers - -ip1 link add veth1 type veth peer name veth2 -ip1 link set veth2 netns $netns2 -n1 bash -c 'printf 0 > /proc/sys/net/ipv6/conf/veth1/accept_dad' -n2 bash -c 'printf 0 > /proc/sys/net/ipv6/conf/veth2/accept_dad' -n1 bash -c 'printf 1 > /proc/sys/net/ipv4/conf/veth1/promote_secondaries' - -# First we check that we aren't overly sticky and can fall over to new IPs when old ones are removed -ip1 addr add 10.0.0.1/24 dev veth1 -ip1 addr add fd00:aa::1/96 dev veth1 -ip2 addr add 10.0.0.2/24 dev veth2 -ip2 addr add fd00:aa::2/96 dev veth2 -ip1 link set veth1 up -ip2 link set veth2 up -waitiface $netns1 veth1 -waitiface $netns2 veth2 -n0 wg set wg1 peer "$pub2" endpoint 10.0.0.2:20000 -n1 ping -W 1 -c 1 192.168.241.2 -ip1 addr add 10.0.0.10/24 dev veth1 -ip1 addr del 10.0.0.1/24 dev veth1 -n1 ping -W 1 -c 1 192.168.241.2 -n0 wg set wg1 peer "$pub2" endpoint [fd00:aa::2]:20000 -n1 ping -W 1 -c 1 192.168.241.2 -ip1 addr add fd00:aa::10/96 dev veth1 -ip1 addr del fd00:aa::1/96 dev veth1 -n1 ping -W 1 -c 1 192.168.241.2 - -# Now we show that we can successfully do reply to sender routing -ip1 link set veth1 down -ip2 link set veth2 down -ip1 addr flush dev veth1 -ip2 addr flush dev veth2 -ip1 addr add 10.0.0.1/24 dev veth1 -ip1 addr add 10.0.0.2/24 dev veth1 -ip1 addr add fd00:aa::1/96 dev veth1 -ip1 addr add fd00:aa::2/96 dev veth1 -ip2 addr add 10.0.0.3/24 dev veth2 -ip2 addr add fd00:aa::3/96 dev veth2 -ip1 link set veth1 up -ip2 link set veth2 up -waitiface $netns1 veth1 -waitiface $netns2 veth2 -n0 wg set wg2 peer "$pub1" endpoint 10.0.0.1:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 10.0.0.1:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint [fd00:aa::1]:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 [fd00:aa::1]:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint 10.0.0.2:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 10.0.0.2:10000" ]] -n0 wg set wg2 peer "$pub1" endpoint [fd00:aa::2]:10000 -n2 ping -W 1 -c 1 192.168.241.1 -[[ $(n0 wg show wg2 endpoints) == "$pub1 [fd00:aa::2]:10000" ]] - -ip1 link del veth1 -ip1 link del wg1 -ip2 link del wg2 - -# Test that Netlink/IPC is working properly by doing things that usually cause split responses - -n0 $program wg0 -sleep 5 -config=( "[Interface]" "PrivateKey=$(wg genkey)" "[Peer]" "PublicKey=$(wg genkey)" ) -for a in {1..255}; do - for b in {0..255}; do - config+=( "AllowedIPs=$a.$b.0.0/16,$a::$b/128" ) - done -done -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -i=0 -for ip in $(n0 wg show wg0 allowed-ips); do - ((++i)) -done -((i == 255*256*2+1)) -ip0 link del wg0 - -n0 $program wg0 -config=( "[Interface]" "PrivateKey=$(wg genkey)" ) -for a in {1..40}; do - config+=( "[Peer]" "PublicKey=$(wg genkey)" ) - for b in {1..52}; do - config+=( "AllowedIPs=$a.$b.0.0/16" ) - done -done -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -i=0 -while read -r line; do - j=0 - for ip in $line; do - ((++j)) - done - ((j == 53)) - ((++i)) -done < <(n0 wg show wg0 allowed-ips) -((i == 40)) -ip0 link del wg0 - -n0 $program wg0 -config=( ) -for i in {1..29}; do - config+=( "[Peer]" "PublicKey=$(wg genkey)" ) -done -config+=( "[Peer]" "PublicKey=$(wg genkey)" "AllowedIPs=255.2.3.4/32,abcd::255/128" ) -n0 wg setconf wg0 <(printf '%s\n' "${config[@]}") -n0 wg showconf wg0 > /dev/null -ip0 link del wg0 - -! n0 wg show doesnotexist || false - -declare -A objects -while read -t 0.1 -r line 2>/dev/null || [[ $? -ne 142 ]]; do - [[ $line =~ .*(wg[0-9]+:\ [A-Z][a-z]+\ [0-9]+)\ .*(created|destroyed).* ]] || continue - objects["${BASH_REMATCH[1]}"]+="${BASH_REMATCH[2]}" -done < /dev/kmsg -alldeleted=1 -for object in "${!objects[@]}"; do - if [[ ${objects["$object"]} != *createddestroyed ]]; then - echo "Error: $object: merely ${objects["$object"]}" >&3 - alldeleted=0 - fi -done -[[ $alldeleted -eq 1 ]] -pretty "" "Objects that were created were also destroyed." diff --git a/tun/alignment_windows_test.go b/tun/alignment_windows_test.go deleted file mode 100644 index e3252b2..0000000 --- a/tun/alignment_windows_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package tun - -import ( - "reflect" - "testing" - "unsafe" -) - -func checkAlignment(t *testing.T, name string, offset uintptr) { - t.Helper() - if offset%8 != 0 { - t.Errorf("offset of %q within struct is %d bytes, which does not align to 64-bit word boundaries (missing %d bytes). Atomic operations will crash on 32-bit systems.", name, offset, 8-(offset%8)) - } -} - -// TestRateJugglerAlignment checks that atomically-accessed fields are -// aligned to 64-bit boundaries, as required by the atomic package. -// -// Unfortunately, violating this rule on 32-bit platforms results in a -// hard segfault at runtime. -func TestRateJugglerAlignment(t *testing.T) { - var r rateJuggler - - typ := reflect.TypeOf(&r).Elem() - t.Logf("Peer type size: %d, with fields:", typ.Size()) - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - t.Logf("\t%30s\toffset=%3v\t(type size=%3d, align=%d)", - field.Name, - field.Offset, - field.Type.Size(), - field.Type.Align(), - ) - } - - checkAlignment(t, "rateJuggler.current", unsafe.Offsetof(r.current)) - checkAlignment(t, "rateJuggler.nextByteCount", unsafe.Offsetof(r.nextByteCount)) - checkAlignment(t, "rateJuggler.nextStartTime", unsafe.Offsetof(r.nextStartTime)) -} - -// TestNativeTunAlignment checks that atomically-accessed fields are -// aligned to 64-bit boundaries, as required by the atomic package. -// -// Unfortunately, violating this rule on 32-bit platforms results in a -// hard segfault at runtime. -func TestNativeTunAlignment(t *testing.T) { - var tun NativeTun - - typ := reflect.TypeOf(&tun).Elem() - t.Logf("Peer type size: %d, with fields:", typ.Size()) - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - t.Logf("\t%30s\toffset=%3v\t(type size=%3d, align=%d)", - field.Name, - field.Offset, - field.Type.Size(), - field.Type.Align(), - ) - } - - checkAlignment(t, "NativeTun.rate", unsafe.Offsetof(tun.rate)) -} diff --git a/tun/checksum_test.go b/tun/checksum_test.go deleted file mode 100644 index 4ea9b8b..0000000 --- a/tun/checksum_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package tun - -import ( - "encoding/binary" - "fmt" - "math/rand" - "testing" - - "golang.org/x/sys/unix" -) - -func checksumRef(b []byte, initial uint16) uint16 { - ac := uint64(initial) - - for len(b) >= 2 { - ac += uint64(binary.BigEndian.Uint16(b)) - b = b[2:] - } - if len(b) == 1 { - ac += uint64(b[0]) << 8 - } - - for (ac >> 16) > 0 { - ac = (ac >> 16) + (ac & 0xffff) - } - return uint16(ac) -} - -func pseudoHeaderChecksumRefNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { - sum := checksumRef(srcAddr, 0) - sum = checksumRef(dstAddr, sum) - sum = checksumRef([]byte{0, protocol}, sum) - tmp := make([]byte, 2) - binary.BigEndian.PutUint16(tmp, totalLen) - return checksumRef(tmp, sum) -} - -func TestChecksum(t *testing.T) { - for length := 0; length <= 9001; length++ { - buf := make([]byte, length) - rng := rand.New(rand.NewSource(1)) - rng.Read(buf) - csum := checksum(buf, 0x1234) - csumRef := checksumRef(buf, 0x1234) - if csum != csumRef { - t.Error("Expected checksum", csumRef, "got", csum) - } - } -} - -func TestPseudoHeaderChecksum(t *testing.T) { - for _, addrLen := range []int{4, 16} { - for length := 0; length <= 9001; length++ { - srcAddr := make([]byte, addrLen) - dstAddr := make([]byte, addrLen) - buf := make([]byte, length) - rng := rand.New(rand.NewSource(1)) - rng.Read(srcAddr) - rng.Read(dstAddr) - rng.Read(buf) - phSum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) - csum := checksum(buf, phSum) - phSumRef := pseudoHeaderChecksumRefNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length)) - csumRef := checksumRef(buf, phSumRef) - if csum != csumRef { - t.Error("Expected checksumRef", csumRef, "got", csum) - } - } - } -} - -func BenchmarkChecksum(b *testing.B) { - lengths := []int{ - 64, - 128, - 256, - 512, - 1024, - 1500, - 2048, - 4096, - 8192, - 9000, - 9001, - } - - for _, length := range lengths { - b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { - buf := make([]byte, length) - rng := rand.New(rand.NewSource(1)) - rng.Read(buf) - b.ResetTimer() - for i := 0; i < b.N; i++ { - checksum(buf, 0) - } - }) - } -} diff --git a/tun/netstack/examples/http_client.go b/tun/netstack/examples/http_client.go deleted file mode 100644 index d71267d..0000000 --- a/tun/netstack/examples/http_client.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "io" - "log" - "net/http" - "net/netip" - - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.28")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8")}, - 1420) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - err = dev.IpcSet(`private_key=087ec6e14bbed210e7215cdc73468dfa23f080a1bfb8665b2fd809bd99d28379 -public_key=c4c8e984c5322c8184c72265b92b250fdb63688705f504ba003c88f03393cf28 -allowed_ip=0.0.0.0/0 -endpoint=127.0.0.1:58120 -`) - err = dev.Up() - if err != nil { - log.Panic(err) - } - - client := http.Client{ - Transport: &http.Transport{ - DialContext: tnet.DialContext, - }, - } - resp, err := client.Get("http://192.168.4.29/") - if err != nil { - log.Panic(err) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Panic(err) - } - log.Println(string(body)) -} diff --git a/tun/netstack/examples/http_server.go b/tun/netstack/examples/http_server.go deleted file mode 100644 index 7278851..0000000 --- a/tun/netstack/examples/http_server.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "io" - "log" - "net" - "net/http" - "net/netip" - - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.29")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")}, - 1420, - ) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - dev.IpcSet(`private_key=003ed5d73b55806c30de3f8a7bdab38af13539220533055e635690b8b87ad641 -listen_port=58120 -public_key=f928d4f6c1b86c12f2562c10b07c555c5c57fd00f59e90c8d8d88767271cbf7c -allowed_ip=192.168.4.28/32 -persistent_keepalive_interval=25 -`) - dev.Up() - listener, err := tnet.ListenTCP(&net.TCPAddr{Port: 80}) - if err != nil { - log.Panicln(err) - } - http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { - log.Printf("> %s - %s - %s", request.RemoteAddr, request.URL.String(), request.UserAgent()) - io.WriteString(writer, "Hello from userspace TCP!") - }) - err = http.Serve(listener, nil) - if err != nil { - log.Panicln(err) - } -} diff --git a/tun/netstack/examples/ping_client.go b/tun/netstack/examples/ping_client.go deleted file mode 100644 index d1b562f..0000000 --- a/tun/netstack/examples/ping_client.go +++ /dev/null @@ -1,75 +0,0 @@ -//go:build ignore - -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package main - -import ( - "bytes" - "log" - "math/rand" - "net/netip" - "time" - - "golang.org/x/net/icmp" - "golang.org/x/net/ipv4" - - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" -) - -func main() { - tun, tnet, err := netstack.CreateNetTUN( - []netip.Addr{netip.MustParseAddr("192.168.4.29")}, - []netip.Addr{netip.MustParseAddr("8.8.8.8")}, - 1420) - if err != nil { - log.Panic(err) - } - dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) - dev.IpcSet(`private_key=a8dac1d8a70a751f0f699fb14ba1cff7b79cf4fbd8f09f44c6e6a90d0369604f -public_key=25123c5dcd3328ff645e4f2a3fce0d754400d3887a0cb7c56f0267e20fbf3c5b -endpoint=163.172.161.0:12912 -allowed_ip=0.0.0.0/0 -`) - err = dev.Up() - if err != nil { - log.Panic(err) - } - - socket, err := tnet.Dial("ping4", "zx2c4.com") - if err != nil { - log.Panic(err) - } - requestPing := icmp.Echo{ - Seq: rand.Intn(1 << 16), - Data: []byte("gopher burrow"), - } - icmpBytes, _ := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) - socket.SetReadDeadline(time.Now().Add(time.Second * 10)) - start := time.Now() - _, err = socket.Write(icmpBytes) - if err != nil { - log.Panic(err) - } - n, err := socket.Read(icmpBytes[:]) - if err != nil { - log.Panic(err) - } - replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) - if err != nil { - log.Panic(err) - } - replyPing, ok := replyPacket.Body.(*icmp.Echo) - if !ok { - log.Panicf("invalid reply type: %v", replyPacket) - } - if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { - log.Panicf("invalid ping reply: %v", replyPing) - } - log.Printf("Ping latency: %v", time.Since(start)) -} diff --git a/tun/netstack/tun.go b/tun/netstack/tun.go deleted file mode 100644 index a7aec9e..0000000 --- a/tun/netstack/tun.go +++ /dev/null @@ -1,1057 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package netstack - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "net/netip" - "os" - "regexp" - "strconv" - "strings" - "syscall" - "time" - - "golang.zx2c4.com/wireguard/tun" - - "golang.org/x/net/dns/dnsmessage" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/link/channel" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" - "gvisor.dev/gvisor/pkg/tcpip/stack" - "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" - "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" - "gvisor.dev/gvisor/pkg/tcpip/transport/udp" - "gvisor.dev/gvisor/pkg/waiter" -) - -type netTun struct { - ep *channel.Endpoint - stack *stack.Stack - events chan tun.Event - notifyHandle *channel.NotificationHandle - incomingPacket chan *buffer.View - mtu int - dnsServers []netip.Addr - hasV4, hasV6 bool -} - -type Net netTun - -func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { - opts := stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, - TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, - HandleLocal: true, - } - dev := &netTun{ - ep: channel.New(1024, uint32(mtu), ""), - stack: stack.New(opts), - events: make(chan tun.Event, 10), - incomingPacket: make(chan *buffer.View), - dnsServers: dnsServers, - mtu: mtu, - } - sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default - tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) - } - dev.notifyHandle = dev.ep.AddNotify(dev) - tcpipErr = dev.stack.CreateNIC(1, dev.ep) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr) - } - for _, ip := range localAddresses { - var protoNumber tcpip.NetworkProtocolNumber - if ip.Is4() { - protoNumber = ipv4.ProtocolNumber - } else if ip.Is6() { - protoNumber = ipv6.ProtocolNumber - } - protoAddr := tcpip.ProtocolAddress{ - Protocol: protoNumber, - AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), - } - tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) - if tcpipErr != nil { - return nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr) - } - if ip.Is4() { - dev.hasV4 = true - } else if ip.Is6() { - dev.hasV6 = true - } - } - if dev.hasV4 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) - } - if dev.hasV6 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) - } - - dev.events <- tun.EventUp - return dev, (*Net)(dev), nil -} - -func (tun *netTun) Name() (string, error) { - return "go", nil -} - -func (tun *netTun) File() *os.File { - return nil -} - -func (tun *netTun) Events() <-chan tun.Event { - return tun.events -} - -func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { - view, ok := <-tun.incomingPacket - if !ok { - return 0, os.ErrClosed - } - - n, err := view.Read(buf[0][offset:]) - if err != nil { - return 0, err - } - sizes[0] = n - return 1, nil -} - -func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { - for _, buf := range buf { - packet := buf[offset:] - if len(packet) == 0 { - continue - } - - pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) - switch packet[0] >> 4 { - case 4: - tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) - case 6: - tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb) - default: - return 0, syscall.EAFNOSUPPORT - } - } - return len(buf), nil -} - -func (tun *netTun) WriteNotify() { - pkt := tun.ep.Read() - if pkt == nil { - return - } - - view := pkt.ToView() - pkt.DecRef() - - tun.incomingPacket <- view -} - -func (tun *netTun) Close() error { - tun.stack.RemoveNIC(1) - tun.stack.Close() - tun.ep.RemoveNotify(tun.notifyHandle) - tun.ep.Close() - - if tun.events != nil { - close(tun.events) - } - - if tun.incomingPacket != nil { - close(tun.incomingPacket) - } - - return nil -} - -func (tun *netTun) MTU() (int, error) { - return tun.mtu, nil -} - -func (tun *netTun) BatchSize() int { - return 1 -} - -func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { - var protoNumber tcpip.NetworkProtocolNumber - if endpoint.Addr().Is4() { - protoNumber = ipv4.ProtocolNumber - } else { - protoNumber = ipv6.ProtocolNumber - } - return tcpip.FullAddress{ - NIC: 1, - Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), - Port: endpoint.Port(), - }, protoNumber -} - -func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) { - fa, pn := convertToFullAddr(addr) - return gonet.DialContextTCP(ctx, net.stack, fa, pn) -} - -func (net *Net) DialContextTCP(ctx context.Context, addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialContextTCPAddrPort(ctx, netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) DialTCPAddrPort(addr netip.AddrPort) (*gonet.TCPConn, error) { - fa, pn := convertToFullAddr(addr) - return gonet.DialTCP(net.stack, fa, pn) -} - -func (net *Net) DialTCP(addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialTCPAddrPort(netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) ListenTCPAddrPort(addr netip.AddrPort) (*gonet.TCPListener, error) { - fa, pn := convertToFullAddr(addr) - return gonet.ListenTCP(net.stack, fa, pn) -} - -func (net *Net) ListenTCP(addr *net.TCPAddr) (*gonet.TCPListener, error) { - if addr == nil { - return net.ListenTCPAddrPort(netip.AddrPort{}) - } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.ListenTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) -} - -func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) { - var lfa, rfa *tcpip.FullAddress - var pn tcpip.NetworkProtocolNumber - if laddr.IsValid() || laddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(laddr) - lfa = &addr - } - if raddr.IsValid() || raddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(raddr) - rfa = &addr - } - return gonet.DialUDP(net.stack, lfa, rfa, pn) -} - -func (net *Net) ListenUDPAddrPort(laddr netip.AddrPort) (*gonet.UDPConn, error) { - return net.DialUDPAddrPort(laddr, netip.AddrPort{}) -} - -func (net *Net) DialUDP(laddr, raddr *net.UDPAddr) (*gonet.UDPConn, error) { - var la, ra netip.AddrPort - if laddr != nil { - ip, _ := netip.AddrFromSlice(laddr.IP) - la = netip.AddrPortFrom(ip, uint16(laddr.Port)) - } - if raddr != nil { - ip, _ := netip.AddrFromSlice(raddr.IP) - ra = netip.AddrPortFrom(ip, uint16(raddr.Port)) - } - return net.DialUDPAddrPort(la, ra) -} - -func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) { - return net.DialUDP(laddr, nil) -} - -type PingConn struct { - laddr PingAddr - raddr PingAddr - wq waiter.Queue - ep tcpip.Endpoint - deadline *time.Timer -} - -type PingAddr struct{ addr netip.Addr } - -func (ia PingAddr) String() string { - return ia.addr.String() -} - -func (ia PingAddr) Network() string { - if ia.addr.Is4() { - return "ping4" - } else if ia.addr.Is6() { - return "ping6" - } - return "ping" -} - -func (ia PingAddr) Addr() netip.Addr { - return ia.addr -} - -func PingAddrFromAddr(addr netip.Addr) *PingAddr { - return &PingAddr{addr} -} - -func (net *Net) DialPingAddr(laddr, raddr netip.Addr) (*PingConn, error) { - if !laddr.IsValid() && !raddr.IsValid() { - return nil, errors.New("ping dial: invalid address") - } - v6 := laddr.Is6() || raddr.Is6() - bind := laddr.IsValid() - if !bind { - if v6 { - laddr = netip.IPv6Unspecified() - } else { - laddr = netip.IPv4Unspecified() - } - } - - tn := icmp.ProtocolNumber4 - pn := ipv4.ProtocolNumber - if v6 { - tn = icmp.ProtocolNumber6 - pn = ipv6.ProtocolNumber - } - - pc := &PingConn{ - laddr: PingAddr{laddr}, - deadline: time.NewTimer(time.Hour << 10), - } - pc.deadline.Stop() - - ep, tcpipErr := net.stack.NewEndpoint(tn, pn, &pc.wq) - if tcpipErr != nil { - return nil, fmt.Errorf("ping socket: endpoint: %s", tcpipErr) - } - pc.ep = ep - - if bind { - fa, _ := convertToFullAddr(netip.AddrPortFrom(laddr, 0)) - if tcpipErr = pc.ep.Bind(fa); tcpipErr != nil { - return nil, fmt.Errorf("ping bind: %s", tcpipErr) - } - } - - if raddr.IsValid() { - pc.raddr = PingAddr{raddr} - fa, _ := convertToFullAddr(netip.AddrPortFrom(raddr, 0)) - if tcpipErr = pc.ep.Connect(fa); tcpipErr != nil { - return nil, fmt.Errorf("ping connect: %s", tcpipErr) - } - } - - return pc, nil -} - -func (net *Net) ListenPingAddr(laddr netip.Addr) (*PingConn, error) { - return net.DialPingAddr(laddr, netip.Addr{}) -} - -func (net *Net) DialPing(laddr, raddr *PingAddr) (*PingConn, error) { - var la, ra netip.Addr - if laddr != nil { - la = laddr.addr - } - if raddr != nil { - ra = raddr.addr - } - return net.DialPingAddr(la, ra) -} - -func (net *Net) ListenPing(laddr *PingAddr) (*PingConn, error) { - var la netip.Addr - if laddr != nil { - la = laddr.addr - } - return net.ListenPingAddr(la) -} - -func (pc *PingConn) LocalAddr() net.Addr { - return pc.laddr -} - -func (pc *PingConn) RemoteAddr() net.Addr { - return pc.raddr -} - -func (pc *PingConn) Close() error { - pc.deadline.Reset(0) - pc.ep.Close() - return nil -} - -func (pc *PingConn) SetWriteDeadline(t time.Time) error { - return errors.New("not implemented") -} - -func (pc *PingConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var na netip.Addr - switch v := addr.(type) { - case *PingAddr: - na = v.addr - case *net.IPAddr: - na, _ = netip.AddrFromSlice(v.IP) - default: - return 0, fmt.Errorf("ping write: wrong net.Addr type") - } - if !((na.Is4() && pc.laddr.addr.Is4()) || (na.Is6() && pc.laddr.addr.Is6())) { - return 0, fmt.Errorf("ping write: mismatched protocols") - } - - buf := bytes.NewReader(p) - rfa, _ := convertToFullAddr(netip.AddrPortFrom(na, 0)) - // won't block, no deadlines - n64, tcpipErr := pc.ep.Write(buf, tcpip.WriteOptions{ - To: &rfa, - }) - if tcpipErr != nil { - return int(n64), fmt.Errorf("ping write: %s", tcpipErr) - } - - return int(n64), nil -} - -func (pc *PingConn) Write(p []byte) (n int, err error) { - return pc.WriteTo(p, &pc.raddr) -} - -func (pc *PingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - e, notifyCh := waiter.NewChannelEntry(waiter.EventIn) - pc.wq.EventRegister(&e) - defer pc.wq.EventUnregister(&e) - - select { - case <-pc.deadline.C: - return 0, nil, os.ErrDeadlineExceeded - case <-notifyCh: - } - - w := tcpip.SliceWriter(p) - - res, tcpipErr := pc.ep.Read(&w, tcpip.ReadOptions{ - NeedRemoteAddr: true, - }) - if tcpipErr != nil { - return 0, nil, fmt.Errorf("ping read: %s", tcpipErr) - } - - remoteAddr, _ := netip.AddrFromSlice(res.RemoteAddr.Addr.AsSlice()) - return res.Count, &PingAddr{remoteAddr}, nil -} - -func (pc *PingConn) Read(p []byte) (n int, err error) { - n, _, err = pc.ReadFrom(p) - return -} - -func (pc *PingConn) SetDeadline(t time.Time) error { - // pc.SetWriteDeadline is unimplemented - - return pc.SetReadDeadline(t) -} - -func (pc *PingConn) SetReadDeadline(t time.Time) error { - pc.deadline.Reset(time.Until(t)) - return nil -} - -var ( - errNoSuchHost = errors.New("no such host") - errLameReferral = errors.New("lame referral") - errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message") - errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message") - errServerMisbehaving = errors.New("server misbehaving") - errInvalidDNSResponse = errors.New("invalid DNS response") - errNoAnswerFromDNSServer = errors.New("no answer from DNS server") - errServerTemporarilyMisbehaving = errors.New("server misbehaving") - errCanceled = errors.New("operation was canceled") - errTimeout = errors.New("i/o timeout") - errNumericPort = errors.New("port must be numeric") - errNoSuitableAddress = errors.New("no suitable address found") - errMissingAddress = errors.New("missing address") -) - -func (net *Net) LookupHost(host string) (addrs []string, err error) { - return net.LookupContextHost(context.Background(), host) -} - -func isDomainName(s string) bool { - l := len(s) - if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { - return false - } - last := byte('.') - nonNumeric := false - partlen := 0 - for i := 0; i < len(s); i++ { - c := s[i] - switch { - default: - return false - case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': - nonNumeric = true - partlen++ - case '0' <= c && c <= '9': - partlen++ - case c == '-': - if last == '.' { - return false - } - partlen++ - nonNumeric = true - case c == '.': - if last == '.' || last == '-' { - return false - } - if partlen > 63 || partlen == 0 { - return false - } - partlen = 0 - } - last = c - } - if last == '-' || partlen > 63 { - return false - } - return nonNumeric -} - -func randU16() uint16 { - var b [2]byte - _, err := rand.Read(b[:]) - if err != nil { - panic(err) - } - return binary.LittleEndian.Uint16(b[:]) -} - -func newRequest(q dnsmessage.Question) (id uint16, udpReq, tcpReq []byte, err error) { - id = randU16() - b := dnsmessage.NewBuilder(make([]byte, 2, 514), dnsmessage.Header{ID: id, RecursionDesired: true}) - b.EnableCompression() - if err := b.StartQuestions(); err != nil { - return 0, nil, nil, err - } - if err := b.Question(q); err != nil { - return 0, nil, nil, err - } - tcpReq, err = b.Finish() - udpReq = tcpReq[2:] - l := len(tcpReq) - 2 - tcpReq[0] = byte(l >> 8) - tcpReq[1] = byte(l) - return id, udpReq, tcpReq, err -} - -func equalASCIIName(x, y dnsmessage.Name) bool { - if x.Length != y.Length { - return false - } - for i := 0; i < int(x.Length); i++ { - a := x.Data[i] - b := y.Data[i] - if 'A' <= a && a <= 'Z' { - a += 0x20 - } - if 'A' <= b && b <= 'Z' { - b += 0x20 - } - if a != b { - return false - } - } - return true -} - -func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQues dnsmessage.Question) bool { - if !respHdr.Response { - return false - } - if reqID != respHdr.ID { - return false - } - if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) { - return false - } - return true -} - -func dnsPacketRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { - if _, err := c.Write(b); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - b = make([]byte, 512) - for { - n, err := c.Read(b) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - var p dnsmessage.Parser - h, err := p.Start(b[:n]) - if err != nil { - continue - } - q, err := p.Question() - if err != nil || !checkResponse(id, query, h, q) { - continue - } - return p, h, nil - } -} - -func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { - if _, err := c.Write(b); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - b = make([]byte, 1280) - if _, err := io.ReadFull(c, b[:2]); err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - l := int(b[0])<<8 | int(b[1]) - if l > len(b) { - b = make([]byte, l) - } - n, err := io.ReadFull(c, b[:l]) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - var p dnsmessage.Parser - h, err := p.Start(b[:n]) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage - } - q, err := p.Question() - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage - } - if !checkResponse(id, query, h, q) { - return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse - } - return p, h, nil -} - -func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { - q.Class = dnsmessage.ClassINET - id, udpReq, tcpReq, err := newRequest(q) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotMarshalDNSMessage - } - - for _, useUDP := range []bool{true, false} { - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) - defer cancel() - - var c net.Conn - var err error - if useUDP { - c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, netip.AddrPortFrom(server, 53)) - } else { - c, err = tnet.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(server, 53)) - } - - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - if d, ok := ctx.Deadline(); ok && !d.IsZero() { - err := c.SetDeadline(d) - if err != nil { - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - } - var p dnsmessage.Parser - var h dnsmessage.Header - if useUDP { - p, h, err = dnsPacketRoundTrip(c, id, q, udpReq) - } else { - p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq) - } - c.Close() - if err != nil { - if err == context.Canceled { - err = errCanceled - } else if err == context.DeadlineExceeded { - err = errTimeout - } - return dnsmessage.Parser{}, dnsmessage.Header{}, err - } - if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone { - return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse - } - if h.Truncated { - continue - } - return p, h, nil - } - return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer -} - -func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error { - if h.RCode == dnsmessage.RCodeNameError { - return errNoSuchHost - } - _, err := p.AnswerHeader() - if err != nil && err != dnsmessage.ErrSectionDone { - return errCannotUnmarshalDNSMessage - } - if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone { - return errLameReferral - } - if h.RCode != dnsmessage.RCodeSuccess && h.RCode != dnsmessage.RCodeNameError { - if h.RCode == dnsmessage.RCodeServerFailure { - return errServerTemporarilyMisbehaving - } - return errServerMisbehaving - } - return nil -} - -func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error { - for { - h, err := p.AnswerHeader() - if err == dnsmessage.ErrSectionDone { - return errNoSuchHost - } - if err != nil { - return errCannotUnmarshalDNSMessage - } - if h.Type == qtype { - return nil - } - if err := p.SkipAnswer(); err != nil { - return errCannotUnmarshalDNSMessage - } - } -} - -func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { - var lastErr error - - n, err := dnsmessage.NewName(name) - if err != nil { - return dnsmessage.Parser{}, "", errCannotMarshalDNSMessage - } - q := dnsmessage.Question{ - Name: n, - Type: qtype, - Class: dnsmessage.ClassINET, - } - - for i := 0; i < 2; i++ { - for _, server := range tnet.dnsServers { - p, h, err := tnet.exchange(ctx, server, q, time.Second*5) - if err != nil { - dnsErr := &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - dnsErr.IsTimeout = true - } - if _, ok := err.(*net.OpError); ok { - dnsErr.IsTemporary = true - } - lastErr = dnsErr - continue - } - - if err := checkHeader(&p, h); err != nil { - dnsErr := &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if err == errServerTemporarilyMisbehaving { - dnsErr.IsTemporary = true - } - if err == errNoSuchHost { - dnsErr.IsNotFound = true - return p, server.String(), dnsErr - } - lastErr = dnsErr - continue - } - - err = skipToAnswer(&p, qtype) - if err == nil { - return p, server.String(), nil - } - lastErr = &net.DNSError{ - Err: err.Error(), - Name: name, - Server: server.String(), - } - if err == errNoSuchHost { - lastErr.(*net.DNSError).IsNotFound = true - return p, server.String(), lastErr - } - } - } - return dnsmessage.Parser{}, "", lastErr -} - -func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { - if host == "" || (!tnet.hasV6 && !tnet.hasV4) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} - } - zlen := len(host) - if strings.IndexByte(host, ':') != -1 { - if zidx := strings.LastIndexByte(host, '%'); zidx != -1 { - zlen = zidx - } - } - if ip, err := netip.ParseAddr(host[:zlen]); err == nil { - return []string{ip.String()}, nil - } - - if !isDomainName(host) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} - } - type result struct { - p dnsmessage.Parser - server string - error - } - var addrsV4, addrsV6 []netip.Addr - lanes := 0 - if tnet.hasV4 { - lanes++ - } - if tnet.hasV6 { - lanes++ - } - lane := make(chan result, lanes) - var lastErr error - if tnet.hasV4 { - go func() { - p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeA) - lane <- result{p, server, err} - }() - } - if tnet.hasV6 { - go func() { - p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeAAAA) - lane <- result{p, server, err} - }() - } - for l := 0; l < lanes; l++ { - result := <-lane - if result.error != nil { - if lastErr == nil { - lastErr = result.error - } - continue - } - - loop: - for { - h, err := result.p.AnswerHeader() - if err != nil && err != dnsmessage.ErrSectionDone { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - } - if err != nil { - break - } - switch h.Type { - case dnsmessage.TypeA: - a, err := result.p.AResource() - if err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - addrsV4 = append(addrsV4, netip.AddrFrom4(a.A)) - - case dnsmessage.TypeAAAA: - aaaa, err := result.p.AAAAResource() - if err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA)) - - default: - if err := result.p.SkipAnswer(); err != nil { - lastErr = &net.DNSError{ - Err: errCannotMarshalDNSMessage.Error(), - Name: host, - Server: result.server, - } - break loop - } - continue - } - } - } - // We don't do RFC6724. Instead just put V6 addresses first if an IPv6 address is enabled - var addrs []netip.Addr - if tnet.hasV6 { - addrs = append(addrsV6, addrsV4...) - } else { - addrs = append(addrsV4, addrsV6...) - } - - if len(addrs) == 0 && lastErr != nil { - return nil, lastErr - } - saddrs := make([]string, 0, len(addrs)) - for _, ip := range addrs { - saddrs = append(saddrs, ip.String()) - } - return saddrs, nil -} - -func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) { - if deadline.IsZero() { - return deadline, nil - } - timeRemaining := deadline.Sub(now) - if timeRemaining <= 0 { - return time.Time{}, errTimeout - } - timeout := timeRemaining / time.Duration(addrsRemaining) - const saneMinimum = 2 * time.Second - if timeout < saneMinimum { - if timeRemaining < saneMinimum { - timeout = timeRemaining - } else { - timeout = saneMinimum - } - } - return now.Add(timeout), nil -} - -var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`) - -func (tnet *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if ctx == nil { - panic("nil context") - } - var acceptV4, acceptV6 bool - matches := protoSplitter.FindStringSubmatch(network) - if matches == nil { - return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)} - } else if len(matches[2]) == 0 { - acceptV4 = true - acceptV6 = true - } else { - acceptV4 = matches[2][0] == '4' - acceptV6 = !acceptV4 - } - var host string - var port int - if matches[1] == "ping" { - host = address - } else { - var sport string - var err error - host, sport, err = net.SplitHostPort(address) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - port, err = strconv.Atoi(sport) - if err != nil || port < 0 || port > 65535 { - return nil, &net.OpError{Op: "dial", Err: errNumericPort} - } - } - allAddr, err := tnet.LookupContextHost(ctx, host) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - var addrs []netip.AddrPort - for _, addr := range allAddr { - ip, err := netip.ParseAddr(addr) - if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) { - addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port))) - } - } - if len(addrs) == 0 && len(allAddr) != 0 { - return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress} - } - - var firstErr error - for i, addr := range addrs { - select { - case <-ctx.Done(): - err := ctx.Err() - if err == context.Canceled { - err = errCanceled - } else if err == context.DeadlineExceeded { - err = errTimeout - } - return nil, &net.OpError{Op: "dial", Err: err} - default: - } - - dialCtx := ctx - if deadline, hasDeadline := ctx.Deadline(); hasDeadline { - partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i) - if err != nil { - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: err} - } - break - } - if partialDeadline.Before(deadline) { - var cancel context.CancelFunc - dialCtx, cancel = context.WithDeadline(ctx, partialDeadline) - defer cancel() - } - } - - var c net.Conn - switch matches[1] { - case "tcp": - c, err = tnet.DialContextTCPAddrPort(dialCtx, addr) - case "udp": - c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, addr) - case "ping": - c, err = tnet.DialPingAddr(netip.Addr{}, addr.Addr()) - } - if err == nil { - return c, nil - } - if firstErr == nil { - firstErr = err - } - } - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: errMissingAddress} - } - return nil, firstErr -} - -func (tnet *Net) Dial(network, address string) (net.Conn, error) { - return tnet.DialContext(context.Background(), network, address) -} diff --git a/tun/offload_linux_test.go b/tun/offload_linux_test.go deleted file mode 100644 index d87e636..0000000 --- a/tun/offload_linux_test.go +++ /dev/null @@ -1,752 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package tun - -import ( - "net/netip" - "testing" - - "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" -) - -const ( - offset = virtioNetHdrLen -) - -var ( - ip4PortA = netip.MustParseAddrPort("192.0.2.1:1") - ip4PortB = netip.MustParseAddrPort("192.0.2.2:1") - ip4PortC = netip.MustParseAddrPort("192.0.2.3:1") - ip6PortA = netip.MustParseAddrPort("[2001:db8::1]:1") - ip6PortB = netip.MustParseAddrPort("[2001:db8::2]:1") - ip6PortC = netip.MustParseAddrPort("[2001:db8::3]:1") -) - -func udp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv4Fields)) []byte { - totalLen := 28 + payloadLen - b := make([]byte, offset+int(totalLen), 65535) - ipv4H := header.IPv4(b[offset:]) - srcAs4 := srcIPPort.Addr().As4() - dstAs4 := dstIPPort.Addr().As4() - ipFields := &header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), - DstAddr: tcpip.AddrFromSlice(dstAs4[:]), - Protocol: unix.IPPROTO_UDP, - TTL: 64, - TotalLength: uint16(totalLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv4H.Encode(ipFields) - udpH := header.UDP(b[offset+20:]) - udpH.Encode(&header.UDPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - Length: uint16(payloadLen + udphLen), - }) - ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(udphLen+payloadLen)) - udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) - return b -} - -func udp6Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { - return udp6PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) -} - -func udp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, payloadLen int, ipFn func(*header.IPv6Fields)) []byte { - totalLen := 48 + payloadLen - b := make([]byte, offset+int(totalLen), 65535) - ipv6H := header.IPv6(b[offset:]) - srcAs16 := srcIPPort.Addr().As16() - dstAs16 := dstIPPort.Addr().As16() - ipFields := &header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), - DstAddr: tcpip.AddrFromSlice(dstAs16[:]), - TransportProtocol: unix.IPPROTO_UDP, - HopLimit: 64, - PayloadLength: uint16(payloadLen + udphLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv6H.Encode(ipFields) - udpH := header.UDP(b[offset+40:]) - udpH.Encode(&header.UDPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - Length: uint16(payloadLen + udphLen), - }) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_UDP, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(udphLen+payloadLen)) - udpH.SetChecksum(^udpH.CalculateChecksum(pseudoCsum)) - return b -} - -func udp4Packet(srcIPPort, dstIPPort netip.AddrPort, payloadLen int) []byte { - return udp4PacketMutateIPFields(srcIPPort, dstIPPort, payloadLen, nil) -} - -func tcp4PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32, ipFn func(*header.IPv4Fields)) []byte { - totalLen := 40 + segmentSize - b := make([]byte, offset+int(totalLen), 65535) - ipv4H := header.IPv4(b[offset:]) - srcAs4 := srcIPPort.Addr().As4() - dstAs4 := dstIPPort.Addr().As4() - ipFields := &header.IPv4Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs4[:]), - DstAddr: tcpip.AddrFromSlice(dstAs4[:]), - Protocol: unix.IPPROTO_TCP, - TTL: 64, - TotalLength: uint16(totalLen), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv4H.Encode(ipFields) - tcpH := header.TCP(b[offset+20:]) - tcpH.Encode(&header.TCPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - SeqNum: seq, - AckNum: 1, - DataOffset: 20, - Flags: flags, - WindowSize: 3000, - }) - ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(20+segmentSize)) - tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) - return b -} - -func tcp4Packet(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32) []byte { - return tcp4PacketMutateIPFields(srcIPPort, dstIPPort, flags, segmentSize, seq, nil) -} - -func tcp6PacketMutateIPFields(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32, ipFn func(*header.IPv6Fields)) []byte { - totalLen := 60 + segmentSize - b := make([]byte, offset+int(totalLen), 65535) - ipv6H := header.IPv6(b[offset:]) - srcAs16 := srcIPPort.Addr().As16() - dstAs16 := dstIPPort.Addr().As16() - ipFields := &header.IPv6Fields{ - SrcAddr: tcpip.AddrFromSlice(srcAs16[:]), - DstAddr: tcpip.AddrFromSlice(dstAs16[:]), - TransportProtocol: unix.IPPROTO_TCP, - HopLimit: 64, - PayloadLength: uint16(segmentSize + 20), - } - if ipFn != nil { - ipFn(ipFields) - } - ipv6H.Encode(ipFields) - tcpH := header.TCP(b[offset+40:]) - tcpH.Encode(&header.TCPFields{ - SrcPort: srcIPPort.Port(), - DstPort: dstIPPort.Port(), - SeqNum: seq, - AckNum: 1, - DataOffset: 20, - Flags: flags, - WindowSize: 3000, - }) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(20+segmentSize)) - tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) - return b -} - -func tcp6Packet(srcIPPort, dstIPPort netip.AddrPort, flags header.TCPFlags, segmentSize, seq uint32) []byte { - return tcp6PacketMutateIPFields(srcIPPort, dstIPPort, flags, segmentSize, seq, nil) -} - -func Test_handleVirtioRead(t *testing.T) { - tests := []struct { - name string - hdr virtioNetHdr - pktIn []byte - wantLens []int - wantErr bool - }{ - { - "tcp4", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_TCPV4, - gsoSize: 100, - hdrLen: 40, - csumStart: 20, - csumOffset: 16, - }, - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck|header.TCPFlagPsh, 200, 1), - []int{140, 140}, - false, - }, - { - "tcp6", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_TCPV6, - gsoSize: 100, - hdrLen: 60, - csumStart: 40, - csumOffset: 16, - }, - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck|header.TCPFlagPsh, 200, 1), - []int{160, 160}, - false, - }, - { - "udp4", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - gsoSize: 100, - hdrLen: 28, - csumStart: 20, - csumOffset: 6, - }, - udp4Packet(ip4PortA, ip4PortB, 200), - []int{128, 128}, - false, - }, - { - "udp6", - virtioNetHdr{ - flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - gsoSize: 100, - hdrLen: 48, - csumStart: 40, - csumOffset: 6, - }, - udp6Packet(ip6PortA, ip6PortB, 200), - []int{148, 148}, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - out := make([][]byte, conn.IdealBatchSize) - sizes := make([]int, conn.IdealBatchSize) - for i := range out { - out[i] = make([]byte, 65535) - } - tt.hdr.encode(tt.pktIn) - n, err := handleVirtioRead(tt.pktIn, out, sizes, offset) - if err != nil { - if tt.wantErr { - return - } - t.Fatalf("got err: %v", err) - } - if n != len(tt.wantLens) { - t.Fatalf("got %d packets, wanted %d", n, len(tt.wantLens)) - } - for i := range tt.wantLens { - if tt.wantLens[i] != sizes[i] { - t.Fatalf("wantLens[%d]: %d != outSizes: %d", i, tt.wantLens[i], sizes[i]) - } - } - }) - } -} - -func flipTCP4Checksum(b []byte) []byte { - at := virtioNetHdrLen + 20 + 16 // 20 byte ipv4 header; tcp csum offset is 16 - b[at] ^= 0xFF - b[at+1] ^= 0xFF - return b -} - -func flipUDP4Checksum(b []byte) []byte { - at := virtioNetHdrLen + 20 + 6 // 20 byte ipv4 header; udp csum offset is 6 - b[at] ^= 0xFF - b[at+1] ^= 0xFF - return b -} - -func Fuzz_handleGRO(f *testing.F) { - pkt0 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1) - pkt1 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101) - pkt2 := tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201) - pkt3 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1) - pkt4 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101) - pkt5 := tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201) - pkt6 := udp4Packet(ip4PortA, ip4PortB, 100) - pkt7 := udp4Packet(ip4PortA, ip4PortB, 100) - pkt8 := udp4Packet(ip4PortA, ip4PortC, 100) - pkt9 := udp6Packet(ip6PortA, ip6PortB, 100) - pkt10 := udp6Packet(ip6PortA, ip6PortB, 100) - pkt11 := udp6Packet(ip6PortA, ip6PortC, 100) - f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, true, offset) - f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, canUDPGRO bool, offset int) { - pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11} - toWrite := make([]int, 0, len(pkts)) - handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), canUDPGRO, &toWrite) - if len(toWrite) > len(pkts) { - t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts)) - } - seenWriteI := make(map[int]bool) - for _, writeI := range toWrite { - if writeI < 0 || writeI > len(pkts)-1 { - t.Errorf("toWrite value (%d) outside bounds of len(pkts): %d", writeI, len(pkts)) - } - if seenWriteI[writeI] { - t.Errorf("duplicate toWrite value: %d", writeI) - } - seenWriteI[writeI] = true - } - }) -} - -func Test_handleGRO(t *testing.T) { - tests := []struct { - name string - pktsIn [][]byte - canUDPGRO bool - wantToWrite []int - wantLens []int - wantErr bool - }{ - { - "multiple protocols and flows", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 - tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - }, - true, - []int{0, 1, 2, 4, 5, 7, 9}, - []int{240, 228, 128, 140, 260, 160, 248}, - false, - }, - { - "multiple protocols and flows no UDP GRO", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // tcp4 flow 1 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp4Packet(ip4PortA, ip4PortC, 100), // udp4 flow 2 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // tcp4 flow 1 - tcp4Packet(ip4PortA, ip4PortC, header.TCPFlagAck, 100, 201), // tcp4 flow 2 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101), // tcp6 flow 1 - tcp6Packet(ip6PortA, ip6PortC, header.TCPFlagAck, 100, 201), // tcp6 flow 2 - udp4Packet(ip4PortA, ip4PortB, 100), // udp4 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1 - }, - false, - []int{0, 1, 2, 4, 5, 7, 8, 9, 10}, - []int{240, 128, 128, 140, 260, 160, 128, 148, 148}, - false, - }, - { - "PSH interleaved", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck|header.TCPFlagPsh, 100, 101), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 301), // v4 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck|header.TCPFlagPsh, 100, 101), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 201), // v6 flow 1 - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 301), // v6 flow 1 - }, - true, - []int{0, 2, 4, 6}, - []int{240, 240, 260, 260}, - false, - }, - { - "coalesceItemInvalidCSum", - [][]byte{ - flipTCP4Checksum(tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)), // v4 flow 1 seq 1 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 seq 101 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 - flipUDP4Checksum(udp4Packet(ip4PortA, ip4PortB, 100)), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4Packet(ip4PortA, ip4PortB, 100), - }, - true, - []int{0, 1, 3, 4}, - []int{140, 240, 128, 228}, - false, - }, - { - "out of order", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101), // v4 flow 1 seq 101 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 seq 1 len 100 - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100 - }, - true, - []int{0}, - []int{340}, - false, - }, - { - "unequal TTL", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.TTL++ - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.TTL++ - }), - }, - true, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal ToS", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.TOS++ - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.TOS++ - }), - }, - true, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal flags more fragments set", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.Flags = 1 - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.Flags = 1 - }), - }, - true, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "unequal flags DF set", - [][]byte{ - tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), - tcp4PacketMutateIPFields(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv4Fields) { - fields.Flags = 2 - }), - udp4Packet(ip4PortA, ip4PortB, 100), - udp4PacketMutateIPFields(ip4PortA, ip4PortB, 100, func(fields *header.IPv4Fields) { - fields.Flags = 2 - }), - }, - true, - []int{0, 1, 2, 3}, - []int{140, 140, 128, 128}, - false, - }, - { - "ipv6 unequal hop limit", - [][]byte{ - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), - tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { - fields.HopLimit++ - }), - udp6Packet(ip6PortA, ip6PortB, 100), - udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { - fields.HopLimit++ - }), - }, - true, - []int{0, 1, 2, 3}, - []int{160, 160, 148, 148}, - false, - }, - { - "ipv6 unequal traffic class", - [][]byte{ - tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1), - tcp6PacketMutateIPFields(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 101, func(fields *header.IPv6Fields) { - fields.TrafficClass++ - }), - udp6Packet(ip6PortA, ip6PortB, 100), - udp6PacketMutateIPFields(ip6PortA, ip6PortB, 100, func(fields *header.IPv6Fields) { - fields.TrafficClass++ - }), - }, - true, - []int{0, 1, 2, 3}, - []int{160, 160, 148, 148}, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - toWrite := make([]int, 0, len(tt.pktsIn)) - err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.canUDPGRO, &toWrite) - if err != nil { - if tt.wantErr { - return - } - t.Fatalf("got err: %v", err) - } - if len(toWrite) != len(tt.wantToWrite) { - t.Fatalf("got %d packets, wanted %d", len(toWrite), len(tt.wantToWrite)) - } - for i, pktI := range tt.wantToWrite { - if tt.wantToWrite[i] != toWrite[i] { - t.Fatalf("wantToWrite[%d]: %d != toWrite: %d", i, tt.wantToWrite[i], toWrite[i]) - } - if tt.wantLens[i] != len(tt.pktsIn[pktI][offset:]) { - t.Errorf("wanted len %d packet at %d, got: %d", tt.wantLens[i], i, len(tt.pktsIn[pktI][offset:])) - } - } - }) - } -} - -func Test_packetIsGROCandidate(t *testing.T) { - tcp4 := tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] - tcp4TooShort := tcp4[:39] - ip4InvalidHeaderLen := make([]byte, len(tcp4)) - copy(ip4InvalidHeaderLen, tcp4) - ip4InvalidHeaderLen[0] = 0x46 - ip4InvalidProtocol := make([]byte, len(tcp4)) - copy(ip4InvalidProtocol, tcp4) - ip4InvalidProtocol[9] = unix.IPPROTO_GRE - - tcp6 := tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 1)[virtioNetHdrLen:] - tcp6TooShort := tcp6[:59] - ip6InvalidProtocol := make([]byte, len(tcp6)) - copy(ip6InvalidProtocol, tcp6) - ip6InvalidProtocol[6] = unix.IPPROTO_GRE - - udp4 := udp4Packet(ip4PortA, ip4PortB, 100)[virtioNetHdrLen:] - udp4TooShort := udp4[:27] - - udp6 := udp6Packet(ip6PortA, ip6PortB, 100)[virtioNetHdrLen:] - udp6TooShort := udp6[:47] - - tests := []struct { - name string - b []byte - canUDPGRO bool - want groCandidateType - }{ - { - "tcp4", - tcp4, - true, - tcp4GROCandidate, - }, - { - "tcp6", - tcp6, - true, - tcp6GROCandidate, - }, - { - "udp4", - udp4, - true, - udp4GROCandidate, - }, - { - "udp4 no support", - udp4, - false, - notGROCandidate, - }, - { - "udp6", - udp6, - true, - udp6GROCandidate, - }, - { - "udp6 no support", - udp6, - false, - notGROCandidate, - }, - { - "udp4 too short", - udp4TooShort, - true, - notGROCandidate, - }, - { - "udp6 too short", - udp6TooShort, - true, - notGROCandidate, - }, - { - "tcp4 too short", - tcp4TooShort, - true, - notGROCandidate, - }, - { - "tcp6 too short", - tcp6TooShort, - true, - notGROCandidate, - }, - { - "invalid IP version", - []byte{0x00}, - true, - notGROCandidate, - }, - { - "invalid IP header len", - ip4InvalidHeaderLen, - true, - notGROCandidate, - }, - { - "ip4 invalid protocol", - ip4InvalidProtocol, - true, - notGROCandidate, - }, - { - "ip6 invalid protocol", - ip6InvalidProtocol, - true, - notGROCandidate, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := packetIsGROCandidate(tt.b, tt.canUDPGRO); got != tt.want { - t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_udpPacketsCanCoalesce(t *testing.T) { - udp4a := udp4Packet(ip4PortA, ip4PortB, 100) - udp4b := udp4Packet(ip4PortA, ip4PortB, 100) - udp4c := udp4Packet(ip4PortA, ip4PortB, 110) - - type args struct { - pkt []byte - iphLen uint8 - gsoSize uint16 - item udpGROItem - bufs [][]byte - bufsOffset int - } - tests := []struct { - name string - args args - want canCoalesce - }{ - { - "coalesceAppend equal gso", - args{ - pkt: udp4a[offset:], - iphLen: 20, - gsoSize: 100, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4b, - }, - bufsOffset: offset, - }, - coalesceAppend, - }, - { - "coalesceAppend smaller gso", - args{ - pkt: udp4a[offset : len(udp4a)-90], - iphLen: 20, - gsoSize: 10, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4b, - }, - bufsOffset: offset, - }, - coalesceAppend, - }, - { - "coalesceUnavailable smaller gso previously appended", - args{ - pkt: udp4a[offset:], - iphLen: 20, - gsoSize: 100, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4c, - udp4b, - }, - bufsOffset: offset, - }, - coalesceUnavailable, - }, - { - "coalesceUnavailable larger following smaller", - args{ - pkt: udp4c[offset:], - iphLen: 20, - gsoSize: 110, - item: udpGROItem{ - gsoSize: 100, - iphLen: 20, - }, - bufs: [][]byte{ - udp4a, - udp4c, - }, - bufsOffset: offset, - }, - coalesceUnavailable, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := udpPacketsCanCoalesce(tt.args.pkt, tt.args.iphLen, tt.args.gsoSize, tt.args.item, tt.args.bufs, tt.args.bufsOffset); got != tt.want { - t.Errorf("udpPacketsCanCoalesce() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/tun/tuntest/tuntest.go b/tun/tuntest/tuntest.go deleted file mode 100644 index 9c4564f..0000000 --- a/tun/tuntest/tuntest.go +++ /dev/null @@ -1,155 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. - */ - -package tuntest - -import ( - "encoding/binary" - "io" - "net/netip" - "os" - - "golang.zx2c4.com/wireguard/tun" -) - -func Ping(dst, src netip.Addr) []byte { - localPort := uint16(1337) - seq := uint16(0) - - payload := make([]byte, 4) - binary.BigEndian.PutUint16(payload[0:], localPort) - binary.BigEndian.PutUint16(payload[2:], seq) - - return genICMPv4(payload, dst, src) -} - -// Checksum is the "internet checksum" from https://tools.ietf.org/html/rfc1071. -func checksum(buf []byte, initial uint16) uint16 { - v := uint32(initial) - for i := 0; i < len(buf)-1; i += 2 { - v += uint32(binary.BigEndian.Uint16(buf[i:])) - } - if len(buf)%2 == 1 { - v += uint32(buf[len(buf)-1]) << 8 - } - for v > 0xffff { - v = (v >> 16) + (v & 0xffff) - } - return ^uint16(v) -} - -func genICMPv4(payload []byte, dst, src netip.Addr) []byte { - const ( - icmpv4ProtocolNumber = 1 - icmpv4Echo = 8 - icmpv4ChecksumOffset = 2 - icmpv4Size = 8 - ipv4Size = 20 - ipv4TotalLenOffset = 2 - ipv4ChecksumOffset = 10 - ttl = 65 - headerSize = ipv4Size + icmpv4Size - ) - - pkt := make([]byte, headerSize+len(payload)) - - ip := pkt[0:ipv4Size] - icmpv4 := pkt[ipv4Size : ipv4Size+icmpv4Size] - - // https://tools.ietf.org/html/rfc792 - icmpv4[0] = icmpv4Echo // type - icmpv4[1] = 0 // code - chksum := ^checksum(icmpv4, checksum(payload, 0)) - binary.BigEndian.PutUint16(icmpv4[icmpv4ChecksumOffset:], chksum) - - // https://tools.ietf.org/html/rfc760 section 3.1 - length := uint16(len(pkt)) - ip[0] = (4 << 4) | (ipv4Size / 4) - binary.BigEndian.PutUint16(ip[ipv4TotalLenOffset:], length) - ip[8] = ttl - ip[9] = icmpv4ProtocolNumber - copy(ip[12:], src.AsSlice()) - copy(ip[16:], dst.AsSlice()) - chksum = ^checksum(ip[:], 0) - binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], chksum) - - copy(pkt[headerSize:], payload) - return pkt -} - -type ChannelTUN struct { - Inbound chan []byte // incoming packets, closed on TUN close - Outbound chan []byte // outbound packets, blocks forever on TUN close - - closed chan struct{} - events chan tun.Event - tun chTun -} - -func NewChannelTUN() *ChannelTUN { - c := &ChannelTUN{ - Inbound: make(chan []byte), - Outbound: make(chan []byte), - closed: make(chan struct{}), - events: make(chan tun.Event, 1), - } - c.tun.c = c - c.events <- tun.EventUp - return c -} - -func (c *ChannelTUN) TUN() tun.Device { - return &c.tun -} - -type chTun struct { - c *ChannelTUN -} - -func (t *chTun) File() *os.File { return nil } - -func (t *chTun) Read(packets [][]byte, sizes []int, offset int) (int, error) { - select { - case <-t.c.closed: - return 0, os.ErrClosed - case msg := <-t.c.Outbound: - n := copy(packets[0][offset:], msg) - sizes[0] = n - return 1, nil - } -} - -// Write is called by the wireguard device to deliver a packet for routing. -func (t *chTun) Write(packets [][]byte, offset int) (int, error) { - if offset == -1 { - close(t.c.closed) - close(t.c.events) - return 0, io.EOF - } - for i, data := range packets { - msg := make([]byte, len(data)-offset) - copy(msg, data[offset:]) - select { - case <-t.c.closed: - return i, os.ErrClosed - case t.c.Inbound <- msg: - } - } - return len(packets), nil -} - -func (t *chTun) BatchSize() int { - return 1 -} - -const DefaultMTU = 1420 - -func (t *chTun) MTU() (int, error) { return DefaultMTU, nil } -func (t *chTun) Name() (string, error) { return "loopbackTun1", nil } -func (t *chTun) Events() <-chan tun.Event { return t.c.events } -func (t *chTun) Close() error { - t.Write(nil, -1) - return nil -} From 75d0f348d59d974bbbc25e74c8d1c15ce8aad28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 20:08:42 +0800 Subject: [PATCH 130/173] Rename module --- conn/bind_std.go | 10 +++------- conn/bind_windows.go | 3 +-- device/device.go | 8 ++++---- device/keypair.go | 2 +- device/noise-protocol.go | 3 +-- device/peer.go | 2 +- device/queueconstants_android.go | 2 +- device/queueconstants_default.go | 2 +- device/receive.go | 3 +-- device/send.go | 4 ++-- device/sticky_default.go | 4 ++-- device/sticky_linux.go | 5 ++--- device/tun.go | 2 +- device/uapi.go | 2 +- go.mod | 2 +- ipc/uapi_linux.go | 3 +-- ipc/uapi_windows.go | 2 +- tun/errors.go | 10 ++++------ tun/offload_linux.go | 2 +- tun/tun_linux.go | 8 +++----- 20 files changed, 33 insertions(+), 46 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index f5c8816..80e8210 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -20,9 +20,7 @@ import ( "golang.org/x/net/ipv6" ) -var ( - _ Bind = (*StdNetBind)(nil) -) +var _ Bind = (*StdNetBind)(nil) // StdNetBind implements Bind for all platforms. While Windows has its own Bind // (see bind_windows.go), it may fall back to StdNetBind. @@ -210,10 +208,8 @@ func (s *StdNetBind) getMessages() *[]ipv6.Message { return s.msgsPool.Get().(*[]ipv6.Message) } -var ( - // If compilation fails here these are no longer the same underlying type. - _ ipv6.Message = ipv4.Message{} -) +// If compilation fails here these are no longer the same underlying type. +var _ ipv6.Message = ipv4.Message{} type batchReader interface { ReadBatch([]ipv6.Message, int) (int, error) diff --git a/conn/bind_windows.go b/conn/bind_windows.go index a3b8460..d166227 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -15,9 +15,8 @@ import ( "sync/atomic" "unsafe" + "github.com/sagernet/wireguard-go/conn/winrio" "golang.org/x/sys/windows" - - "golang.zx2c4.com/wireguard/conn/winrio" ) const ( diff --git a/device/device.go b/device/device.go index 6854ed8..bfc56bb 100644 --- a/device/device.go +++ b/device/device.go @@ -11,10 +11,10 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/ratelimiter" - "golang.zx2c4.com/wireguard/rwcancel" - "golang.zx2c4.com/wireguard/tun" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/ratelimiter" + "github.com/sagernet/wireguard-go/rwcancel" + "github.com/sagernet/wireguard-go/tun" ) type Device struct { diff --git a/device/keypair.go b/device/keypair.go index 0b72e19..0704748 100644 --- a/device/keypair.go +++ b/device/keypair.go @@ -11,7 +11,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/replay" + "github.com/sagernet/wireguard-go/replay" ) /* Due to limitations in Go and /x/crypto there is currently diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 5cf1702..a9b8498 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -12,11 +12,10 @@ import ( "sync" "time" + "github.com/sagernet/wireguard-go/tai64n" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" - - "golang.zx2c4.com/wireguard/tai64n" ) type handshakeState int diff --git a/device/peer.go b/device/peer.go index ebf25f9..fff1cf5 100644 --- a/device/peer.go +++ b/device/peer.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - "golang.zx2c4.com/wireguard/conn" + "github.com/sagernet/wireguard-go/conn" ) type Peer struct { diff --git a/device/queueconstants_android.go b/device/queueconstants_android.go index 236dea1..a3bee69 100644 --- a/device/queueconstants_android.go +++ b/device/queueconstants_android.go @@ -5,7 +5,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/sagernet/wireguard-go/conn" /* Reduce memory consumption for Android */ diff --git a/device/queueconstants_default.go b/device/queueconstants_default.go index b061185..1d09285 100644 --- a/device/queueconstants_default.go +++ b/device/queueconstants_default.go @@ -7,7 +7,7 @@ package device -import "golang.zx2c4.com/wireguard/conn" +import "github.com/sagernet/wireguard-go/conn" const ( QueueStagedSize = conn.IdealBatchSize diff --git a/device/receive.go b/device/receive.go index 1392957..2c40730 100644 --- a/device/receive.go +++ b/device/receive.go @@ -12,10 +12,10 @@ import ( "sync" "time" + "github.com/sagernet/wireguard-go/conn" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/conn" ) type QueueHandshakeElement struct { @@ -411,7 +411,6 @@ func (device *Device) RoutineHandshake(id int) { // derive keypair err = peer.BeginSymmetricSession() - if err != nil { device.log.Errorf("%v - Failed to derive keypair: %v", peer, err) goto skip diff --git a/device/send.go b/device/send.go index ff8f7da..457fde7 100644 --- a/device/send.go +++ b/device/send.go @@ -13,11 +13,11 @@ import ( "sync" "time" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/tun" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/tun" ) /* Outbound flow diff --git a/device/sticky_default.go b/device/sticky_default.go index 22e1e15..cac7add 100644 --- a/device/sticky_default.go +++ b/device/sticky_default.go @@ -3,8 +3,8 @@ package device import ( - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" ) func (device *Device) startRouteListener(_ conn.Bind) (*rwcancel.RWCancel, error) { diff --git a/device/sticky_linux.go b/device/sticky_linux.go index f23ff02..9fcfeeb 100644 --- a/device/sticky_linux.go +++ b/device/sticky_linux.go @@ -18,10 +18,9 @@ import ( "sync" "unsafe" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" "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) { diff --git a/device/tun.go b/device/tun.go index c85dd50..01a92ed 100644 --- a/device/tun.go +++ b/device/tun.go @@ -8,7 +8,7 @@ package device import ( "fmt" - "golang.zx2c4.com/wireguard/tun" + "github.com/sagernet/wireguard-go/tun" ) const DefaultMTU = 1420 diff --git a/device/uapi.go b/device/uapi.go index cc69488..cba371d 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -18,7 +18,7 @@ import ( "sync" "time" - "golang.zx2c4.com/wireguard/ipc" + "github.com/sagernet/wireguard-go/ipc" ) type IPCError struct { diff --git a/go.mod b/go.mod index 85766cf..eab479d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module golang.zx2c4.com/wireguard +module github.com/sagernet/wireguard-go go 1.23.1 diff --git a/ipc/uapi_linux.go b/ipc/uapi_linux.go index fddded0..be59e58 100644 --- a/ipc/uapi_linux.go +++ b/ipc/uapi_linux.go @@ -9,8 +9,8 @@ import ( "net" "os" + "github.com/sagernet/wireguard-go/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/rwcancel" ) type UAPIListener struct { @@ -84,7 +84,6 @@ func UAPIListen(name string, file *os.File) (net.Listener, error) { unix.IN_DELETE| unix.IN_DELETE_SELF, ) - if err != nil { return nil, err } diff --git a/ipc/uapi_windows.go b/ipc/uapi_windows.go index 86e60b0..a146f1a 100644 --- a/ipc/uapi_windows.go +++ b/ipc/uapi_windows.go @@ -8,8 +8,8 @@ package ipc import ( "net" + "github.com/sagernet/wireguard-go/ipc/namedpipe" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/ipc/namedpipe" ) // TODO: replace these with actual standard windows error numbers from the win package diff --git a/tun/errors.go b/tun/errors.go index 75ae3a4..2c49fc7 100644 --- a/tun/errors.go +++ b/tun/errors.go @@ -4,9 +4,7 @@ import ( "errors" ) -var ( - // ErrTooManySegments is returned by Device.Read() when segmentation - // overflows the length of supplied buffers. This error should not cause - // reads to cease. - ErrTooManySegments = errors.New("too many segments") -) +// ErrTooManySegments is returned by Device.Read() when segmentation +// overflows the length of supplied buffers. This error should not cause +// reads to cease. +var ErrTooManySegments = errors.New("too many segments") diff --git a/tun/offload_linux.go b/tun/offload_linux.go index 5f0db06..d360d40 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -12,8 +12,8 @@ import ( "io" "unsafe" + "github.com/sagernet/wireguard-go/conn" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" ) const tcpFlagsOffset = 13 diff --git a/tun/tun_linux.go b/tun/tun_linux.go index 1461e06..b10ec3c 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -17,9 +17,9 @@ import ( "time" "unsafe" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/rwcancel" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/rwcancel" ) const ( @@ -514,9 +514,7 @@ func (tun *NativeTun) initFromFlags(name string) error { return err } if e := sc.Control(func(fd uintptr) { - var ( - ifr *unix.Ifreq - ) + var ifr *unix.Ifreq ifr, err = unix.NewIfreq(name) if err != nil { return From 45cd03b8a17cbe11e20af9d55753a8edacf72b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 19:34:11 +0800 Subject: [PATCH 131/173] Downgrade dependencies --- go.mod | 8 ++++---- go.sum | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index eab479d..2dfe133 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/sagernet/wireguard-go -go 1.23.1 +go 1.20 require ( - golang.org/x/crypto v0.37.0 - golang.org/x/net v0.39.0 - golang.org/x/sys v0.32.0 + golang.org/x/crypto v0.13.0 + golang.org/x/net v0.15.0 + golang.org/x/sys v0.12.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 ) diff --git a/go.sum b/go.sum index c9da2b3..ec4169f 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= From 749db0015c62b64eae1c2fd449177d28a6ae28ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 20:11:01 +0800 Subject: [PATCH 132/173] Apply Tailscale bind send headroom --- conn/bind_std.go | 9 +++++---- conn/bind_windows.go | 3 ++- conn/conn.go | 8 +++++--- device/constants.go | 6 +++--- device/noise-protocol.go | 15 ++++++++------- device/peer.go | 5 ++++- device/send.go | 36 +++++++++++++++++++++++------------- 7 files changed, 50 insertions(+), 32 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 80e8210..7cbf9a8 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -334,7 +334,7 @@ func (e ErrUDPGSODisabled) Unwrap() error { return e.RetryErr } -func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { +func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { s.mu.Lock() blackhole := s.blackhole4 conn := s.ipv4 @@ -377,7 +377,7 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint) error { ) retry: if offload { - n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, *msgs, setGSOSize) + n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, offset, *msgs, setGSOSize) err = s.send(conn, br, (*msgs)[:n]) if err != nil && offload && errShouldDisableUDPGSO(err) { offload = false @@ -394,7 +394,7 @@ retry: } else { for i := range bufs { (*msgs)[i].Addr = ua - (*msgs)[i].Buffers[0] = bufs[i] + (*msgs)[i].Buffers[0] = bufs[i][offset:] setSrcControl(&(*msgs)[i].OOB, endpoint.(*StdNetEndpoint)) } err = s.send(conn, br, (*msgs)[:len(bufs)]) @@ -443,7 +443,7 @@ const ( 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 ( base = -1 // index of msg we are currently coalescing into gsoSize int // segmentation size of msgs[base] @@ -455,6 +455,7 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, msgs maxPayloadLen = maxIPv6PayloadLen } for i, buf := range bufs { + buf = buf[offset:] if i > 0 { msgLen := len(buf) baseLenBefore := len(msgs[base].Buffers[0]) diff --git a/conn/bind_windows.go b/conn/bind_windows.go index d166227..66d91cd 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -485,7 +485,7 @@ func (bind *afWinRingBind) Send(buf []byte, nend *WinRingEndpoint, isOpen *atomi 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) if !ok { return ErrWrongEndpointType @@ -493,6 +493,7 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint) error { bind.mu.RLock() defer bind.mu.RUnlock() for _, buf := range bufs { + buf = buf[offset:] switch nend.family { case windows.AF_INET: if bind.v4.blackhole { diff --git a/conn/conn.go b/conn/conn.go index 1304657..139b06e 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -45,9 +45,11 @@ type Bind interface { // This mark is passed to the kernel as the socket option SO_MARK. SetMark(mark uint32) error - // Send writes one or more packets in bufs to address ep. The length of - // bufs must not exceed BatchSize(). - Send(bufs [][]byte, ep Endpoint) error + // Send writes one or more packets in bufs to address ep. A nonzero offset + // can be used to instruct the Bind on where packet data begins in each + // 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(s string) (Endpoint, error) diff --git a/device/constants.go b/device/constants.go index 41da618..1a02daf 100644 --- a/device/constants.go +++ b/device/constants.go @@ -27,9 +27,9 @@ const ( ) const ( - MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive) - MaxMessageSize = MaxSegmentSize // maximum size of transport message - MaxContentSize = MaxSegmentSize - MessageTransportSize // maximum size of transport message content + MinMessageSize = MessageKeepaliveSize // minimum size of transport message (keepalive) + MaxMessageSize = MaxSegmentSize // maximum size of transport message + MaxContentSize = MaxSegmentSize - MessageTransportSize - MessageEncapsulatingTransportSize // maximum size of transport message content ) /* Implementation constants */ diff --git a/device/noise-protocol.go b/device/noise-protocol.go index a9b8498..98a03bc 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -60,13 +60,14 @@ const ( ) const ( - MessageInitiationSize = 148 // size of handshake initiation message - MessageResponseSize = 92 // size of response message - MessageCookieReplySize = 64 // size of cookie reply message - MessageTransportHeaderSize = 16 // size of data preceding content in transport message - MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport - MessageKeepaliveSize = MessageTransportSize // size of keepalive - MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message + MessageInitiationSize = 148 // size of handshake initiation message + MessageResponseSize = 92 // size of response message + MessageCookieReplySize = 64 // size of cookie reply message + MessageTransportHeaderSize = 16 // size of data preceding content in transport message + MessageEncapsulatingTransportSize = 8 // size of optional, free (for use by conn.Bind.Send()) space preceding the transport header + MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport + MessageKeepaliveSize = MessageTransportSize // size of keepalive + MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message ) const ( diff --git a/device/peer.go b/device/peer.go index fff1cf5..bca121d 100644 --- a/device/peer.go +++ b/device/peer.go @@ -113,6 +113,9 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { return peer, nil } +// 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 { peer.device.net.RLock() defer peer.device.net.RUnlock() @@ -133,7 +136,7 @@ func (peer *Peer) SendBuffers(buffers [][]byte) error { } peer.endpoint.Unlock() - err := peer.device.net.bind.Send(buffers, endpoint) + err := peer.device.net.bind.Send(buffers, endpoint, MessageEncapsulatingTransportSize) if err == nil { var totalLen uint64 for _, b := range buffers { diff --git a/device/send.go b/device/send.go index 457fde7..78a3108 100644 --- a/device/send.go +++ b/device/send.go @@ -45,11 +45,15 @@ import ( */ type QueueOutboundElement struct { - buffer *[MaxMessageSize]byte // slice holding the packet data - packet []byte // slice of "buffer" (always!) - nonce uint64 // nonce for encryption - keypair *Keypair // keypair for encryption - peer *Peer // related peer + buffer *[MaxMessageSize]byte // slice holding the packet data + // packet is always a slice of "buffer". The starting offset in buffer + // is either: + // a) MessageEncapsulatingTransportSize+MessageTransportHeaderSize (plaintext) + // b) 0 (post-encryption) + packet []byte + nonce uint64 // nonce for encryption + keypair *Keypair // keypair for encryption + peer *Peer // related peer } type QueueOutboundElementsContainer struct { @@ -123,14 +127,15 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - packet := make([]byte, MessageInitiationSize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageInitiationSize) + packet := buf[MessageEncapsulatingTransportSize:] _ = msg.marshal(packet) peer.cookieGenerator.AddMacs(packet) peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - err = peer.SendBuffers([][]byte{packet}) + err = peer.SendBuffers([][]byte{buf}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -152,7 +157,8 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - packet := make([]byte, MessageResponseSize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageResponseSize) + packet := buf[MessageEncapsulatingTransportSize:] _ = response.marshal(packet) peer.cookieGenerator.AddMacs(packet) @@ -167,7 +173,7 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketSent() // TODO: allocation could be avoided - err = peer.SendBuffers([][]byte{packet}) + err = peer.SendBuffers([][]byte{buf}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } @@ -184,10 +190,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) return err } - packet := make([]byte, MessageCookieReplySize) + buf := make([]byte, MessageEncapsulatingTransportSize+MessageCookieReplySize) + packet := buf[MessageEncapsulatingTransportSize:] _ = reply.marshal(packet) // TODO: allocation could be avoided - device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint) + device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint, MessageEncapsulatingTransportSize) return nil } @@ -220,7 +227,7 @@ func (device *Device) RoutineReadFromTUN() { elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize) count = 0 sizes = make([]int, batchSize) - offset = MessageTransportHeaderSize + offset = MessageEncapsulatingTransportSize + MessageTransportHeaderSize ) for i := range elems { @@ -446,7 +453,7 @@ func (device *Device) RoutineEncryption(id int) { for elemsContainer := range device.queue.encryption.c { for _, elem := range elemsContainer.elems { // populate header fields - header := elem.buffer[:MessageTransportHeaderSize] + header := elem.buffer[MessageEncapsulatingTransportSize : MessageEncapsulatingTransportSize+MessageTransportHeaderSize] fieldType := header[0:4] fieldReceiver := header[4:8] @@ -469,6 +476,9 @@ func (device *Device) RoutineEncryption(id int) { elem.packet, nil, ) + + // re-slice packet to include encapsulating transport space + elem.packet = elem.buffer[:MessageEncapsulatingTransportSize+len(elem.packet)] } elemsContainer.Unlock() } From 414291f6d6ab8a4041e78b400be0f3af0c0b1d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 20:12:11 +0800 Subject: [PATCH 133/173] Apply Tailscale endpoint awareness --- conn/conn.go | 34 ++++++++++++++++++++++++++++++++++ device/noise-protocol.go | 8 +++++++- device/receive.go | 5 ++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/conn/conn.go b/conn/conn.go index 139b06e..87b7510 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -86,6 +86,40 @@ type Endpoint interface { 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 ( ErrBindAlreadyOpen = errors.New("bind is already open") ErrWrongEndpointType = errors.New("endpoint type does not correspond with bind type") diff --git a/device/noise-protocol.go b/device/noise-protocol.go index 98a03bc..ed4d82a 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/tai64n" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/chacha20poly1305" @@ -337,7 +338,7 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e return &msg, nil } -func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { +func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation, endpoint conn.Endpoint) *Peer { var ( hash [blake2s.Size]byte chainKey [blake2s.Size]byte @@ -371,6 +372,11 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer { // lookup peer + initEP, ok := endpoint.(conn.InitiationAwareEndpoint) + if ok { + initEP.InitiationMessagePublicKey(peerPK) + } + peer := device.LookupPeer(peerPK) if peer == nil || !peer.isRunning.Load() { return nil diff --git a/device/receive.go b/device/receive.go index 2c40730..e13c987 100644 --- a/device/receive.go +++ b/device/receive.go @@ -359,7 +359,7 @@ func (device *Device) RoutineHandshake(id int) { // consume initiation - peer := device.ConsumeMessageInitiation(&msg) + peer := device.ConsumeMessageInitiation(&msg, elem.endpoint) if peer == nil { device.log.Verbosef("Received invalid initiation message from %s", elem.endpoint.DstToString()) goto skip @@ -459,6 +459,9 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { peer.timersHandshakeComplete() 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 { From 824e7573c086a09674a7a6c90061664719df8395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 17 May 2026 20:15:11 +0800 Subject: [PATCH 134/173] Apply Tailscale TUN offload APIs --- tun/checksum.go | 11 +++ tun/offload.go | 200 +++++++++++++++++++++++++++++++++++++++++++ tun/offload_linux.go | 18 ++-- tun/tun.go | 14 +++ tun/tun_linux.go | 47 +++++++++- 5 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 tun/offload.go diff --git a/tun/checksum.go b/tun/checksum.go index b489c56..ac16569 100644 --- a/tun/checksum.go +++ b/tun/checksum.go @@ -92,6 +92,11 @@ func checksum(b []byte, initial uint64) uint16 { return uint16(ac) } +// Checksum computes an IP checksum starting with the provided initial value. +func Checksum(data []byte, initial uint16) uint16 { + return checksum(data, uint64(initial)) +} + func pseudoHeaderChecksumNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint64 { sum := checksumNoFold(srcAddr, 0) sum = checksumNoFold(dstAddr, sum) @@ -100,3 +105,9 @@ func pseudoHeaderChecksumNoFold(protocol uint8, srcAddr, dstAddr []byte, totalLe binary.BigEndian.PutUint16(tmp, totalLen) return checksumNoFold(tmp, sum) } + +// PseudoHeaderChecksum computes an IP pseudo-header checksum. srcAddr and +// dstAddr must be 4 or 16 bytes in length. +func PseudoHeaderChecksum(protocol uint8, srcAddr, dstAddr []byte, totalLen uint16) uint16 { + return checksum([]byte{}, pseudoHeaderChecksumNoFold(protocol, srcAddr, dstAddr, totalLen)) +} diff --git a/tun/offload.go b/tun/offload.go new file mode 100644 index 0000000..4e84db4 --- /dev/null +++ b/tun/offload.go @@ -0,0 +1,200 @@ +package tun + +import ( + "encoding/binary" + "fmt" +) + +// GSOType represents the type of segmentation offload. +type GSOType int + +const ( + GSONone GSOType = iota + GSOTCPv4 + GSOTCPv6 + GSOUDPL4 +) + +func (g GSOType) String() string { + switch g { + case GSONone: + return "GSONone" + case GSOTCPv4: + return "GSOTCPv4" + case GSOTCPv6: + return "GSOTCPv6" + case GSOUDPL4: + return "GSOUDPL4" + default: + return "unknown" + } +} + +// GSOOptions is loosely modeled after struct virtio_net_hdr from the VIRTIO +// specification. It is a common representation of GSO metadata that can be +// applied to support packet GSO across tun.Device implementations. +type GSOOptions struct { + // GSOType represents the type of segmentation offload. + GSOType GSOType + // HdrLen is the sum of the layer 3 and 4 header lengths. This field may be + // zero when GSOType == GSONone. + HdrLen uint16 + // CsumStart is the head byte index of the packet data to be checksummed, + // i.e. the start of the TCP or UDP header. + CsumStart uint16 + // CsumOffset is the offset from CsumStart where the 2-byte checksum value + // should be placed. + CsumOffset uint16 + // GSOSize is the size of each segment exclusive of HdrLen. The tail segment + // may be smaller than this value. + GSOSize uint16 + // NeedsCsum may be set where GSOType == GSONone. When set, the checksum + // at CsumStart + CsumOffset must be a partial checksum, i.e. the + // pseudo-header sum. + NeedsCsum bool +} + +const ( + gsoIPv4SrcAddrOffset = 12 + gsoIPv6SrcAddrOffset = 8 + gsoTCPFlagsOffset = 13 + gsoIPProtoTCP = 6 + gsoIPProtoUDP = 17 +) + +const ( + gsoTCPFlagFIN uint8 = 0x01 + gsoTCPFlagPSH uint8 = 0x08 +) + +// GSOSplit splits packets from in into outBufs[][outOffset:], writing +// the size of each element into sizes. It returns the number of buffers +// populated, and/or an error. Callers may pass an in slice that overlaps with +// the first element of outBufs, i.e. &in[0] may be equal to +// &outBufs[0][outOffset]. GSONone is a valid options.GSOType regardless of the +// value of options.NeedsCsum. Length of each outBufs element must be greater +// than or equal to the length of in, otherwise output may be silently truncated. +func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outOffset int) (int, error) { + cSumAt := int(options.CsumStart) + int(options.CsumOffset) + if cSumAt+1 >= len(in) { + return 0, fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(in)) + } + + if len(in) < int(options.HdrLen) { + return 0, fmt.Errorf("length of packet (%d) < GSO HdrLen (%d)", len(in), options.HdrLen) + } + + payloadLen := len(in) - int(options.HdrLen) + if options.GSOType == GSONone || payloadLen < int(options.GSOSize) { + if len(in) > len(outBufs[0][outOffset:]) { + return 0, fmt.Errorf("length of packet (%d) exceeds output element length (%d)", len(in), len(outBufs[0][outOffset:])) + } + if options.NeedsCsum { + initial := binary.BigEndian.Uint16(in[cSumAt:]) + in[cSumAt], in[cSumAt+1] = 0, 0 + binary.BigEndian.PutUint16(in[cSumAt:], ^Checksum(in[options.CsumStart:], initial)) + } + sizes[0] = copy(outBufs[0][outOffset:], in) + return 1, nil + } + + if options.HdrLen < options.CsumStart { + return 0, fmt.Errorf("GSO HdrLen (%d) < GSO CsumStart (%d)", options.HdrLen, options.CsumStart) + } + + ipVersion := in[0] >> 4 + switch ipVersion { + case 4: + if options.GSOType != GSOTCPv4 && options.GSOType != GSOUDPL4 { + return 0, fmt.Errorf("ip header version: %d, GSO type: %s", ipVersion, options.GSOType) + } + if len(in) < 20 { + return 0, fmt.Errorf("length of packet (%d) < minimum ipv4 header size (%d)", len(in), 20) + } + case 6: + if options.GSOType != GSOTCPv6 && options.GSOType != GSOUDPL4 { + return 0, fmt.Errorf("ip header version: %d, GSO type: %s", ipVersion, options.GSOType) + } + if len(in) < 40 { + return 0, fmt.Errorf("length of packet (%d) < minimum ipv6 header size (%d)", len(in), 40) + } + default: + return 0, fmt.Errorf("invalid ip header version: %d", ipVersion) + } + + iphLen := int(options.CsumStart) + srcAddrOffset := gsoIPv6SrcAddrOffset + addrLen := 16 + if ipVersion == 4 { + srcAddrOffset = gsoIPv4SrcAddrOffset + addrLen = 4 + } + transportCsumAt := int(options.CsumStart + options.CsumOffset) + var firstTCPSeqNum uint32 + var protocol uint8 + if options.GSOType == GSOTCPv4 || options.GSOType == GSOTCPv6 { + protocol = gsoIPProtoTCP + if len(in) < int(options.CsumStart)+20 { + return 0, fmt.Errorf("length of packet (%d) < GSO CsumStart (%d) + minimum TCP header size (%d)", + len(in), options.CsumStart, 20) + } + firstTCPSeqNum = binary.BigEndian.Uint32(in[options.CsumStart+4:]) + } else { + protocol = gsoIPProtoUDP + } + nextSegmentDataAt := int(options.HdrLen) + i := 0 + for ; nextSegmentDataAt < len(in); i++ { + if i == len(outBufs) { + return i - 1, ErrTooManySegments + } + nextSegmentEnd := nextSegmentDataAt + int(options.GSOSize) + if nextSegmentEnd > len(in) { + nextSegmentEnd = len(in) + } + segmentDataLen := nextSegmentEnd - nextSegmentDataAt + totalLen := int(options.HdrLen) + segmentDataLen + sizes[i] = totalLen + out := outBufs[i][outOffset:] + + copy(out, in[:iphLen]) + if ipVersion == 4 { + if i > 0 { + id := binary.BigEndian.Uint16(out[4:]) + id += uint16(i) + binary.BigEndian.PutUint16(out[4:], id) + } + out[10], out[11] = 0, 0 + binary.BigEndian.PutUint16(out[2:], uint16(totalLen)) + ipv4CSum := ^Checksum(out[:iphLen], 0) + binary.BigEndian.PutUint16(out[10:], ipv4CSum) + } else { + binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) + } + + copy(out[options.CsumStart:options.HdrLen], in[options.CsumStart:options.HdrLen]) + + if protocol == gsoIPProtoTCP { + tcpSeq := firstTCPSeqNum + uint32(options.GSOSize*uint16(i)) + binary.BigEndian.PutUint32(out[options.CsumStart+4:], tcpSeq) + if nextSegmentEnd != len(in) { + clearFlags := gsoTCPFlagFIN | gsoTCPFlagPSH + out[options.CsumStart+gsoTCPFlagsOffset] &^= clearFlags + } + } else { + binary.BigEndian.PutUint16(out[options.CsumStart+4:], uint16(segmentDataLen)+(options.HdrLen-options.CsumStart)) + } + + copy(out[options.HdrLen:], in[nextSegmentDataAt:nextSegmentEnd]) + + out[transportCsumAt], out[transportCsumAt+1] = 0, 0 + transportHeaderLen := int(options.HdrLen - options.CsumStart) + lenForPseudo := uint16(transportHeaderLen + segmentDataLen) + transportCSum := PseudoHeaderChecksum(protocol, in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum = ^Checksum(out[options.CsumStart:totalLen], transportCSum) + binary.BigEndian.PutUint16(out[options.CsumStart+options.CsumOffset:], transportCSum) + + nextSegmentDataAt += int(options.GSOSize) + } + return i, nil +} diff --git a/tun/offload_linux.go b/tun/offload_linux.go index d360d40..6825bbc 100644 --- a/tun/offload_linux.go +++ b/tun/offload_linux.go @@ -748,7 +748,7 @@ const ( udp6GROCandidate ) -func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType { +func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if len(b) < 28 { return notGROCandidate } @@ -757,17 +757,17 @@ func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType { // IPv4 packets w/IP options do not coalesce return notGROCandidate } - if b[9] == unix.IPPROTO_TCP && len(b) >= 40 { + if b[9] == unix.IPPROTO_TCP && len(b) >= 40 && gro.canTCPGRO() { return tcp4GROCandidate } - if b[9] == unix.IPPROTO_UDP && canUDPGRO { + if b[9] == unix.IPPROTO_UDP && gro.canUDPGRO() { return udp4GROCandidate } } else if b[0]>>4 == 6 { - if b[6] == unix.IPPROTO_TCP && len(b) >= 60 { + if b[6] == unix.IPPROTO_TCP && len(b) >= 60 && gro.canTCPGRO() { return tcp6GROCandidate } - if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && canUDPGRO { + if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && gro.canUDPGRO() { return udp6GROCandidate } } @@ -860,15 +860,15 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) // handleGRO evaluates bufs for GRO, and writes the indices of the resulting // packets into toWrite. toWrite, tcpTable, and udpTable should initially be // empty (but non-nil), and are passed in to save allocs as the caller may reset -// and recycle them across vectors of packets. canUDPGRO indicates if UDP GRO is -// supported. -func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, canUDPGRO bool, toWrite *[]int) error { +// and recycle them across vectors of packets. gro indicates if TCP and UDP GRO +// are supported/enabled. +func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, gro groDisablementFlags, toWrite *[]int) error { for i := range bufs { if offset < virtioNetHdrLen || offset > len(bufs[i])-1 { return errors.New("invalid offset") } var result groResult - switch packetIsGROCandidate(bufs[i][offset:], canUDPGRO) { + switch packetIsGROCandidate(bufs[i][offset:], gro) { case tcp4GROCandidate: result = tcpGRO(bufs, offset, i, tcpTable, false) case tcp6GROCandidate: diff --git a/tun/tun.go b/tun/tun.go index 336d642..6fb5c56 100644 --- a/tun/tun.go +++ b/tun/tun.go @@ -51,3 +51,17 @@ type Device interface { // lifetime of a Device. BatchSize() int } + +// GRODevice is a Device extended with methods for disabling GRO. Certain OS +// versions may have offload bugs. Where these bugs negatively impact throughput +// or break connectivity entirely we can use these methods to disable the +// related offload. +type GRODevice interface { + Device + + // DisableUDPGRO disables UDP GRO if it is enabled. + DisableUDPGRO() + + // DisableTCPGRO disables TCP GRO if it is enabled. + DisableTCPGRO() +} diff --git a/tun/tun_linux.go b/tun/tun_linux.go index b10ec3c..4b7866d 100644 --- a/tun/tun_linux.go +++ b/tun/tun_linux.go @@ -38,7 +38,6 @@ type NativeTun struct { statusListenersShutdown chan struct{} batchSize int vnetHdr bool - udpGSO bool closeOnce sync.Once @@ -49,10 +48,34 @@ type NativeTun struct { readOpMu sync.Mutex // readOpMu guards readBuff readBuff [virtioNetHdrLen + 65535]byte // if vnetHdr every read() is prefixed by virtioNetHdr - writeOpMu sync.Mutex // writeOpMu guards toWrite, tcpGROTable + writeOpMu sync.Mutex // writeOpMu guards toWrite, tcpGROTable, udpGROTable, gro toWrite []int tcpGROTable *tcpGROTable udpGROTable *udpGROTable + gro groDisablementFlags +} + +type groDisablementFlags int + +const ( + tcpGRODisabled groDisablementFlags = 1 << iota + udpGRODisabled +) + +func (g *groDisablementFlags) disableTCPGRO() { + *g |= tcpGRODisabled +} + +func (g *groDisablementFlags) canTCPGRO() bool { + return (*g)&tcpGRODisabled == 0 +} + +func (g *groDisablementFlags) disableUDPGRO() { + *g |= udpGRODisabled +} + +func (g *groDisablementFlags) canUDPGRO() bool { + return (*g)&udpGRODisabled == 0 } func (tun *NativeTun) File() *os.File { @@ -345,7 +368,7 @@ func (tun *NativeTun) Write(bufs [][]byte, offset int) (int, error) { ) tun.toWrite = tun.toWrite[:0] if tun.vnetHdr { - err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.udpGSO, &tun.toWrite) + err := handleGRO(bufs, offset, tun.tcpGROTable, tun.udpGROTable, tun.gro, &tun.toWrite) if err != nil { return 0, err } @@ -502,6 +525,20 @@ func (tun *NativeTun) BatchSize() int { return tun.batchSize } +// DisableUDPGRO disables UDP GRO if it is enabled. +func (tun *NativeTun) DisableUDPGRO() { + tun.writeOpMu.Lock() + tun.gro.disableUDPGRO() + tun.writeOpMu.Unlock() +} + +// DisableTCPGRO disables TCP GRO if it is enabled. +func (tun *NativeTun) DisableTCPGRO() { + tun.writeOpMu.Lock() + tun.gro.disableTCPGRO() + tun.writeOpMu.Unlock() +} + const ( // TODO: support TSO with ECN bits tunTCPOffloads = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6 @@ -535,7 +572,9 @@ func (tun *NativeTun) initFromFlags(name string) error { tun.batchSize = conn.IdealBatchSize // tunUDPOffloads were added in Linux v6.2. We do not return an // error if they are unsupported at runtime. - tun.udpGSO = unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) == nil + if unix.IoctlSetInt(int(fd), unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) != nil { + tun.gro.disableUDPGRO() + } } else { tun.batchSize = 1 } From b4db0692d3ad34898a95be7ec4ef59b14861c336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Nov 2022 15:41:48 +0800 Subject: [PATCH 135/173] Add custom worker size params (cherry picked from commit 7c2acadba17cadf8a1df957c49e1333130d460ad) (cherry picked from commit a7bac1754e7717e1d4009d1ffd2d13330067d631) (cherry picked from commit 7a2f11c693b49e784318bbf987173095c67b563d) --- device/device.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/device/device.go b/device/device.go index bfc56bb..88cdf61 100644 --- a/device/device.go +++ b/device/device.go @@ -281,7 +281,7 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error { return nil } -func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { +func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { device := new(Device) device.state.state.Store(uint32(deviceStateDown)) device.closed = make(chan struct{}) @@ -308,10 +308,12 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device { // start workers - cpus := runtime.NumCPU() + if workers == 0 { + workers = runtime.NumCPU() + } device.state.stopping.Wait() - device.queue.encryption.wg.Add(cpus) // One for each RoutineHandshake - for i := 0; i < cpus; i++ { + device.queue.encryption.wg.Add(workers) // One for each RoutineHandshake + for i := 0; i < workers; i++ { go device.RoutineEncryption(i + 1) go device.RoutineDecryption(i + 1) go device.RoutineHandshake(i + 1) From 7be452de15404412946d6a0d1706bd5cced956e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 7 Aug 2023 20:57:31 +0800 Subject: [PATCH 136/173] Add pause support --- device/device.go | 13 +++++++++---- device/timers.go | 3 +++ go.mod | 3 ++- go.sum | 6 ++++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/device/device.go b/device/device.go index 88cdf61..8afa6fd 100644 --- a/device/device.go +++ b/device/device.go @@ -6,11 +6,14 @@ package device import ( + "context" "runtime" "sync" "sync/atomic" "time" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/ratelimiter" "github.com/sagernet/wireguard-go/rwcancel" @@ -86,9 +89,10 @@ type Device struct { mtu atomic.Int32 } - ipcMutex sync.RWMutex - closed chan struct{} - log *Logger + ipcMutex sync.RWMutex + closed chan struct{} + log *Logger + pauseManager pause.Manager } // deviceState represents the state of a Device. @@ -281,8 +285,9 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error { return nil } -func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { +func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device { device := new(Device) + device.pauseManager = service.FromContext[pause.Manager](ctx) device.state.state.Store(uint32(deviceStateDown)) device.closed = make(chan struct{}) device.log = logger diff --git a/device/timers.go b/device/timers.go index 32519aa..80fb7d9 100644 --- a/device/timers.go +++ b/device/timers.go @@ -39,6 +39,9 @@ func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer { timer.isPending = false timer.modifyingLock.Unlock() + if pauseManager := peer.device.pauseManager; pauseManager != nil { + pauseManager.WaitActive() + } expirationFunction(peer) }) timer.Stop() diff --git a/go.mod b/go.mod index 2dfe133..773bf00 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/sagernet/wireguard-go go 1.20 require ( + github.com/sagernet/sing v0.7.10 golang.org/x/crypto v0.13.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 ) diff --git a/go.sum b/go.sum index ec4169f..9ce9725 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ +github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw= +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/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= From a71256d250b1838f2db96db7a80a68a1a06d40de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 20 Nov 2024 18:53:51 +0800 Subject: [PATCH 137/173] Add device.InputPacket --- device/send.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/device/send.go b/device/send.go index 78a3108..64a4221 100644 --- a/device/send.go +++ b/device/send.go @@ -322,6 +322,30 @@ func (device *Device) RoutineReadFromTUN() { } } +func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { + peer := device.allowedips.Lookup(destination) + if peer == nil { + return + } + elem := device.NewOutboundElement() + packet := elem.buffer[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.PutMessageBuffer(elem.buffer) + device.PutOutboundElement(elem) + device.PutOutboundElementsContainer(elemsForPeer) + } +} + func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { for { select { From 73f8c6542b658e44eac4b98b539c5741ed208599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Dec 2023 18:44:05 +0800 Subject: [PATCH 138/173] Export std net bind --- conn/bind_std.go | 58 +++++++++++++++++++++++++++++---- conn/bind_windows.go | 76 ++++++++++++++++++++++++++++++++++++++++---- conn/conn.go | 2 ++ conn/controlfns.go | 26 ++------------- conn/default.go | 6 +++- 5 files changed, 129 insertions(+), 39 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 7cbf9a8..cb979e3 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -16,6 +16,9 @@ import ( "sync" "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/ipv6" ) @@ -28,6 +31,9 @@ var _ Bind = (*StdNetBind)(nil) // methods for sending and receiving multiple datagrams per-syscall. See the // proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564. type StdNetBind struct { + externalControl control.Func + reservedForEndpoint map[netip.AddrPort][3]uint8 + mu sync.Mutex // protects all fields except as specified ipv4 *net.UDPConn ipv6 *net.UDPConn @@ -46,8 +52,11 @@ type StdNetBind struct { blackhole6 bool } -func NewStdNetBind() Bind { +func NewStdNetBind(externalControl control.Func) Bind { return &StdNetBind{ + externalControl: externalControl, + reservedForEndpoint: make(map[netip.AddrPort][3]uint8), + udpAddrPool: sync.Pool{ New: func() any { return &net.UDPAddr{ @@ -117,8 +126,29 @@ func (e *StdNetEndpoint) DstToString() string { return e.AddrPort.String() } -func listenNet(network string, port int) (*net.UDPConn, int, error) { - conn, err := listenConfig().ListenPacket(context.Background(), network, ":"+strconv.Itoa(port)) +func listenNet(externalControl control.Func, network string, port int) (*net.UDPConn, int, error) { + 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 { return nil, 0, err } @@ -154,13 +184,13 @@ again: var v4pc *ipv4.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) { return nil, 0, err } // 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 { v4conn.Close() tries++ @@ -265,8 +295,10 @@ func (s *StdNetBind) receiveIP( if sizes[i] == 0 { continue } - addrPort := msg.Addr.(*net.UDPAddr).AddrPort() - ep := &StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation + if msg.N > 3 { + common.ClearArray(bufs[i][1:4]) + } + ep := &StdNetEndpoint{AddrPort: M.AddrPortFromNet(msg.Addr)} // TODO: remove allocation getSrcFromControl(msg.OOB[:msg.NN], ep) eps[i] = ep } @@ -375,6 +407,14 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { retried bool err error ) + for _, buf := range bufs { + if len(buf) > 3 { + reserved, loaded := s.reservedForEndpoint[endpoint.(*StdNetEndpoint).AddrPort] + if loaded { + copy(buf[1:4], reserved[:]) + } + } + } retry: if offload { n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, offset, *msgs, setGSOSize) @@ -405,6 +445,10 @@ retry: return err } +func (s *StdNetBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) { + s.reservedForEndpoint[destination] = reserved +} + func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error { var ( n int diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 66d91cd..121079f 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -15,6 +15,10 @@ import ( "sync/atomic" "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" ) @@ -71,18 +75,26 @@ type afWinRingBind struct { // WinRingBind uses Windows registered I/O for fast ring buffered networking. type WinRingBind struct { + externalControl control.Func + reservedForEndpoint map[WinRingEndpoint][3]uint8 + v4, v6 afWinRingBind mu sync.RWMutex 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() { - return NewStdNetBind() + return NewStdNetBind(externalControl) + } + return &WinRingBind{ + externalControl: externalControl, + reservedForEndpoint: make(map[WinRingEndpoint][3]uint8), } - return new(WinRingBind) } type WinRingEndpoint struct { @@ -238,7 +250,7 @@ func (ring *ringBuffer) Open() error { 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 bind.sock, err = winrio.Socket(family, windows.SOCK_DGRAM, windows.IPPROTO_UDP) if err != nil { @@ -256,6 +268,19 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock if err != nil { 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) if err != nil { return nil, err @@ -267,6 +292,23 @@ func (bind *afWinRingBind) Open(family int32, sa windows.Sockaddr) (windows.Sock 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) { bind.mu.Lock() defer bind.mu.Unlock() @@ -279,11 +321,11 @@ func (bind *WinRingBind) Open(port uint16) (recvFns []ReceiveFunc, selectedPort return nil, 0, ErrBindAlreadyOpen } 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 { 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 { return nil, 0, err } @@ -419,6 +461,9 @@ func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen) + if n > 3 { + common.ClearArray(bufs[0][1:4]) + } sizes[0] = n eps[0] = ep return 1, err @@ -428,6 +473,9 @@ func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen) + if n > 3 { + common.ClearArray(bufs[0][1:4]) + } sizes[0] = n eps[0] = ep return 1, err @@ -494,6 +542,12 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint, offset int) erro defer bind.mu.RUnlock() 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 { case windows.AF_INET: if bind.v4.blackhole { @@ -514,6 +568,14 @@ func (bind *WinRingBind) Send(bufs [][]byte, endpoint Endpoint, offset int) erro 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 +} + func (s *StdNetBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/conn/conn.go b/conn/conn.go index 87b7510..c949b59 100644 --- a/conn/conn.go +++ b/conn/conn.go @@ -57,6 +57,8 @@ type Bind interface { // BatchSize is the number of buffers expected to be passed to // the ReceiveFuncs, and the maximum expected to be passed to SendBatch. BatchSize() int + + SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) } // BindSocketToInterface is implemented by Bind objects that support being diff --git a/conn/controlfns.go b/conn/controlfns.go index 27421bd..d4164e4 100644 --- a/conn/controlfns.go +++ b/conn/controlfns.go @@ -6,8 +6,7 @@ package conn import ( - "net" - "syscall" + "github.com/sagernet/sing/common/control" ) // UDP socket read/write buffer size (7MB). The value of 7MB is chosen as it is @@ -17,27 +16,6 @@ import ( // around this limitation) 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 // that can apply socket options. -var controlFns = []controlFn{} - -// 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 - }, - } -} +var controlFns []control.Func diff --git a/conn/default.go b/conn/default.go index 2ce1579..9907f64 100644 --- a/conn/default.go +++ b/conn/default.go @@ -7,4 +7,8 @@ package conn -func NewDefaultBind() Bind { return NewStdNetBind() } +import "github.com/sagernet/sing/common/control" + +func NewDefaultBind(externalControl control.Func) Bind { + return NewStdNetBind(externalControl) +} From fa73d0f1ae147fd4a214b20e018a8e2c6c4ba6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Sep 2025 19:03:11 +0800 Subject: [PATCH 139/173] Fix input packet --- device/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device/send.go b/device/send.go index 64a4221..264e964 100644 --- a/device/send.go +++ b/device/send.go @@ -328,7 +328,7 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { return } elem := device.NewOutboundElement() - packet := elem.buffer[MessageTransportHeaderSize:] + packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] var n int for _, packetSlice := range packetSlices { n += copy(packet[n:], packetSlice) From 19b0d3587703f82379d4d05b03e8666d16df1b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Feb 2026 15:47:47 +0800 Subject: [PATCH 140/173] Fix reserved bytes offset in StdNetBind.Send --- conn/bind_std.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index cb979e3..4dadb0d 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -408,10 +408,10 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { err error ) for _, buf := range bufs { - if len(buf) > 3 { + if len(buf) > offset+3 { reserved, loaded := s.reservedForEndpoint[endpoint.(*StdNetEndpoint).AddrPort] if loaded { - copy(buf[1:4], reserved[:]) + copy(buf[offset+1:offset+4], reserved[:]) } } } From 58f534ff106c89ae30ce6b1cf16a9906aa31a83a Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:51:32 +0300 Subject: [PATCH 141/173] =?UTF-8?q?docs:=20add=20README=20=E2=80=94=20sage?= =?UTF-8?q?rnet/wireguard-go=20+=20AmneziaWG=202.0=20(3-way=20merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explain why this fork exists (sing-box needs sagernet APIs, AmneziaWG obfuscation lives in amneziawg-go, neither alone works), how the merge is done (3-way merge + MessageEncapsulatingTransportSize=0, obfuscation isolated to device/), how sing-box-lx consumes it (submodule + replace), and the recipe to rebase onto a new sagernet tag. --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..90b89dc --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# wireguard-go (lx fork) — sagernet + AmneziaWG 2.0 + +The WireGuard-Go runtime used by **[sing-box-lx](https://github.com/Leadaxe/sing-box-lx)**: +**[sagernet/wireguard-go](https://github.com/sagernet/wireguard-go)** (the fork sing-box builds on) **+ AmneziaWG 2.0 obfuscation**, merged together. + +This is **not** a general-purpose project. It exists for one reason — see below — and lives on the **`lx`** branch. + +--- + +## Why this fork exists + +sing-box's WireGuard endpoint needs **sagernet/wireguard-go**'s additions (the `conn.Bind.Send(…, offset)` contract, `device.InputPacket`, reserved/control). AmneziaWG's DPI-evasion obfuscation lives in **[amnezia-vpn/amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go)**, which is a fork of *upstream* wireguard-go and therefore **lacks** those sagernet additions. + +So neither fork alone works for sing-box-lx: + +| | sing-box-compat API | AmneziaWG obfuscation | +|---|:---:|:---:| +| `sagernet/wireguard-go` | ✅ | ❌ | +| `amnezia-vpn/amneziawg-go` | ❌ | ✅ | +| **this fork** | ✅ | ✅ | + +The approach is **(A): keep the sagernet base, graft the obfuscation onto it.** sing-box compiles unchanged against this fork, and a config without AWG fields behaves exactly like plain WireGuard. + +## How the merge works + +Both `sagernet/wireguard-go` and `amneziawg-go` descend from the same upstream `git.zx2c4.com/wireguard-go`, so they share git history — which makes a real **3-way merge** possible (not a hand-port). + +- **Base:** `sagernet/wireguard-go` (the exact commit sing-box pins — currently `506b7631853c`). +- **Merged in:** `amnezia-vpn/amneziawg-go` (a tip with AWG2 / I1–I5 + the S4-keepalive fix). +- **Key trick:** `MessageEncapsulatingTransportSize` is set to **`0`** in `device/noise-protocol.go`. sing-box-lx does not use sagernet's 8-byte `Bind.Send` headroom, and zeroing it makes the AmneziaWG obfuscation compose cleanly with no weave conflicts in the packet send path. +- **Isolation:** the obfuscation is confined to `device/` — new files `device/obf*.go`, `device/magic-header.go`, plus grafts in `device/{send,receive,device,uapi}.go`. **`conn/`, `tun/`, `ipc/` stay pure sagernet.** +- **Module path is unchanged** (`module github.com/sagernet/wireguard-go`) so the consumer plugs it in with a `replace` directive and needs **no import edits**. + +## Consumed by + +[sing-box-lx](https://github.com/Leadaxe/sing-box-lx) wires this in as a git submodule + a `replace`: + +``` +# sing-box-lx/.gitmodules +[submodule "submodules/wireguard-go"] + url = https://github.com/Leadaxe/wireguard-go + branch = lx + +# sing-box-lx/go.mod (// lx) +replace github.com/sagernet/wireguard-go => ./submodules/wireguard-go +``` + +Built with the `with_awg` tag, it has been **live-validated** against a real AmneziaWG 2.0 server (handshake + keepalive + outbound traffic) and cross-compiles on linux/darwin/windows × amd64/arm64. + +## Maintaining it (rebase onto a new sagernet tag) + +When sing-box bumps `sagernet/wireguard-go`, redo the merge: + +```sh +git remote add origin https://github.com/sagernet/wireguard-go # base +git remote add amnezia https://github.com/amnezia-vpn/amneziawg-go # obfuscation source +git fetch --all +git checkout -b lx +git merge amnezia/master # real 3-way merge via the shared upstream ancestor +``` + +Conflict resolution recipe: + +1. New `device/obf*.go` + `device/magic-header.go` come in clean. +2. **Mechanical** conflicts (amnezia → sagernet import paths, `queueconstants*`, `sticky*`, `tun.go`) → take **ours** (sagernet). +3. Remove amnezia-added infra duplicates: `conn/gso_*.go`, `outline/*`, `tun/*_test.go`. +4. `device/device.go` → **union** (sagernet `pauseManager` + amnezia obf fields). +5. `device/send.go` / `receive.go` → take amnezia's obfuscation, set `MessageEncapsulatingTransportSize = 0`, and keep the 3-arg `bind.Send(…, 0)` calls. +6. `conn/`, `tun/`, `ipc/`, `go.mod` module path → **ours** (sagernet). + +Then in sing-box-lx: bump the submodule, `make -f Makefile.lx lx-build`, and re-test against an AWG2 server. + +## Links + +| | | +|---|---| +| Consumer | [Leadaxe/sing-box-lx](https://github.com/Leadaxe/sing-box-lx) | +| Base | [sagernet/wireguard-go](https://github.com/sagernet/wireguard-go) | +| Obfuscation source | [amnezia-vpn/amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go) · [docs.amnezia.org](https://docs.amnezia.org/documentation/amnezia-wg/) | +| Original | [WireGuard/wireguard-go](https://git.zx2c4.com/wireguard-go/about/) | + +## License + +MIT, inherited from WireGuard-Go (see [`LICENSE`](LICENSE)). The AmneziaWG obfuscation is likewise MIT (from amneziawg-go). This is an unofficial fork, not affiliated with WireGuard, SagerNet, or Amnezia. From 7e50606f8014633270abe13e801fc92c4256afeb Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:58:39 +0300 Subject: [PATCH 142/173] docs: bilingual README (EN + RU); repo renamed to wireguard-go-awg2-lx --- README.md | 4 ++- README.ru.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 README.ru.md diff --git a/README.md b/README.md index 90b89dc..42b94d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**English** · [Русский](README.ru.md) + # wireguard-go (lx fork) — sagernet + AmneziaWG 2.0 The WireGuard-Go runtime used by **[sing-box-lx](https://github.com/Leadaxe/sing-box-lx)**: @@ -38,7 +40,7 @@ Both `sagernet/wireguard-go` and `amneziawg-go` descend from the same upstream ` ``` # sing-box-lx/.gitmodules [submodule "submodules/wireguard-go"] - url = https://github.com/Leadaxe/wireguard-go + url = https://github.com/Leadaxe/wireguard-go-awg2-lx branch = lx # sing-box-lx/go.mod (// lx) diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..9acd5d5 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,86 @@ +[English](README.md) · **Русский** + +# wireguard-go (lx-форк) — sagernet + AmneziaWG 2.0 + +Рантайм WireGuard-Go для **[sing-box-lx](https://github.com/Leadaxe/sing-box-lx)**: +**[sagernet/wireguard-go](https://github.com/sagernet/wireguard-go)** (форк, на котором собирается sing-box) **+ обфускация AmneziaWG 2.0**, слитые вместе. + +Это **не** универсальный проект. Он существует ради одной задачи (см. ниже) и живёт на ветке **`lx`**. + +--- + +## Зачем этот форк + +WireGuard-endpoint sing-box нуждается в добавках **sagernet/wireguard-go** (контракт `conn.Bind.Send(…, offset)`, `device.InputPacket`, reserved/control). Обфускация против DPI у AmneziaWG живёт в **[amnezia-vpn/amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go)**, который форкнут от *upstream* wireguard-go и потому этих sagernet-добавок **не имеет**. + +Значит для sing-box-lx ни один форк по отдельности не подходит: + +| | API под sing-box | обфускация AmneziaWG | +|---|:---:|:---:| +| `sagernet/wireguard-go` | ✅ | ❌ | +| `amnezia-vpn/amneziawg-go` | ❌ | ✅ | +| **этот форк** | ✅ | ✅ | + +Подход **(A): берём sagernet-базу и граффтим обфускацию на неё.** sing-box компилируется с этим форком без изменений, а конфиг без AWG-полей ведёт себя как обычный WireGuard. + +## Как устроен merge + +И `sagernet/wireguard-go`, и `amneziawg-go` происходят от одного upstream `git.zx2c4.com/wireguard-go`, поэтому делят git-историю — а значит возможен настоящий **3-way merge** (а не ручной перенос). + +- **База:** `sagernet/wireguard-go` (тот коммит, что пинит sing-box — сейчас `506b7631853c`). +- **Вливаем:** `amnezia-vpn/amneziawg-go` (тип с AWG2 / I1–I5 + фикс S4-keepalive). +- **Ключевой трюк:** `MessageEncapsulatingTransportSize` выставлен в **`0`** в `device/noise-protocol.go`. sing-box-lx не использует 8-байтный headroom sagernet для `Bind.Send`, и обнуление позволяет обфускации AmneziaWG встать чисто, без конфликтов в send-пути. +- **Изоляция:** обфускация замкнута в `device/` — новые файлы `device/obf*.go`, `device/magic-header.go` + графты в `device/{send,receive,device,uapi}.go`. **`conn/`, `tun/`, `ipc/` остаются чистым sagernet.** +- **Module-path не меняется** (`module github.com/sagernet/wireguard-go`), поэтому потребитель подключает форк через `replace` без правки импортов. + +## Кто потребляет + +[sing-box-lx](https://github.com/Leadaxe/sing-box-lx) подключает это как git submodule + `replace`: + +``` +# sing-box-lx/.gitmodules +[submodule "submodules/wireguard-go"] + url = https://github.com/Leadaxe/wireguard-go-awg2-lx + branch = lx + +# sing-box-lx/go.mod (// lx) +replace github.com/sagernet/wireguard-go => ./submodules/wireguard-go +``` + +Собранный с тегом `with_awg`, он **проверен живым** сервером AmneziaWG 2.0 (handshake + keepalive + трафик наружу) и кросс-компилируется на linux/darwin/windows × amd64/arm64. + +## Сопровождение (ребейз на новый sagernet-тег) + +Когда sing-box бампит `sagernet/wireguard-go`, повторяем merge: + +```sh +git remote add origin https://github.com/sagernet/wireguard-go # база +git remote add amnezia https://github.com/amnezia-vpn/amneziawg-go # источник обфускации +git fetch --all +git checkout -b lx <новый-sagernet-коммит> +git merge amnezia/master # настоящий 3-way merge через общего upstream-предка +``` + +Рецепт разрешения конфликтов: + +1. Новые `device/obf*.go` + `device/magic-header.go` приходят чисто. +2. **Механические** конфликты (import-path amnezia → sagernet, `queueconstants*`, `sticky*`, `tun.go`) → берём **наши** (sagernet). +3. Удаляем amnezia-инфра-дубликаты: `conn/gso_*.go`, `outline/*`, `tun/*_test.go`. +4. `device/device.go` → **union** (sagernet `pauseManager` + obf-поля amnezia). +5. `device/send.go` / `receive.go` → берём обфускацию amnezia, ставим `MessageEncapsulatingTransportSize = 0`, сохраняем 3-арг `bind.Send(…, 0)`. +6. `conn/`, `tun/`, `ipc/`, module-path в `go.mod` → **наши** (sagernet). + +Затем в sing-box-lx: бампим submodule, `make -f Makefile.lx lx-build` и пере-тест против AWG2-сервера. + +## Ссылки + +| | | +|---|---| +| Потребитель | [Leadaxe/sing-box-lx](https://github.com/Leadaxe/sing-box-lx) | +| База | [sagernet/wireguard-go](https://github.com/sagernet/wireguard-go) | +| Источник обфускации | [amnezia-vpn/amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go) · [docs.amnezia.org](https://docs.amnezia.org/documentation/amnezia-wg/) | +| Оригинал | [WireGuard/wireguard-go](https://git.zx2c4.com/wireguard-go/about/) | + +## Лицензия + +MIT, унаследована от WireGuard-Go (см. [`LICENSE`](LICENSE)). Обфускация AmneziaWG — тоже MIT (из amneziawg-go). Это неофициальный форк, не аффилирован с WireGuard, SagerNet или Amnezia. From a2307b51f775dba98f3b467531b3a3a4fc0f6f3f Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:04:06 +0300 Subject: [PATCH 143/173] docs: drop dangling "(A)" label; explain the merge direction inline --- README.md | 2 +- README.ru.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42b94d1..f62356a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ So neither fork alone works for sing-box-lx: | `amnezia-vpn/amneziawg-go` | ❌ | ✅ | | **this fork** | ✅ | ✅ | -The approach is **(A): keep the sagernet base, graft the obfuscation onto it.** sing-box compiles unchanged against this fork, and a config without AWG fields behaves exactly like plain WireGuard. +The approach: **keep the sagernet base and graft the obfuscation onto it** — rather than the reverse (adding sagernet's APIs to amneziawg-go, which would route even plain WireGuard through a foreign device). This way sing-box compiles unchanged, the obfuscation is additive and off by default, and a config without AWG fields behaves exactly like plain WireGuard. ## How the merge works diff --git a/README.ru.md b/README.ru.md index 9acd5d5..842ba7c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -21,7 +21,7 @@ WireGuard-endpoint sing-box нуждается в добавках **sagernet/wi | `amnezia-vpn/amneziawg-go` | ❌ | ✅ | | **этот форк** | ✅ | ✅ | -Подход **(A): берём sagernet-базу и граффтим обфускацию на неё.** sing-box компилируется с этим форком без изменений, а конфиг без AWG-полей ведёт себя как обычный WireGuard. +Подход: **берём sagernet-базу и граффтим обфускацию на неё** — а не наоборот (не дотачиваем sagernet-API к amneziawg-go, иначе даже обычный WireGuard шёл бы через чужой device). Так sing-box компилируется без изменений, обфускация аддитивна и выключена по умолчанию, а конфиг без AWG-полей ведёт себя как обычный WireGuard. ## Как устроен merge From 7b15aacbfef2219ba4eea47c8d6245a000a29e36 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:05:40 +0300 Subject: [PATCH 144/173] docs: spell out why neither fork alone works (each gives half) before the merge rationale --- README.md | 7 +++++++ README.ru.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index f62356a..dd92535 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ So neither fork alone works for sing-box-lx: | `amnezia-vpn/amneziawg-go` | ❌ | ✅ | | **this fork** | ✅ | ✅ | +Each existing fork gives exactly **half** of what's needed: + +- **Take `sagernet/wireguard-go`** → sing-box-lx compiles and runs, but the AWG fields (`jc`/`h1`/`i1`…) do nothing → **no obfuscation**; AmneziaWG doesn't actually work. +- **Take `amnezia-vpn/amneziawg-go`** → the obfuscation is there, but sing-box-lx **won't even compile** (the sagernet functions are missing). + +We need **both** ✅ at once, and no ready-made fork has them — so we built one by **merging**: sagernet (for the API) + amnezia (for the obfuscation). That is exactly the **"this fork"** row above. + The approach: **keep the sagernet base and graft the obfuscation onto it** — rather than the reverse (adding sagernet's APIs to amneziawg-go, which would route even plain WireGuard through a foreign device). This way sing-box compiles unchanged, the obfuscation is additive and off by default, and a config without AWG fields behaves exactly like plain WireGuard. ## How the merge works diff --git a/README.ru.md b/README.ru.md index 842ba7c..7482bd4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -21,6 +21,13 @@ WireGuard-endpoint sing-box нуждается в добавках **sagernet/wi | `amnezia-vpn/amneziawg-go` | ❌ | ✅ | | **этот форк** | ✅ | ✅ | +Каждый существующий форк даёт ровно **половину** нужного: + +- **возьмёшь `sagernet/wireguard-go`** → sing-box-lx соберётся и запустится, но AWG-поля (`jc`/`h1`/`i1`…) ничего не сделают → **обфускации нет**; AmneziaWG не работает; +- **возьмёшь `amnezia-vpn/amneziawg-go`** → обфускация есть, но sing-box-lx **не скомпилируется** (нет sagernet-функций). + +Нужны **обе** ✅ сразу, а готового форка с двумя галочками не существует — поэтому мы собрали его **слиянием**: sagernet (за API) + amnezia (за обфускацию). Это ровно строка **«этот форк»** выше. + Подход: **берём sagernet-базу и граффтим обфускацию на неё** — а не наоборот (не дотачиваем sagernet-API к amneziawg-go, иначе даже обычный WireGuard шёл бы через чужой device). Так sing-box компилируется без изменений, обфускация аддитивна и выключена по умолчанию, а конфиг без AWG-полей ведёт себя как обычный WireGuard. ## Как устроен merge From fb8d8d8fcaf39fc5e7e38526b5c95e3d923ad4b8 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:49:57 +0300 Subject: [PATCH 145/173] fix(010): never enable UDP_GRO on android (GRO split-brain killed download) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runtime.GOOS=="android" (not "linux"), so StdNetBind enabled UDP_GRO and read rxOffload=true, but the GRO receive dispatcher in bind_std.go is gated on GOOS=="linux" → dead on android. A coalesced GRO super-packet was read as one datagram, corrupting the WG transport stream → download died on no-detour WG-endpoint (detour path uses ClientBind, no offload, so it worked). Gate both the UDP_GRO setsockopt (controlfns_linux.go) and the rxOffload readback (features_linux.go) behind !android. TX/GSO untouched, so non-android linux performance is unchanged. Confirmed on device (CPH2411/Android-15: probe gave rxOffload=true + dispatch=single). See SPECS/010-B-O-WG_ENDPOINT_GRO_SPLIT_BRAIN. --- conn/controlfns_linux.go | 9 +++++++++ conn/features_linux.go | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index 752fbca..7602665 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -60,6 +60,15 @@ func init() { // Attempt to enable UDP_GRO func(network, address string, c syscall.RawConn) error { + // lx(010): skip UDP_GRO on android. The GRO receive path in bind_std.go + // is gated on runtime.GOOS=="linux", which is false on android — so a + // coalesced super-packet is never split and corrupts the WG stream + // (download dies). Belt-and-suspenders with the rxOffload guard in + // features_linux.go. TX/GSO untouched. + // See SPECS/010-B-O-WG_ENDPOINT_GRO_SPLIT_BRAIN. + if runtime.GOOS == "android" { + return nil + } c.Control(func(fd uintptr) { _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPGRO, 1) }) diff --git a/conn/features_linux.go b/conn/features_linux.go index 513202e..b74c398 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -7,6 +7,7 @@ package conn import ( "net" + "runtime" "golang.org/x/sys/unix" ) @@ -29,6 +30,15 @@ func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { return } txOffload = true + // lx(010): never advertise RX offload on android. runtime.GOOS=="android" + // (not "linux"), so the GRO receive dispatcher in bind_std.go (gated on + // GOOS=="linux") is dead there — a coalesced GRO super-packet would be read + // as one datagram and corrupt the WG transport stream, killing download. + // Confirmed on device (CPH2411/Android-15: rxOffload=true, dispatch=single). + // TX is left untouched. See SPECS/010-B-O-WG_ENDPOINT_GRO_SPLIT_BRAIN. + if runtime.GOOS == "android" { + return + } opt, errSyscall := unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPGRO) if errSyscall != nil { return From 6513629626537c5626bdd6c400481a728326b413 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:52:53 +0300 Subject: [PATCH 146/173] =?UTF-8?q?docs(010):=20spec=20folder=20O=E2=86=92?= =?UTF-8?q?C=20in=20fix=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conn/controlfns_linux.go | 2 +- conn/features_linux.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index 7602665..8c8b652 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -65,7 +65,7 @@ func init() { // coalesced super-packet is never split and corrupts the WG stream // (download dies). Belt-and-suspenders with the rxOffload guard in // features_linux.go. TX/GSO untouched. - // See SPECS/010-B-O-WG_ENDPOINT_GRO_SPLIT_BRAIN. + // See SPECS/010-B-C-WG_ENDPOINT_GRO_SPLIT_BRAIN. if runtime.GOOS == "android" { return nil } diff --git a/conn/features_linux.go b/conn/features_linux.go index b74c398..7d12890 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -35,7 +35,7 @@ func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { // GOOS=="linux") is dead there — a coalesced GRO super-packet would be read // as one datagram and corrupt the WG transport stream, killing download. // Confirmed on device (CPH2411/Android-15: rxOffload=true, dispatch=single). - // TX is left untouched. See SPECS/010-B-O-WG_ENDPOINT_GRO_SPLIT_BRAIN. + // TX is left untouched. See SPECS/010-B-C-WG_ENDPOINT_GRO_SPLIT_BRAIN. if runtime.GOOS == "android" { return } From 0c0c10b5d3236796bd3832a6813223d6dc7d0bb1 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:27:23 +0300 Subject: [PATCH 147/173] docs(010): use rename-stable SPECS folder ref in fix comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sing-box-lx is dropping type/status from SPEC folder names (NNN-T-S-NAME → NNN-NAME), so the -B-C- tail goes stale. Point at the new stable folder name SPECS/010-WG_ENDPOINT_GRO_SPLIT_BRAIN. Comment-only, no behavior change. --- conn/controlfns_linux.go | 2 +- conn/features_linux.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conn/controlfns_linux.go b/conn/controlfns_linux.go index 8c8b652..ff591f1 100644 --- a/conn/controlfns_linux.go +++ b/conn/controlfns_linux.go @@ -65,7 +65,7 @@ func init() { // coalesced super-packet is never split and corrupts the WG stream // (download dies). Belt-and-suspenders with the rxOffload guard in // features_linux.go. TX/GSO untouched. - // See SPECS/010-B-C-WG_ENDPOINT_GRO_SPLIT_BRAIN. + // See SPECS/010-WG_ENDPOINT_GRO_SPLIT_BRAIN. if runtime.GOOS == "android" { return nil } diff --git a/conn/features_linux.go b/conn/features_linux.go index 7d12890..69dd2f4 100644 --- a/conn/features_linux.go +++ b/conn/features_linux.go @@ -35,7 +35,7 @@ func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) { // GOOS=="linux") is dead there — a coalesced GRO super-packet would be read // as one datagram and corrupt the WG transport stream, killing download. // Confirmed on device (CPH2411/Android-15: rxOffload=true, dispatch=single). - // TX is left untouched. See SPECS/010-B-C-WG_ENDPOINT_GRO_SPLIT_BRAIN. + // TX is left untouched. See SPECS/010-WG_ENDPOINT_GRO_SPLIT_BRAIN. if runtime.GOOS == "android" { return } From 9de6dc32df775a70ba6e727ca4a9591a65296d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 14:17:42 +0800 Subject: [PATCH 148/173] Add batched InputPackets --- device/send.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/device/send.go b/device/send.go index 264e964..7a94068 100644 --- a/device/send.go +++ b/device/send.go @@ -329,6 +329,15 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { } elem := device.NewOutboundElement() packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] + var totalLength int + for _, packetSlice := range packetSlices { + totalLength += len(packetSlice) + } + if totalLength > len(packet) { + device.PutMessageBuffer(elem.buffer) + device.PutOutboundElement(elem) + return + } var n int for _, packetSlice := range packetSlices { n += copy(packet[n:], packetSlice) @@ -346,6 +355,58 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { } } +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.allowedips.Lookup(packetRef.Destination) + if peer == nil { + unmatched = append(unmatched, packetRef) + continue + } + elem := device.NewOutboundElement() + packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] + var totalLength int + for _, packetSlice := range packetRef.PacketSlices { + totalLength += len(packetSlice) + } + if totalLength > len(packet) { + device.PutMessageBuffer(elem.buffer) + device.PutOutboundElement(elem) + continue + } + var n int + for _, packetSlice := range packetRef.PacketSlices { + n += copy(packet[n:], packetSlice) + } + elem.packet = packet[:n] + elemsForPeer, ok := elemsByPeer[peer] + if !ok { + elemsForPeer = device.GetOutboundElementsContainer() + elemsByPeer[peer] = elemsForPeer + } + elemsForPeer.elems = append(elemsForPeer.elems, elem) + } + for peer, elemsForPeer := range elemsByPeer { + if peer.isRunning.Load() { + peer.StagePackets(elemsForPeer) + peer.SendStagedPackets() + } else { + for _, elem := range elemsForPeer.elems { + device.PutMessageBuffer(elem.buffer) + device.PutOutboundElement(elem) + } + device.PutOutboundElementsContainer(elemsForPeer) + } + } + return unmatched +} + func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { for { select { From 8403cdb937ee38ac5bb3dc9ae1cadca1c2562418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 21:05:53 +0800 Subject: [PATCH 149/173] 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. --- device/channels.go | 2 +- device/device.go | 8 +++--- device/peer.go | 3 ++ device/pools.go | 32 ++++++++++++++++------ device/send.go | 68 +++++++++++++++++++++++++++++++--------------- 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/device/channels.go b/device/channels.go index be15d1c..1eaec56 100644 --- a/device/channels.go +++ b/device/channels.go @@ -126,7 +126,7 @@ func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { case elemsContainer := <-q.c: elemsContainer.Lock() for _, elem := range elemsContainer.elems { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } device.PutOutboundElementsContainer(elemsContainer) diff --git a/device/device.go b/device/device.go index 8afa6fd..529951a 100644 --- a/device/device.go +++ b/device/device.go @@ -71,11 +71,11 @@ type Device struct { cookieChecker CookieChecker pool struct { - inboundElementsContainer *WaitPool - outboundElementsContainer *WaitPool + inboundElementsContainer *sync.Pool + outboundElementsContainer *sync.Pool messageBuffers *WaitPool - inboundElements *WaitPool - outboundElements *WaitPool + inboundElements *sync.Pool + outboundElements *sync.Pool } queue struct { diff --git a/device/peer.go b/device/peer.go index bca121d..a2703bd 100644 --- a/device/peer.go +++ b/device/peer.go @@ -25,6 +25,8 @@ type Peer struct { rxBytes atomic.Uint64 // bytes received from peer lastHandshakeNano atomic.Int64 // nano seconds since epoch + queuedOutboundPackets atomic.Int32 // packets in staged+outbound queues, for input backpressure + endpoint struct { sync.Mutex val conn.Endpoint @@ -193,6 +195,7 @@ func (peer *Peer) Start() { // reset routine state peer.stopping.Wait() peer.stopping.Add(2) + peer.queuedOutboundPackets.Store(0) peer.handshake.mutex.Lock() peer.handshake.lastSentHandshake = time.Now().Add(-(RekeyTimeout + time.Second)) diff --git a/device/pools.go b/device/pools.go index 2c18f41..b7536a3 100644 --- a/device/pools.go +++ b/device/pools.go @@ -7,6 +7,8 @@ package device import ( "sync" + + "github.com/sagernet/sing/common/buf" ) type WaitPool struct { @@ -47,23 +49,23 @@ func (p *WaitPool) Put(x any) { } 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()) 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()) return &QueueOutboundElementsContainer{elems: s} - }) + }} device.pool.messageBuffers = NewWaitPool(PreallocatedBuffersPerPool, func() any { return new([MaxMessageSize]byte) }) - device.pool.inboundElements = NewWaitPool(PreallocatedBuffersPerPool, func() any { + device.pool.inboundElements = &sync.Pool{New: func() any { return new(QueueInboundElement) - }) - device.pool.outboundElements = NewWaitPool(PreallocatedBuffersPerPool, func() any { + }} + device.pool.outboundElements = &sync.Pool{New: func() any { return new(QueueOutboundElement) - }) + }} } func (device *Device) GetInboundElementsContainer() *QueueInboundElementsContainer { @@ -102,6 +104,20 @@ func (device *Device) PutMessageBuffer(msg *[MaxMessageSize]byte) { 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 { return device.pool.inboundElements.Get().(*QueueInboundElement) } diff --git a/device/send.go b/device/send.go index 7a94068..bf4b85c 100644 --- a/device/send.go +++ b/device/send.go @@ -45,7 +45,7 @@ import ( */ type QueueOutboundElement struct { - buffer *[MaxMessageSize]byte // slice holding the packet data + buffer []byte // sing-allocated buffer holding the packet data // packet is always a slice of "buffer". The starting offset in buffer // is either: // a) MessageEncapsulatingTransportSize+MessageTransportHeaderSize (plaintext) @@ -63,7 +63,7 @@ type QueueOutboundElementsContainer struct { func (device *Device) NewOutboundElement() *QueueOutboundElement { elem := device.GetOutboundElement() - elem.buffer = device.GetMessageBuffer() + elem.buffer = device.GetOutboundBuffer(MaxMessageSize) elem.nonce = 0 // keypair and peer were cleared (if necessary) by clearPointers. return elem @@ -89,9 +89,10 @@ func (peer *Peer) SendKeepalive() { elemsContainer.elems = append(elemsContainer.elems, elem) select { case peer.queue.staged <- elemsContainer: + peer.queuedOutboundPackets.Add(1) peer.device.log.Verbosef("%v - Sending keepalive packet", peer) default: - peer.device.PutMessageBuffer(elem.buffer) + peer.device.PutOutboundBuffer(elem.buffer) peer.device.PutOutboundElement(elem) peer.device.PutOutboundElementsContainer(elemsContainer) } @@ -238,7 +239,7 @@ func (device *Device) RoutineReadFromTUN() { defer func() { for _, elem := range elems { if elem != nil { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } } @@ -295,7 +296,7 @@ func (device *Device) RoutineReadFromTUN() { peer.SendStagedPackets() } else { for _, elem := range elemsForPeer.elems { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } device.PutOutboundElementsContainer(elemsForPeer) @@ -322,22 +323,33 @@ 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) InputPacket(destination []byte, packetSlices [][]byte) { peer := device.allowedips.Lookup(destination) if peer == nil { return } - elem := device.NewOutboundElement() - packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] + if peer.queuedOutboundPackets.Load() >= maxQueuedInputPackets { + return + } var totalLength int for _, packetSlice := range packetSlices { totalLength += len(packetSlice) } - if totalLength > len(packet) { - device.PutMessageBuffer(elem.buffer) - device.PutOutboundElement(elem) + allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + 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) @@ -349,7 +361,7 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { peer.StagePackets(elemsForPeer) peer.SendStagedPackets() } else { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) device.PutOutboundElementsContainer(elemsForPeer) } @@ -369,17 +381,21 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef unmatched = append(unmatched, packetRef) continue } - elem := device.NewOutboundElement() - packet := elem.buffer[MessageEncapsulatingTransportSize+MessageTransportHeaderSize:] + if peer.queuedOutboundPackets.Load() >= maxQueuedInputPackets { + continue + } var totalLength int for _, packetSlice := range packetRef.PacketSlices { totalLength += len(packetSlice) } - if totalLength > len(packet) { - device.PutMessageBuffer(elem.buffer) - device.PutOutboundElement(elem) + allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + 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) @@ -398,7 +414,7 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef peer.SendStagedPackets() } else { for _, elem := range elemsForPeer.elems { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } device.PutOutboundElementsContainer(elemsForPeer) @@ -408,6 +424,7 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef } func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { + peer.queuedOutboundPackets.Add(int32(len(elems.elems))) for { select { case peer.queue.staged <- elems: @@ -416,8 +433,9 @@ func (peer *Peer) StagePackets(elems *QueueOutboundElementsContainer) { } select { case tooOld := <-peer.queue.staged: + peer.queuedOutboundPackets.Add(-int32(len(tooOld.elems))) for _, elem := range tooOld.elems { - peer.device.PutMessageBuffer(elem.buffer) + peer.device.PutOutboundBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } peer.device.PutOutboundElementsContainer(tooOld) @@ -464,6 +482,8 @@ top: elemsContainer.elems = elemsContainer.elems[:i] 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 } @@ -477,8 +497,9 @@ top: peer.queue.outbound.c <- elemsContainer peer.device.queue.encryption.c <- elemsContainer } else { + peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems))) for _, elem := range elemsContainer.elems { - peer.device.PutMessageBuffer(elem.buffer) + peer.device.PutOutboundBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } peer.device.PutOutboundElementsContainer(elemsContainer) @@ -497,8 +518,9 @@ func (peer *Peer) FlushStagedPackets() { for { select { case elemsContainer := <-peer.queue.staged: + peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems))) for _, elem := range elemsContainer.elems { - peer.device.PutMessageBuffer(elem.buffer) + peer.device.PutOutboundBuffer(elem.buffer) peer.device.PutOutboundElement(elem) } peer.device.PutOutboundElementsContainer(elemsContainer) @@ -592,8 +614,9 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { // TODO: rework peer shutdown order to ensure // that we never accidentally keep timers alive longer than necessary. elemsContainer.Lock() + peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems))) for _, elem := range elemsContainer.elems { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } device.PutOutboundElementsContainer(elemsContainer) @@ -615,8 +638,9 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { if dataSent { peer.timersDataSent() } + peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems))) for _, elem := range elemsContainer.elems { - device.PutMessageBuffer(elem.buffer) + device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) } device.PutOutboundElementsContainer(elemsContainer) From fcbb7c473b483653e279c4f86250bd48efa855e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 21:06:45 +0800 Subject: [PATCH 150/173] 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. --- conn/bind_std.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 4dadb0d..3ff0408 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -228,8 +228,11 @@ again: func (s *StdNetBind) putMessages(msgs *[]ipv6.Message) { for i := range *msgs { - (*msgs)[i].OOB = (*msgs)[i].OOB[:0] - (*msgs)[i] = ipv6.Message{Buffers: (*msgs)[i].Buffers, OOB: (*msgs)[i].OOB} + buffers := (*msgs)[i].Buffers + for j := range buffers { + buffers[j] = nil + } + (*msgs)[i] = ipv6.Message{Buffers: buffers[:1], OOB: (*msgs)[i].OOB[:0]} } s.msgsPool.Put(msgs) } @@ -491,6 +494,7 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, offs var ( base = -1 // index of msg we are currently coalescing into 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] endBatch bool // tracking flag to start a new batch on next iteration of bufs ) @@ -502,14 +506,14 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, offs buf = buf[offset:] if i > 0 { msgLen := len(buf) - baseLenBefore := len(msgs[base].Buffers[0]) - freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore - if msgLen+baseLenBefore <= maxPayloadLen && + if msgLen+totalLen <= maxPayloadLen && msgLen <= gsoSize && - msgLen <= freeBaseCap && dgramCnt < udpSegmentMaxDatagrams && !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 { setGSO(&msgs[base].OOB, uint16(gsoSize)) } @@ -530,8 +534,9 @@ func coalesceMessages(addr *net.UDPAddr, ep *StdNetEndpoint, bufs [][]byte, offs endBatch = false base++ gsoSize = len(buf) + totalLen = len(buf) setSrcControl(&msgs[base].OOB, ep) - msgs[base].Buffers[0] = buf + msgs[base].Buffers = append(msgs[base].Buffers[:0], buf) msgs[base].Addr = addr dgramCnt = 1 } From 57baac9504a8461a6f8fdabefb3c1dc2a1194ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 21:06:55 +0800 Subject: [PATCH 151/173] 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. --- conn/bind_std.go | 48 ++++++- conn/msgx_darwin.go | 325 +++++++++++++++++++++++++++++++++++++++++++ conn/msgx_default.go | 30 ++++ 3 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 conn/msgx_darwin.go create mode 100644 conn/msgx_default.go diff --git a/conn/bind_std.go b/conn/bind_std.go index 3ff0408..2ecbd11 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -39,6 +39,8 @@ type StdNetBind struct { ipv6 *net.UDPConn ipv4PC *ipv4.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 ipv4RxOffload bool ipv6TxOffload bool @@ -48,6 +50,8 @@ type StdNetBind struct { udpAddrPool sync.Pool msgsPool sync.Pool + msgx msgXState + blackhole4 bool blackhole6 bool } @@ -175,6 +179,7 @@ func (s *StdNetBind) Open(uport uint16) ([]ReceiveFunc, uint16, error) { if s.ipv4 != nil || s.ipv6 != nil { return nil, 0, ErrBindAlreadyOpen } + s.msgx.reset() // Attempt to open ipv4 and ipv6 listeners on the same port. // If uport is 0, we can retry on failure. @@ -207,7 +212,22 @@ again: v4pc = ipv4.NewPacketConn(v4conn) 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 } if v6conn != nil { @@ -216,7 +236,22 @@ again: v6pc = ipv6.NewPacketConn(v6conn) 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 } if len(fns) == 0 { @@ -326,6 +361,9 @@ func (s *StdNetBind) BatchSize() int { if runtime.GOOS == "linux" || runtime.GOOS == "android" { return IdealBatchSize } + if supportsMsgX { + return msgXBatchSize + } return 1 } @@ -467,6 +505,12 @@ func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message start += n } } else { + if supportsMsgX { + handled, sendErr := s.sendMsgX(conn, msgs) + if handled { + return sendErr + } + } for _, msg := range msgs { _, _, err = conn.WriteMsgUDP(msg.Buffers[0], msg.OOB, msg.Addr.(*net.UDPAddr)) if err != nil { diff --git a/conn/msgx_darwin.go b/conn/msgx_darwin.go new file mode 100644 index 0000000..138c8a8 --- /dev/null +++ b/conn/msgx_darwin.go @@ -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 { + 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 { + 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 +} diff --git a/conn/msgx_default.go b/conn/msgx_default.go new file mode 100644 index 0000000..6fffdd5 --- /dev/null +++ b/conn/msgx_default.go @@ -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") +} From 2c27bbf4f97f4d29cc2dbe1abd1a66e8403a6866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 23:38:46 +0800 Subject: [PATCH 152/173] FIx batched InputPackets --- conn/bind_std.go | 7 +++++++ device/send.go | 27 ++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index 2ecbd11..c437da3 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -408,6 +408,13 @@ func (e ErrUDPGSODisabled) Unwrap() 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:] + } s.mu.Lock() blackhole := s.blackhole4 conn := s.ipv4 diff --git a/device/send.go b/device/send.go index bf4b85c..add170b 100644 --- a/device/send.go +++ b/device/send.go @@ -374,7 +374,7 @@ type InputPacketRef struct { func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef { var unmatched []*InputPacketRef - elemsByPeer := make(map[*Peer]*QueueOutboundElementsContainer, len(packets)) + elemsByPeer := make(map[*Peer][]*QueueOutboundElementsContainer, len(packets)) for _, packetRef := range packets { peer := device.allowedips.Lookup(packetRef.Destination) if peer == nil { @@ -401,23 +401,28 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef n += copy(packet[n:], packetSlice) } elem.packet = packet[:n] - elemsForPeer, ok := elemsByPeer[peer] - if !ok { - elemsForPeer = device.GetOutboundElementsContainer() - elemsByPeer[peer] = elemsForPeer + 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, elemsForPeer := range elemsByPeer { + for peer, containers := range elemsByPeer { if peer.isRunning.Load() { - peer.StagePackets(elemsForPeer) + for _, elemsForPeer := range containers { + peer.StagePackets(elemsForPeer) + } peer.SendStagedPackets() } else { - for _, elem := range elemsForPeer.elems { - device.PutOutboundBuffer(elem.buffer) - device.PutOutboundElement(elem) + for _, elemsForPeer := range containers { + for _, elem := range elemsForPeer.elems { + device.PutOutboundBuffer(elem.buffer) + device.PutOutboundElement(elem) + } + device.PutOutboundElementsContainer(elemsForPeer) } - device.PutOutboundElementsContainer(elemsForPeer) } } return unmatched From 6f5e8b1947aed69ad09d6f8b47f815e2f520573b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Jul 2026 10:40:26 +0800 Subject: [PATCH 153/173] Add EgressProvider --- conn/bind_std.go | 56 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index c437da3..0a15de0 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -23,6 +23,12 @@ import ( "golang.org/x/net/ipv6" ) +type EgressProvider interface { + 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 @@ -32,6 +38,7 @@ var _ Bind = (*StdNetBind)(nil) // proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564. type StdNetBind struct { externalControl control.Func + egressProvider EgressProvider reservedForEndpoint map[netip.AddrPort][3]uint8 mu sync.Mutex // protects all fields except as specified @@ -257,10 +264,29 @@ again: if len(fns) == 0 { 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 { + common.ClearArray(bufs[0][1:4]) + } + endpoints[0] = &StdNetEndpoint{AddrPort: source} + return 1, nil + }) + } return fns, uint16(port), nil } +func (s *StdNetBind) SetEgressProvider(provider EgressProvider) { + s.egressProvider = provider +} + func (s *StdNetBind) putMessages(msgs *[]ipv6.Message) { for i := range *msgs { buffers := (*msgs)[i].Buffers @@ -371,6 +397,9 @@ func (s *StdNetBind) Close() error { s.mu.Lock() defer s.mu.Unlock() + if s.egressProvider != nil { + s.egressProvider.SetEgressPort(0) + } var err1, err2 error if s.ipv4 != nil { err1 = s.ipv4.Close() @@ -415,13 +444,14 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { } bufs = bufs[IdealBatchSize:] } + standardEndpoint := endpoint.(*StdNetEndpoint) s.mu.Lock() blackhole := s.blackhole4 conn := s.ipv4 offload := s.ipv4TxOffload br := batchWriter(s.ipv4PC) is6 := false - if endpoint.DstIP().Is6() { + if standardEndpoint.DstIP().Is6() { blackhole = s.blackhole6 conn = s.ipv6 br = s.ipv6PC @@ -442,30 +472,42 @@ func (s *StdNetBind) Send(bufs [][]byte, endpoint Endpoint, offset int) error { ua := s.udpAddrPool.Get().(*net.UDPAddr) defer s.udpAddrPool.Put(ua) if is6 { - as16 := endpoint.DstIP().As16() + as16 := standardEndpoint.DstIP().As16() copy(ua.IP, as16[:]) ua.IP = ua.IP[:16] } else { - as4 := endpoint.DstIP().As4() + as4 := standardEndpoint.DstIP().As4() copy(ua.IP, as4[:]) ua.IP = ua.IP[:4] } - ua.Port = int(endpoint.(*StdNetEndpoint).Port()) + ua.Port = int(standardEndpoint.Port()) var ( retried bool err error ) for _, buf := range bufs { if len(buf) > offset+3 { - reserved, loaded := s.reservedForEndpoint[endpoint.(*StdNetEndpoint).AddrPort] + 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: if offload { - n := coalesceMessages(ua, endpoint.(*StdNetEndpoint), bufs, offset, *msgs, setGSOSize) + n := coalesceMessages(ua, standardEndpoint, bufs, offset, *msgs, setGSOSize) err = s.send(conn, br, (*msgs)[:n]) if err != nil && offload && errShouldDisableUDPGSO(err) { offload = false @@ -483,7 +525,7 @@ retry: for i := range bufs { (*msgs)[i].Addr = ua (*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)]) } From 70b09a6edd3bf037b06501553674146fb244b257 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 21 Nov 2025 09:42:42 -0800 Subject: [PATCH 154/173] device: put AllowedIPs mutex before what it guards, unexport fields Signed-off-by: Brad Fitzpatrick --- device/allowedips.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/device/allowedips.go b/device/allowedips.go index d15373c..84fbd90 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -205,14 +205,14 @@ func (node *trieEntry) lookup(ip []byte) *Peer { } type AllowedIPs struct { - IPv4 *trieEntry - IPv6 *trieEntry - mutex sync.RWMutex + mu sync.RWMutex + ipv4 *trieEntry + ipv6 *trieEntry } func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix) bool) { - table.mutex.RLock() - defer table.mutex.RUnlock() + table.mu.RLock() + defer table.mu.RUnlock() for elem := peer.trieEntries.Front(); elem != nil; elem = elem.Next() { node := elem.Value.(*trieEntry) @@ -278,8 +278,8 @@ func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) { } func (table *AllowedIPs) RemoveByPeer(peer *Peer) { - table.mutex.Lock() - defer table.mutex.Unlock() + table.mu.Lock() + defer table.mu.Unlock() var next *list.Element for elem := peer.trieEntries.Front(); elem != nil; elem = next { @@ -289,28 +289,28 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) { } func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) { - table.mutex.Lock() - defer table.mutex.Unlock() + table.mu.Lock() + defer table.mu.Unlock() if prefix.Addr().Is6() { 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() { 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 { panic(errors.New("inserting unknown address type")) } } func (table *AllowedIPs) Lookup(ip []byte) *Peer { - table.mutex.RLock() - defer table.mutex.RUnlock() + table.mu.RLock() + defer table.mu.RUnlock() switch len(ip) { case net.IPv6len: - return table.IPv6.lookup(ip) + return table.ipv6.lookup(ip) case net.IPv4len: - return table.IPv4.lookup(ip) + return table.ipv4.lookup(ip) default: panic(errors.New("looking up unknown address type")) } From e924a91e998843e753999047e1a9785290596030 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 12 Nov 2025 13:04:17 -0800 Subject: [PATCH 155/173] device: add API for on-demand configuration of peers Updates tailscale/tailscale#17858 Signed-off-by: Brad Fitzpatrick --- device/allowedips.go | 29 +++++++++++--- device/device.go | 92 +++++++++++++++++++++++++++++++++++++++++++- device/peer.go | 30 ++++++++++++++- device/timers.go | 8 ++++ go.mod | 2 +- 5 files changed, 151 insertions(+), 10 deletions(-) diff --git a/device/allowedips.go b/device/allowedips.go index 84fbd90..05081ac 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -257,17 +257,17 @@ func (node *trieEntry) remove() { } func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) { - table.mutex.Lock() - defer table.mutex.Unlock() + 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())) + 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())) + node, exact = table.ipv4.nodePlacement(ip[:], uint8(prefix.Bits())) } else { panic(errors.New("removing unknown address type")) } @@ -277,10 +277,26 @@ func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) { node.remove() } -func (table *AllowedIPs) RemoveByPeer(peer *Peer) { + +// 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 for elem := peer.trieEntries.Front(); elem != nil; elem = next { next = elem.Next() @@ -291,7 +307,10 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) { func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) { table.mu.Lock() defer table.mu.Unlock() + table.insertLocked(prefix, peer) +} +func (table *AllowedIPs) insertLocked(prefix netip.Prefix, peer *Peer) { if prefix.Addr().Is6() { ip := prefix.Addr().As16() parentIndirection{&table.ipv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer) diff --git a/device/device.go b/device/device.go index 529951a..1a05490 100644 --- a/device/device.go +++ b/device/device.go @@ -7,6 +7,8 @@ package device import ( "context" + "errors" + "net/netip" "runtime" "sync" "sync/atomic" @@ -59,6 +61,7 @@ type Device struct { peers struct { sync.RWMutex // protects keyMap keyMap map[NoisePublicKey]*Peer + lookupFunc PeerLookupFunc // or nil if unused } rate struct { @@ -345,13 +348,63 @@ func (device *Device) BatchSize() int { 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 { 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] + allowedIPs := lookupFunc(pk) + if allowedIPs == 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(allowedIPs) + p.deleteOnIdle = true + 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) { device.peers.Lock() defer device.peers.Unlock() @@ -374,6 +427,41 @@ func (device *Device) RemoveAllPeers() { 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 +} + +// 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) (allowedIPs []netip.Prefix) + +// 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 +} + func (device *Device) Close() { device.state.Lock() defer device.state.Unlock() diff --git a/device/peer.go b/device/peer.go index a2703bd..875d6ba 100644 --- a/device/peer.go +++ b/device/peer.go @@ -8,6 +8,8 @@ package device import ( "container/list" "errors" + "net/netip" + "slices" "sync" "sync/atomic" "time" @@ -27,6 +29,12 @@ type Peer struct { 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 { sync.Mutex val conn.Endpoint @@ -46,7 +54,9 @@ type Peer struct { } state struct { - sync.Mutex // protects against concurrent Start/Stop + sync.Mutex // protects against concurrent Start/Stop, and fields below + + allowedIPs []netip.Prefix } queue struct { @@ -89,7 +99,7 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { // map public key _, ok := device.peers.keyMap[pk] if ok { - return nil, errors.New("adding existing peer") + return nil, errAddExistingPeer } // pre-compute DH @@ -115,6 +125,22 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) { 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) + p.state.allowedIPs = slices.Clone(allowedIPs) // avoid retaining caller's slice +} + // SendBuffers sends buffers to peer. WireGuard packet data in each element of // buffers must be preceded by MessageEncapsulatingTransportSize number of // bytes. diff --git a/device/timers.go b/device/timers.go index 80fb7d9..97ed451 100644 --- a/device/timers.go +++ b/device/timers.go @@ -129,6 +129,14 @@ func expiredNewHandshake(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.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 expiredPersistentKeepalive(peer *Peer) { diff --git a/go.mod b/go.mod index 773bf00..d445678 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sagernet/wireguard-go -go 1.20 +go 1.25 require ( github.com/sagernet/sing v0.7.10 From f69b24781e6fd8b404db1e8c2e49b6f7edfac10c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 26 Nov 2025 13:10:21 -0800 Subject: [PATCH 156/173] device: further add, revise API for on-demand configuration of peers Updates tailscale/tailscale#17858 Updates tailscale/corp#35603 Signed-off-by: Brad Fitzpatrick --- device/allowedips.go | 157 ++++++++++++++++++++++++++++++++++++++++++- device/device.go | 45 +++++++++++-- device/peer.go | 12 +++- device/receive.go | 7 +- device/send.go | 11 +-- 5 files changed, 220 insertions(+), 12 deletions(-) diff --git a/device/allowedips.go b/device/allowedips.go index 05081ac..8724802 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -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() { 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 size := uint8(len(ip)) for node != nil && commonBits(node.bits, ip) >= node.cidr { @@ -208,6 +257,9 @@ type AllowedIPs struct { mu sync.RWMutex ipv4 *trieEntry 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) { @@ -322,9 +374,53 @@ func (table *AllowedIPs) insertLocked(prefix netip.Prefix, peer *Peer) { } } +// LookupFromPacket looks up the peer to which an outbound IP packet should be +// sent. It lives on [AllowedIPs] for legacy/structural reasons: historically +// WireGuard's only peer-selection mechanism was the AllowedIPs trie, and the +// send path already had a reference to the table. When a [PeerByIPPacketFunc] +// has been registered via [Device.SetPeerByIPPacketFunc], that callback is used +// instead of the trie and the AllowedIPs table is not consulted at all. +// +// 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: + 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) @@ -334,3 +430,62 @@ func (table *AllowedIPs) Lookup(ip []byte) *Peer { 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 + } + } +} diff --git a/device/device.go b/device/device.go index 1a05490..4e7950b 100644 --- a/device/device.go +++ b/device/device.go @@ -368,8 +368,8 @@ func (device *Device) LookupPeer(pk NoisePublicKey) *Peer { return p } - allowedIPs := lookupFunc(pk) - if allowedIPs == nil { + conf, ok := lookupFunc(pk) + if !ok || conf == nil { return nil } @@ -383,8 +383,11 @@ func (device *Device) LookupPeer(pk NoisePublicKey) *Peer { device.log.Errorf("Failed to create peer: %v", err) return nil } - p.SetAllowedIPs(allowedIPs) + p.SetAllowedIPs(conf.AllowedIPs) p.deleteOnIdle = true + if conf.Endpoint != nil { + p.SetEndpointFromPacket(conf.Endpoint) + } p.Start() return p } @@ -443,6 +446,17 @@ func (device *Device) RemoveMatchingPeers(shouldRemove func(NoisePublicKey) bool 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. // @@ -452,7 +466,21 @@ func (device *Device) RemoveMatchingPeers(shouldRemove func(NoisePublicKey) bool // with the provided allowed IPs. // // See [Device.SetPeerLookupFunc] and [Device.LookupPeer]. -type PeerLookupFunc func(NoisePublicKey) (allowedIPs []netip.Prefix) +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) // SetPeerLookupFunc sets the function used to look up peers by public key // when receiving packets for unknown peers. @@ -462,6 +490,15 @@ func (device *Device) SetPeerLookupFunc(f PeerLookupFunc) { 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 +} + func (device *Device) Close() { device.state.Lock() defer device.state.Unlock() diff --git a/device/peer.go b/device/peer.go index 875d6ba..b9d45a4 100644 --- a/device/peer.go +++ b/device/peer.go @@ -57,6 +57,11 @@ type Peer struct { 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 { @@ -138,7 +143,12 @@ func (p *Peer) SetAllowedIPs(allowedIPs []netip.Prefix) { return } p.device.allowedips.setPeerPrefixes(p, allowedIPs) - p.state.allowedIPs = slices.Clone(allowedIPs) // avoid retaining caller's slice + + 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 diff --git a/device/receive.go b/device/receive.go index e13c987..b6edc56 100644 --- a/device/receive.go +++ b/device/receive.go @@ -9,6 +9,7 @@ import ( "encoding/binary" "errors" "net" + "net/netip" "sync" "time" @@ -482,7 +483,8 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { } elem.packet = elem.packet[:length] src := elem.packet[IPv4offsetSrc : IPv4offsetSrc+net.IPv4len] - if device.allowedips.Lookup(src) != peer { + srcAddr, _ := netip.AddrFromSlice(src) + if !peer.AllowedPeerSourceIP(srcAddr) { device.log.Verbosef("IPv4 packet with disallowed source address from %v", peer) continue } @@ -499,7 +501,8 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { } elem.packet = elem.packet[:length] src := elem.packet[IPv6offsetSrc : IPv6offsetSrc+net.IPv6len] - if device.allowedips.Lookup(src) != peer { + srcAddr, _ := netip.AddrFromSlice(src) + if !peer.AllowedPeerSourceIP(srcAddr) { device.log.Verbosef("IPv6 packet with disallowed source address from %v", peer) continue } diff --git a/device/send.go b/device/send.go index add170b..6bd1ec9 100644 --- a/device/send.go +++ b/device/send.go @@ -9,6 +9,7 @@ import ( "encoding/binary" "errors" "net" + "net/netip" "os" "sync" "time" @@ -263,15 +264,17 @@ func (device *Device) RoutineReadFromTUN() { if len(elem.packet) < ipv4.HeaderLen { continue } - dst := elem.packet[IPv4offsetDst : IPv4offsetDst+net.IPv4len] - peer = device.allowedips.Lookup(dst) + src := netip.AddrFrom4([4]byte(elem.packet[IPv4offsetSrc : IPv4offsetSrc+net.IPv4len])) + dst := netip.AddrFrom4([4]byte(elem.packet[IPv4offsetDst : IPv4offsetDst+net.IPv4len])) + peer = device.allowedips.LookupFromPacket(src, dst, elem.packet) case 6: if len(elem.packet) < ipv6.HeaderLen { continue } - dst := elem.packet[IPv6offsetDst : IPv6offsetDst+net.IPv6len] - peer = device.allowedips.Lookup(dst) + src := netip.AddrFrom16([16]byte(elem.packet[IPv6offsetSrc : IPv6offsetSrc+net.IPv6len])) + dst := netip.AddrFrom16([16]byte(elem.packet[IPv6offsetDst : IPv6offsetDst+net.IPv6len])) + peer = device.allowedips.LookupFromPacket(src, dst, elem.packet) default: device.log.Verbosef("Received packet with unknown IP version") From 010dd5c6f2d3b43b9249e0a5158595c1901d6bbf Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 24 Apr 2026 20:49:35 +0000 Subject: [PATCH 157/173] 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 --- device/device.go | 21 +++++++++++++++++++-- device/lock-ordering.md | 27 +++++++++++++++++++++++++++ device/noise-protocol.go | 26 +++++++++++++++++--------- 3 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 device/lock-ordering.md diff --git a/device/device.go b/device/device.go index 4e7950b..2368383 100644 --- a/device/device.go +++ b/device/device.go @@ -186,14 +186,22 @@ func (device *Device) upLocked() error { device.ipcMutex.Lock() 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() + peers := make([]*Peer, 0, len(device.peers.keyMap)) for _, peer := range device.peers.keyMap { + peers = append(peers, peer) + } + device.peers.RUnlock() + for _, peer := range peers { peer.Start() if peer.persistentKeepaliveInterval.Load() > 0 { peer.SendKeepalive() } } - device.peers.RUnlock() return nil } @@ -540,16 +548,25 @@ func (device *Device) SendKeepalivesToPeersWithCurrentKeypair() { 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() for _, peer := range device.peers.keyMap { peer.keypairs.RLock() sendKeepalive := peer.keypairs.current != nil && !peer.keypairs.current.created.Add(RejectAfterTime).Before(time.Now()) peer.keypairs.RUnlock() if sendKeepalive { - peer.SendKeepalive() + peers = append(peers, peer) } } device.peers.RUnlock() + for _, peer := range peers { + peer.SendKeepalive() + } } // closeBindLocked closes the device's net.bind. diff --git a/device/lock-ordering.md b/device/lock-ordering.md new file mode 100644 index 0000000..55a15c0 --- /dev/null +++ b/device/lock-ordering.md @@ -0,0 +1,27 @@ +# Lock Ordering in wireguard-go/device + +## Lock hierarchy + +Locks must be acquired in the order listed below. A goroutine holding a +lock with a higher number must never attempt to acquire a lock with a +lower number. + +``` +Level 0 device.state.Mutex +Level 1 device.ipcMutex (sync.RWMutex) +Level 2 device.net.RWMutex +Level 3 device.staticIdentity.RWMutex +Level 4 device.peers.RWMutex +Level 5 peer.state.Mutex +Level 6 peer.handshake.mutex (sync.RWMutex) +Level 7 peer.keypairs.RWMutex +Level 8 device.allowedips.mu (sync.RWMutex) +Level 9 device.indexTable.RWMutex +Level 10 peer.endpoint.Mutex +Level 11 device.cookieChecker.RWMutex +Level 12 peer.cookieGenerator.RWMutex +Level 13 Timer.modifyingLock / Timer.runningLock +``` + +Not every pair of locks appears in practice; the ordering above is the +transitive closure of the pairs that do. diff --git a/device/noise-protocol.go b/device/noise-protocol.go index ed4d82a..d72bb25 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -348,17 +348,22 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation, endpoint 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() - 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[:]) mixKey(&chainKey, &InitialChainKey, msg.Ephemeral[:]) // decrypt static key var peerPK NoisePublicKey var key [chacha20poly1305.KeySize]byte - ss, err := device.staticIdentity.privateKey.sharedSecret(msg.Ephemeral) + ss, err := privateKey.sharedSecret(msg.Ephemeral) if err != nil { return nil } @@ -533,6 +538,14 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { 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 { // lock handshake state @@ -543,11 +556,6 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { return false } - // lock private key for reading - - device.staticIdentity.RLock() - defer device.staticIdentity.RUnlock() - // finish 3-way DH mixHash(&hash, &handshake.hash, msg.Ephemeral[:]) @@ -560,7 +568,7 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer { mixKey(&chainKey, &chainKey, ss[:]) setZero(ss[:]) - ss, err = device.staticIdentity.privateKey.sharedSecret(msg.Ephemeral) + ss, err = privateKey.sharedSecret(msg.Ephemeral) if err != nil { return false } From 09268b375cbbe076ebccf64decb2b8ee304c6957 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 3 Jun 2026 16:09:03 +0000 Subject: [PATCH 158/173] 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 --- device/channels.go | 16 ++++++++++++++-- device/pools.go | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/device/channels.go b/device/channels.go index 1eaec56..45b2a76 100644 --- a/device/channels.go +++ b/device/channels.go @@ -83,10 +83,16 @@ func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue { q := &autodrainingInboundQueue{ c: make(chan *QueueInboundElementsContainer, QueueInboundSize), } - runtime.SetFinalizer(q, device.flushInboundQueue) + if device.needsInboundQueueFinalizer() { + runtime.SetFinalizer(q, device.flushInboundQueue) + } return q } +func (device *Device) needsInboundQueueFinalizer() bool { + return device.pool.messageBuffers.hasAccounting() +} + func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { for { select { @@ -116,10 +122,16 @@ func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue { q := &autodrainingOutboundQueue{ c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize), } - runtime.SetFinalizer(q, device.flushOutboundQueue) + if device.needsOutboundQueueFinalizer() { + runtime.SetFinalizer(q, device.flushOutboundQueue) + } return q } +func (device *Device) needsOutboundQueueFinalizer() bool { + return device.pool.messageBuffers.hasAccounting() +} + func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { for { select { diff --git a/device/pools.go b/device/pools.go index b7536a3..173486e 100644 --- a/device/pools.go +++ b/device/pools.go @@ -25,6 +25,10 @@ func NewWaitPool(max uint32, new func() any) *WaitPool { return p } +func (p *WaitPool) hasAccounting() bool { + return p != nil && p.max != 0 +} + func (p *WaitPool) Get() any { if p.max != 0 { p.lock.Lock() From 7c3a736cbe1f365d082cf13235ca3fc76a01a282 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Jun 2026 21:19:50 +0000 Subject: [PATCH 159/173] device: add peer session state callback Add a minimal callback API for observing WireGuard peer session state changes. Updates tailscale/corp#42874 --- device/device.go | 45 +++++++++++++++++++++++++++++ device/peer.go | 73 ++++++++++++++++++++++++++++++++++++++++++------ device/timers.go | 16 +++++++++++ 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/device/device.go b/device/device.go index 2368383..0cb5735 100644 --- a/device/device.go +++ b/device/device.go @@ -64,6 +64,11 @@ type Device struct { lookupFunc PeerLookupFunc // or nil if unused } + sessionState struct { + sync.Mutex // serializes PeerSessionStateFunc calls and protects peer.sessionState + fn PeerSessionStateFunc + } + rate struct { underLoadUntil atomic.Int64 limiter ratelimiter.Ratelimiter @@ -490,6 +495,34 @@ type PeerLookupFunc func(NoisePublicKey) (_ *NewPeerConfig, ok bool) // 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 Device and delivered in 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) { @@ -507,6 +540,18 @@ func (device *Device) SetPeerByIPPacketFunc(f PeerByIPPacketFunc) { 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. +func (device *Device) SetSessionStateFunc(f PeerSessionStateFunc) { + device.sessionState.Lock() + defer device.sessionState.Unlock() + device.sessionState.fn = f +} + func (device *Device) Close() { device.state.Lock() defer device.state.Unlock() diff --git a/device/peer.go b/device/peer.go index b9d45a4..4f90144 100644 --- a/device/peer.go +++ b/device/peer.go @@ -18,14 +18,16 @@ import ( ) type Peer struct { - isRunning atomic.Bool - keypairs Keypairs - handshake Handshake - device *Device - stopping sync.WaitGroup // routines pending stop - txBytes atomic.Uint64 // bytes send to peer (endpoint) - rxBytes atomic.Uint64 // bytes received from peer - lastHandshakeNano atomic.Int64 // nano seconds since epoch + isRunning atomic.Bool + keypairs Keypairs + handshake Handshake + device *Device + stopping sync.WaitGroup // routines pending stop + txBytes atomic.Uint64 // bytes send to peer (endpoint) + rxBytes atomic.Uint64 // bytes received from peer + lastHandshakeNano atomic.Int64 // nano seconds since epoch + sessionExpiresNano atomic.Int64 // nano seconds since epoch + sessionState PeerSessionState // guarded by device.sessionState.Mutex queuedOutboundPackets atomic.Int32 // packets in staged+outbound queues, for input backpressure @@ -46,6 +48,7 @@ type Peer struct { retransmitHandshake *Timer sendKeepalive *Timer newHandshake *Timer + sessionExpired *Timer zeroKeyMaterial *Timer persistentKeepalive *Timer handshakeAttempts atomic.Uint32 @@ -255,6 +258,10 @@ func (peer *Peer) Start() { func (peer *Peer) ZeroAndFlushAll() { device := peer.device + peer.sessionExpiresNano.Store(0) + if peer.timers.sessionExpired != nil { + peer.timers.sessionExpired.Del() + } // clear key pairs @@ -277,6 +284,7 @@ func (peer *Peer) ZeroAndFlushAll() { handshake.mutex.Unlock() peer.FlushStagedPackets() + peer.noteSessionState(PeerSessionNone) } func (peer *Peer) ExpireCurrentKeypairs() { @@ -296,6 +304,9 @@ func (peer *Peer) ExpireCurrentKeypairs() { next.sendNonce.Store(RejectAfterMessages) } keypairs.Unlock() + + peer.sessionExpiresNano.Store(0) + peer.noteSessionState(PeerSessionExpired) } func (peer *Peer) Stop() { @@ -318,6 +329,52 @@ func (peer *Peer) Stop() { peer.ZeroAndFlushAll() } +func (peer *Peer) noteSessionState(state PeerSessionState) { + device := peer.device + device.sessionState.Lock() + defer device.sessionState.Unlock() + + if peer.sessionState == state { + return + } + peer.sessionState = state + if f := device.sessionState.fn; f != nil { + f(peer.handshake.remoteStatic, state) + } +} + +func (peer *Peer) noteSessionHandshakeStarted() { + device := peer.device + device.sessionState.Lock() + defer device.sessionState.Unlock() + + switch peer.sessionState { + case PeerSessionEstablished: + return + case PeerSessionHandshake: + return + } + peer.sessionState = PeerSessionHandshake + if f := device.sessionState.fn; f != nil { + f(peer.handshake.remoteStatic, PeerSessionHandshake) + } +} + +func (peer *Peer) noteSessionHandshakeStopped() { + state := PeerSessionNone + if peer.hasKeyMaterial() { + state = PeerSessionExpired + } + peer.noteSessionState(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) { peer.endpoint.Lock() defer peer.endpoint.Unlock() diff --git a/device/timers.go b/device/timers.go index 97ed451..affa792 100644 --- a/device/timers.go +++ b/device/timers.go @@ -98,6 +98,7 @@ func expiredRetransmitHandshake(peer *Peer) { if peer.timersActive() && !peer.timers.zeroKeyMaterial.IsPending() { peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3) } + peer.noteSessionHandshakeStopped() } else { 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) @@ -139,6 +140,15 @@ func expiredZeroKeyMaterial(peer *Peer) { } } +func expiredSession(peer *Peer) { + expires := peer.sessionExpiresNano.Load() + if expires == 0 || time.Now().UnixNano() < expires { + return + } + peer.device.log.Verbosef("%s - Session expired after %d seconds", peer, int(RejectAfterTime.Seconds())) + peer.noteSessionState(PeerSessionExpired) +} + func expiredPersistentKeepalive(peer *Peer) { if peer.persistentKeepaliveInterval.Load() > 0 { peer.SendKeepalive() @@ -182,6 +192,7 @@ func (peer *Peer) timersHandshakeInitiated() { if peer.timersActive() { 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. */ @@ -197,8 +208,11 @@ 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. */ func (peer *Peer) timersSessionDerived() { if peer.timersActive() { + peer.sessionExpiresNano.Store(time.Now().Add(RejectAfterTime).UnixNano()) + peer.timers.sessionExpired.Mod(RejectAfterTime) peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3) } + peer.noteSessionState(PeerSessionEstablished) } /* Should be called before a packet with authentication -- keepalive, data, or handshake -- is sent, or after one is received. */ @@ -213,6 +227,7 @@ func (peer *Peer) timersInit() { peer.timers.retransmitHandshake = peer.NewTimer(expiredRetransmitHandshake) peer.timers.sendKeepalive = peer.NewTimer(expiredSendKeepalive) peer.timers.newHandshake = peer.NewTimer(expiredNewHandshake) + peer.timers.sessionExpired = peer.NewTimer(expiredSession) peer.timers.zeroKeyMaterial = peer.NewTimer(expiredZeroKeyMaterial) peer.timers.persistentKeepalive = peer.NewTimer(expiredPersistentKeepalive) } @@ -227,6 +242,7 @@ func (peer *Peer) timersStop() { peer.timers.retransmitHandshake.DelSync() peer.timers.sendKeepalive.DelSync() peer.timers.newHandshake.DelSync() + peer.timers.sessionExpired.DelSync() peer.timers.zeroKeyMaterial.DelSync() peer.timers.persistentKeepalive.DelSync() } From 2ad9837e6cc15a90a094abbc0bc8e680989ac042 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 26 Apr 2026 15:02:22 +0000 Subject: [PATCH 160/173] 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. --- device/allowedips.go | 1 - device/channels.go | 4 +- device/lock-ordering.md | 27 ----- device/pools.go | 2 - device/receive.go | 234 +++++++++++++++++++++++----------------- device/send.go | 128 +++++++++++++--------- 6 files changed, 214 insertions(+), 182 deletions(-) delete mode 100644 device/lock-ordering.md diff --git a/device/allowedips.go b/device/allowedips.go index 8724802..2271af1 100644 --- a/device/allowedips.go +++ b/device/allowedips.go @@ -329,7 +329,6 @@ func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) { 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) { diff --git a/device/channels.go b/device/channels.go index 45b2a76..9ac767f 100644 --- a/device/channels.go +++ b/device/channels.go @@ -97,7 +97,7 @@ func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { for { select { case elemsContainer := <-q.c: - elemsContainer.Lock() + elemsContainer.filling.Wait() for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) device.PutInboundElement(elem) @@ -136,7 +136,7 @@ func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { for { select { case elemsContainer := <-q.c: - elemsContainer.Lock() + elemsContainer.filling.Wait() for _, elem := range elemsContainer.elems { device.PutOutboundBuffer(elem.buffer) device.PutOutboundElement(elem) diff --git a/device/lock-ordering.md b/device/lock-ordering.md deleted file mode 100644 index 55a15c0..0000000 --- a/device/lock-ordering.md +++ /dev/null @@ -1,27 +0,0 @@ -# Lock Ordering in wireguard-go/device - -## Lock hierarchy - -Locks must be acquired in the order listed below. A goroutine holding a -lock with a higher number must never attempt to acquire a lock with a -lower number. - -``` -Level 0 device.state.Mutex -Level 1 device.ipcMutex (sync.RWMutex) -Level 2 device.net.RWMutex -Level 3 device.staticIdentity.RWMutex -Level 4 device.peers.RWMutex -Level 5 peer.state.Mutex -Level 6 peer.handshake.mutex (sync.RWMutex) -Level 7 peer.keypairs.RWMutex -Level 8 device.allowedips.mu (sync.RWMutex) -Level 9 device.indexTable.RWMutex -Level 10 peer.endpoint.Mutex -Level 11 device.cookieChecker.RWMutex -Level 12 peer.cookieGenerator.RWMutex -Level 13 Timer.modifyingLock / Timer.runningLock -``` - -Not every pair of locks appears in practice; the ordering above is the -transitive closure of the pairs that do. diff --git a/device/pools.go b/device/pools.go index 173486e..6a52472 100644 --- a/device/pools.go +++ b/device/pools.go @@ -74,7 +74,6 @@ func (device *Device) PopulatePools() { func (device *Device) GetInboundElementsContainer() *QueueInboundElementsContainer { c := device.pool.inboundElementsContainer.Get().(*QueueInboundElementsContainer) - c.Mutex = sync.Mutex{} return c } @@ -88,7 +87,6 @@ func (device *Device) PutInboundElementsContainer(c *QueueInboundElementsContain func (device *Device) GetOutboundElementsContainer() *QueueOutboundElementsContainer { c := device.pool.outboundElementsContainer.Get().(*QueueOutboundElementsContainer) - c.Mutex = sync.Mutex{} return c } diff --git a/device/receive.go b/device/receive.go index b6edc56..e11e30a 100644 --- a/device/receive.go +++ b/device/receive.go @@ -8,6 +8,7 @@ package device import ( "encoding/binary" "errors" + "fmt" "net" "net/netip" "sync" @@ -35,8 +36,13 @@ type QueueInboundElement struct { } type QueueInboundElementsContainer struct { - sync.Mutex - elems []*QueueInboundElement + // filling is a one-shot barrier signaling decryption→receive + // 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. @@ -178,7 +184,6 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive elemsForPeer, ok := elemsByPeer[peer] if !ok { elemsForPeer = device.GetInboundElementsContainer() - elemsForPeer.Lock() elemsByPeer[peer] = elemsForPeer } elemsForPeer.elems = append(elemsForPeer.elems, elem) @@ -222,6 +227,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive } for peer, elemsContainer := range elemsByPeer { if peer.isRunning.Load() { + elemsContainer.filling.Add(1) peer.queue.inbound.c <- elemsContainer device.queue.decryption.c <- elemsContainer } else { @@ -263,7 +269,7 @@ func (device *Device) RoutineDecryption(id int) { elem.packet = nil } } - elemsContainer.Unlock() + elemsContainer.filling.Done() } } @@ -440,102 +446,128 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) { if elemsContainer == nil { return } - elemsContainer.Lock() - 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() - } - 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 - } - - 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) + peer.processInboundContainer(elemsContainer, bufs[:0]) + } +} + +// 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.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) } } diff --git a/device/send.go b/device/send.go index 6bd1ec9..fae5c41 100644 --- a/device/send.go +++ b/device/send.go @@ -8,6 +8,7 @@ package device import ( "encoding/binary" "errors" + "fmt" "net" "net/netip" "os" @@ -58,8 +59,13 @@ type QueueOutboundElement struct { } type QueueOutboundElementsContainer struct { - sync.Mutex - elems []*QueueOutboundElement + // filling is a one-shot barrier signaling encryption→send handoff. + // 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 { @@ -486,7 +492,6 @@ top: elem.keypair = keypair } - elemsContainer.Lock() elemsContainer.elems = elemsContainer.elems[:i] if elemsContainerOOO != nil { @@ -502,6 +507,7 @@ top: // add to parallel and sequential queue if peer.isRunning.Load() { + elemsContainer.filling.Add(1) peer.queue.outbound.c <- elemsContainer peer.device.queue.encryption.c <- elemsContainer } else { @@ -595,7 +601,7 @@ func (device *Device) RoutineEncryption(id int) { // re-slice packet to include encapsulating transport space elem.packet = elem.buffer[:MessageEncapsulatingTransportSize+len(elem.packet)] } - elemsContainer.Unlock() + elemsContainer.filling.Done() } } @@ -610,60 +616,84 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { bufs := make([][]byte, 0, maxBatchSize) for elemsContainer := range peer.queue.outbound.c { - bufs = bufs[:0] if elemsContainer == nil { return } - 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. - elemsContainer.Lock() - peer.queuedOutboundPackets.Add(-int32(len(elemsContainer.elems))) - for _, elem := range elemsContainer.elems { - device.PutOutboundBuffer(elem.buffer) - device.PutOutboundElement(elem) - } - device.PutOutboundElementsContainer(elemsContainer) - continue - } - dataSent := false - elemsContainer.Lock() - for _, elem := range elemsContainer.elems { - if len(elem.packet) != MessageKeepaliveSize { - dataSent = true - } - bufs = append(bufs, elem.packet) - } + peer.processOutboundContainer(elemsContainer, bufs[:0]) + } +} - peer.timersAnyAuthenticatedPacketTraversal() - peer.timersAnyAuthenticatedPacketSent() +// 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))) + } - err := peer.SendBuffers(bufs) - if dataSent { - peer.timersDataSent() - } + 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) } - 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() + return } + + dataSent := false + for _, elem := range elemsContainer.elems { + if len(elem.packet[MessageEncapsulatingTransportSize:]) != MessageKeepaliveSize { + dataSent = true + } + 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() } From cd7ac13b86fde959a222441a6a206e9e3cb9e135 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Thu, 18 Jun 2026 18:02:42 -0700 Subject: [PATCH 161/173] device: convert runtime.SetFinalizer to AddCleanup (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- device/channels.go | 12 ++++++------ device/peer.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/device/channels.go b/device/channels.go index 9ac767f..9af6e3d 100644 --- a/device/channels.go +++ b/device/channels.go @@ -84,7 +84,7 @@ func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue { c: make(chan *QueueInboundElementsContainer, QueueInboundSize), } if device.needsInboundQueueFinalizer() { - runtime.SetFinalizer(q, device.flushInboundQueue) + runtime.AddCleanup(q, device.flushInboundQueue, q.c) } return q } @@ -93,10 +93,10 @@ func (device *Device) needsInboundQueueFinalizer() bool { return device.pool.messageBuffers.hasAccounting() } -func (device *Device) flushInboundQueue(q *autodrainingInboundQueue) { +func (device *Device) flushInboundQueue(c <-chan *QueueInboundElementsContainer) { for { select { - case elemsContainer := <-q.c: + case elemsContainer := <-c: elemsContainer.filling.Wait() for _, elem := range elemsContainer.elems { device.PutMessageBuffer(elem.buffer) @@ -123,7 +123,7 @@ func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue { c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize), } if device.needsOutboundQueueFinalizer() { - runtime.SetFinalizer(q, device.flushOutboundQueue) + runtime.AddCleanup(q, device.flushOutboundQueue, q.c) } return q } @@ -132,10 +132,10 @@ func (device *Device) needsOutboundQueueFinalizer() bool { return device.pool.messageBuffers.hasAccounting() } -func (device *Device) flushOutboundQueue(q *autodrainingOutboundQueue) { +func (device *Device) flushOutboundQueue(c <-chan *QueueOutboundElementsContainer) { for { select { - case elemsContainer := <-q.c: + case elemsContainer := <-c: elemsContainer.filling.Wait() for _, elem := range elemsContainer.elems { device.PutOutboundBuffer(elem.buffer) diff --git a/device/peer.go b/device/peer.go index 4f90144..c0ca59a 100644 --- a/device/peer.go +++ b/device/peer.go @@ -244,8 +244,8 @@ func (peer *Peer) Start() { peer.timersStart() - device.flushInboundQueue(peer.queue.inbound) - device.flushOutboundQueue(peer.queue.outbound) + device.flushInboundQueue(peer.queue.inbound.c) + device.flushOutboundQueue(peer.queue.outbound.c) // Use the device batch size, not the bind batch size, as the device size is // the size of the batch pools. From 35a60acb84f48aa8f7b6e99f4e9a88eef552f78c Mon Sep 17 00:00:00 2001 From: Alex Valiushko Date: Mon, 22 Jun 2026 09:46:46 -0700 Subject: [PATCH 162/173] 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 --- device/peer.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/device/peer.go b/device/peer.go index c0ca59a..14ecdc2 100644 --- a/device/peer.go +++ b/device/peer.go @@ -254,6 +254,14 @@ func (peer *Peer) Start() { go peer.RoutineSequentialReceiver(batchSize) 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() { From 15b912c1c0f41babf41d653d1d154513009626a5 Mon Sep 17 00:00:00 2001 From: Alex Valiushko Date: Wed, 24 Jun 2026 14:09:25 -0700 Subject: [PATCH 163/173] 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 Change-Id: Iee2cdf135375519e58a8e84362349d966a6a6964 --- device/device.go | 17 ++++++----- device/peer.go | 76 ++++++++++++++++++++++++++---------------------- device/timers.go | 15 ++++++---- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/device/device.go b/device/device.go index 0cb5735..a826c6d 100644 --- a/device/device.go +++ b/device/device.go @@ -64,10 +64,7 @@ type Device struct { lookupFunc PeerLookupFunc // or nil if unused } - sessionState struct { - sync.Mutex // serializes PeerSessionStateFunc calls and protects peer.sessionState - fn PeerSessionStateFunc - } + peerStateFn atomic.Pointer[PeerSessionStateFunc] // observes peer session state changes, nil if unset rate struct { underLoadUntil atomic.Int64 @@ -519,7 +516,7 @@ const ( // PeerSessionStateFunc is called when a peer's WireGuard session state changes. // -// Calls are serialized per Device and delivered in transition order. The +// 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) @@ -546,10 +543,14 @@ func (device *Device) SetPeerByIPPacketFunc(f PeerByIPPacketFunc) { // 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) { - device.sessionState.Lock() - defer device.sessionState.Unlock() - device.sessionState.fn = f + if f == nil { + device.peerStateFn.Store(nil) + return + } + device.peerStateFn.Store(&f) } func (device *Device) Close() { diff --git a/device/peer.go b/device/peer.go index 14ecdc2..9726f90 100644 --- a/device/peer.go +++ b/device/peer.go @@ -18,16 +18,20 @@ import ( ) type Peer struct { - isRunning atomic.Bool - keypairs Keypairs - handshake Handshake - device *Device - stopping sync.WaitGroup // routines pending stop - txBytes atomic.Uint64 // bytes send to peer (endpoint) - rxBytes atomic.Uint64 // bytes received from peer - lastHandshakeNano atomic.Int64 // nano seconds since epoch - sessionExpiresNano atomic.Int64 // nano seconds since epoch - sessionState PeerSessionState // guarded by device.sessionState.Mutex + isRunning atomic.Bool + keypairs Keypairs + handshake Handshake + device *Device + stopping sync.WaitGroup // routines pending stop + txBytes atomic.Uint64 // bytes send to peer (endpoint) + rxBytes atomic.Uint64 // bytes received from peer + 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 @@ -266,7 +270,6 @@ func (peer *Peer) Start() { func (peer *Peer) ZeroAndFlushAll() { device := peer.device - peer.sessionExpiresNano.Store(0) if peer.timers.sessionExpired != nil { peer.timers.sessionExpired.Del() } @@ -292,7 +295,11 @@ func (peer *Peer) ZeroAndFlushAll() { handshake.mutex.Unlock() peer.FlushStagedPackets() - peer.noteSessionState(PeerSessionNone) + + peer.sessionState.Lock() + peer.sessionState.sessionExpires = time.Time{} + peer.noteSessionStateLocked(PeerSessionNone) + peer.sessionState.Unlock() } func (peer *Peer) ExpireCurrentKeypairs() { @@ -313,8 +320,10 @@ func (peer *Peer) ExpireCurrentKeypairs() { } keypairs.Unlock() - peer.sessionExpiresNano.Store(0) - peer.noteSessionState(PeerSessionExpired) + peer.sessionState.Lock() + peer.sessionState.sessionExpires = time.Time{} + peer.noteSessionStateLocked(PeerSessionExpired) + peer.sessionState.Unlock() } func (peer *Peer) Stop() { @@ -338,42 +347,41 @@ func (peer *Peer) Stop() { } func (peer *Peer) noteSessionState(state PeerSessionState) { - device := peer.device - device.sessionState.Lock() - defer device.sessionState.Unlock() + peer.sessionState.Lock() + defer peer.sessionState.Unlock() + peer.noteSessionStateLocked(state) +} - if peer.sessionState == 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 = state - if f := device.sessionState.fn; f != nil { - f(peer.handshake.remoteStatic, state) + peer.sessionState.current = state + if f := peer.device.peerStateFn.Load(); f != nil { + (*f)(peer.handshake.remoteStatic, state) } } func (peer *Peer) noteSessionHandshakeStarted() { - device := peer.device - device.sessionState.Lock() - defer device.sessionState.Unlock() - - switch peer.sessionState { - case PeerSessionEstablished: - return - case PeerSessionHandshake: + peer.sessionState.Lock() + defer peer.sessionState.Unlock() + if peer.sessionState.current == PeerSessionEstablished { return } - peer.sessionState = PeerSessionHandshake - if f := device.sessionState.fn; f != nil { - f(peer.handshake.remoteStatic, PeerSessionHandshake) - } + peer.noteSessionStateLocked(PeerSessionHandshake) } func (peer *Peer) noteSessionHandshakeStopped() { + peer.sessionState.Lock() + defer peer.sessionState.Unlock() state := PeerSessionNone if peer.hasKeyMaterial() { state = PeerSessionExpired } - peer.noteSessionState(state) + peer.noteSessionStateLocked(state) } func (peer *Peer) hasKeyMaterial() bool { diff --git a/device/timers.go b/device/timers.go index affa792..d30f26b 100644 --- a/device/timers.go +++ b/device/timers.go @@ -141,12 +141,13 @@ func expiredZeroKeyMaterial(peer *Peer) { } func expiredSession(peer *Peer) { - expires := peer.sessionExpiresNano.Load() - if expires == 0 || time.Now().UnixNano() < expires { + 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.noteSessionState(PeerSessionExpired) + peer.noteSessionStateLocked(PeerSessionExpired) } func expiredPersistentKeepalive(peer *Peer) { @@ -208,11 +209,15 @@ 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. */ func (peer *Peer) timersSessionDerived() { if peer.timersActive() { - peer.sessionExpiresNano.Store(time.Now().Add(RejectAfterTime).UnixNano()) + 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) + } else { + peer.noteSessionState(PeerSessionEstablished) } - peer.noteSessionState(PeerSessionEstablished) } /* Should be called before a packet with authentication -- keepalive, data, or handshake -- is sent, or after one is received. */ From 7a66fbee4ae184b9afcd63e1d4cbc3564c62acdd Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Tue, 7 Jul 2026 14:55:53 -0700 Subject: [PATCH 164/173] 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 --- device/device.go | 35 +++++++++++++++++++++++++- device/receive.go | 2 ++ device/send.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/device/device.go b/device/device.go index a826c6d..4cb1afe 100644 --- a/device/device.go +++ b/device/device.go @@ -64,7 +64,8 @@ type Device struct { lookupFunc PeerLookupFunc // or nil if unused } - peerStateFn atomic.Pointer[PeerSessionStateFunc] // observes peer session state changes, nil if unset + 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 { underLoadUntil atomic.Int64 @@ -553,6 +554,38 @@ func (device *Device) SetSessionStateFunc(f PeerSessionStateFunc) { 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() { device.state.Lock() defer device.state.Unlock() diff --git a/device/receive.go b/device/receive.go index e11e30a..3d42639 100644 --- a/device/receive.go +++ b/device/receive.go @@ -425,6 +425,7 @@ func (device *Device) RoutineHandshake(id int) { peer.timersSessionDerived() peer.timersHandshakeComplete() + peer.SendPriorityMessage() peer.SendKeepalive() } skip: @@ -493,6 +494,7 @@ func (peer *Peer) processInboundContainer(elemsContainer *QueueInboundElementsCo if peer.ReceivedWithKeypair(elem.keypair) { peer.SetEndpointFromPacket(elem.endpoint) peer.timersHandshakeComplete() + peer.SendPriorityMessage() peer.SendStagedPackets() } if ep, ok := elem.endpoint.(conn.PeerAwareEndpoint); ok { diff --git a/device/send.go b/device/send.go index fae5c41..f5561d7 100644 --- a/device/send.go +++ b/device/send.go @@ -107,6 +107,70 @@ func (peer *Peer) SendKeepalive() { 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 { if !isRetry { peer.timers.handshakeAttempts.Store(0) From da8671622b26a46653074847a47800079a9b8f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 5 Aug 2026 10:47:57 +0800 Subject: [PATCH 165/173] Fix InputPackets exceeding device batch size --- device/send.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/device/send.go b/device/send.go index f5561d7..034aa95 100644 --- a/device/send.go +++ b/device/send.go @@ -447,6 +447,7 @@ type InputPacketRef struct { func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef { var unmatched []*InputPacketRef + batchSize := device.BatchSize() elemsByPeer := make(map[*Peer][]*QueueOutboundElementsContainer, len(packets)) for _, packetRef := range packets { peer := device.allowedips.Lookup(packetRef.Destination) @@ -475,7 +476,7 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef } elem.packet = packet[:n] containers := elemsByPeer[peer] - if len(containers) == 0 || len(containers[len(containers)-1].elems) >= conn.IdealBatchSize { + if len(containers) == 0 || len(containers[len(containers)-1].elems) >= batchSize { containers = append(containers, device.GetOutboundElementsContainer()) elemsByPeer[peer] = containers } From f39689ad35629263dff7172e0e93eccf2f987ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 5 Aug 2026 12:29:41 +0800 Subject: [PATCH 166/173] 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. --- device/send.go | 52 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/device/send.go b/device/send.go index 034aa95..d434788 100644 --- a/device/send.go +++ b/device/send.go @@ -403,8 +403,51 @@ func (device *Device) RoutineReadFromTUN() { // 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.allowedips.Lookup(destination) + peer := device.inputPacketPeer(destination, packetSlices) if peer == nil { return } @@ -447,10 +490,9 @@ type InputPacketRef struct { func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef { var unmatched []*InputPacketRef - batchSize := device.BatchSize() elemsByPeer := make(map[*Peer][]*QueueOutboundElementsContainer, len(packets)) for _, packetRef := range packets { - peer := device.allowedips.Lookup(packetRef.Destination) + peer := device.inputPacketPeer(packetRef.Destination, packetRef.PacketSlices) if peer == nil { unmatched = append(unmatched, packetRef) continue @@ -476,7 +518,7 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef } elem.packet = packet[:n] containers := elemsByPeer[peer] - if len(containers) == 0 || len(containers[len(containers)-1].elems) >= batchSize { + if len(containers) == 0 || len(containers[len(containers)-1].elems) >= conn.IdealBatchSize { containers = append(containers, device.GetOutboundElementsContainer()) elemsByPeer[peer] = containers } @@ -678,7 +720,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { }() 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 { if elemsContainer == nil { From 831d48336675fc6889b9d24fb406f127cd5aa323 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:41:07 +0300 Subject: [PATCH 167/173] lx: re-graft AmneziaWG 2.0 obfuscation onto sagernet/wireguard-go v0.0.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebase of the AWG obf graft (was e5feca7 on v0.0.3) onto v0.0.5 (2c27bbf4f97f, '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). --- device/cookie.go | 3 +- device/device.go | 31 ++++- device/magic-header.go | 63 +++++++++ device/noise-protocol.go | 19 +-- device/obf.go | 140 +++++++++++++++++++ device/obf_bytes.go | 47 +++++++ device/obf_data.go | 25 ++++ device/obf_datasize.go | 38 +++++ device/obf_datastring.go | 29 ++++ device/obf_rand.go | 39 ++++++ device/obf_randchars.go | 48 +++++++ device/obf_randdigits.go | 48 +++++++ device/obf_timestamp.go | 31 +++++ device/receive.go | 86 ++++++++++-- device/send.go | 108 ++++++++++++--- device/uapi.go | 291 +++++++++++++++++++++++++++++++++++++-- 16 files changed, 1001 insertions(+), 45 deletions(-) create mode 100644 device/magic-header.go create mode 100644 device/obf.go create mode 100644 device/obf_bytes.go create mode 100644 device/obf_data.go create mode 100644 device/obf_datasize.go create mode 100644 device/obf_datastring.go create mode 100644 device/obf_rand.go create mode 100644 device/obf_randchars.go create mode 100644 device/obf_randdigits.go create mode 100644 device/obf_timestamp.go diff --git a/device/cookie.go b/device/cookie.go index a093c8b..6a0463c 100644 --- a/device/cookie.go +++ b/device/cookie.go @@ -118,6 +118,7 @@ func (st *CookieChecker) CreateReply( msg []byte, recv uint32, src []byte, + msgType uint32, ) (*MessageCookieReply, error) { st.RLock() @@ -153,7 +154,7 @@ func (st *CookieChecker) CreateReply( smac1 := smac2 - blake2s.Size128 reply := new(MessageCookieReply) - reply.Type = MessageCookieReplyType + reply.Type = msgType reply.Receiver = recv _, err := rand.Read(reply.Nonce[:]) diff --git a/device/device.go b/device/device.go index 4cb1afe..654447a 100644 --- a/device/device.go +++ b/device/device.go @@ -99,6 +99,29 @@ type Device struct { closed chan struct{} 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 } // deviceState represents the state of a Device. @@ -172,7 +195,8 @@ func (device *Device) changeState(want deviceState) (err error) { 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 } @@ -317,6 +341,11 @@ func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger device.rate.limiter.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() // create queues diff --git a/device/magic-header.go b/device/magic-header.go new file mode 100644 index 0000000..78e59d6 --- /dev/null +++ b/device/magic-header.go @@ -0,0 +1,63 @@ +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 { + high := int64(h.end - h.start + 1) + r, _ := rand.Int(rand.Reader, big.NewInt(high)) + return h.start + uint32(r.Int64()) +} diff --git a/device/noise-protocol.go b/device/noise-protocol.go index d72bb25..75fa025 100644 --- a/device/noise-protocol.go +++ b/device/noise-protocol.go @@ -54,10 +54,11 @@ const ( ) const ( - MessageInitiationType = 1 - MessageResponseType = 2 - MessageCookieReplyType = 3 - MessageTransportType = 4 + MessageUnknownType uint32 = 0 + MessageInitiationType uint32 = 1 + MessageResponseType uint32 = 2 + MessageCookieReplyType uint32 = 3 + MessageTransportType uint32 = 4 ) const ( @@ -65,7 +66,7 @@ const ( MessageResponseSize = 92 // size of response message MessageCookieReplySize = 64 // size of cookie reply message MessageTransportHeaderSize = 16 // size of data preceding content in transport message - MessageEncapsulatingTransportSize = 8 // size of optional, free (for use by conn.Bind.Send()) space preceding the transport header + MessageEncapsulatingTransportSize = 0 // lx: zeroed so AmneziaWG obfuscation composes without sagernet headroom (AWG path doesn't use the Bind.Send prepend) MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport MessageKeepaliveSize = MessageTransportSize // size of keepalive MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message @@ -218,7 +219,7 @@ type Handshake struct { localEphemeral NoisePrivateKey // ephemeral secret key localIndex uint32 // used to clear hash-table 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 precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret lastTimestamp tai64n.Timestamp @@ -287,8 +288,10 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e handshake.mixHash(handshake.remoteStatic[:]) + msgType := device.headers.init.Generate() + msg := MessageInitiation{ - Type: MessageInitiationType, + Type: msgType, Ephemeral: handshake.localEphemeral.publicKey(), } @@ -471,7 +474,7 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error } var msg MessageResponse - msg.Type = MessageResponseType + msg.Type = device.headers.response.Generate() msg.Sender = handshake.localIndex msg.Receiver = handshake.remoteIndex diff --git a/device/obf.go b/device/obf.go new file mode 100644 index 0000000..53c55ff --- /dev/null +++ b/device/obf.go @@ -0,0 +1,140 @@ +package device + +import ( + "errors" + "fmt" + "strings" +) + +type obfBuilder func(val string) (obf, error) + +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 +} diff --git a/device/obf_bytes.go b/device/obf_bytes.go new file mode 100644 index 0000000..68d722b --- /dev/null +++ b/device/obf_bytes.go @@ -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 +} diff --git a/device/obf_data.go b/device/obf_data.go new file mode 100644 index 0000000..42d3f65 --- /dev/null +++ b/device/obf_data.go @@ -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 +} diff --git a/device/obf_datasize.go b/device/obf_datasize.go new file mode 100644 index 0000000..7267e2a --- /dev/null +++ b/device/obf_datasize.go @@ -0,0 +1,38 @@ +package device + +import "strconv" + +func newDataSizeObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_datastring.go b/device/obf_datastring.go new file mode 100644 index 0000000..2701e95 --- /dev/null +++ b/device/obf_datastring.go @@ -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) +} diff --git a/device/obf_rand.go b/device/obf_rand.go new file mode 100644 index 0000000..edf461e --- /dev/null +++ b/device/obf_rand.go @@ -0,0 +1,39 @@ +package device + +import ( + "crypto/rand" + "strconv" +) + +func newRandObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_randchars.go b/device/obf_randchars.go new file mode 100644 index 0000000..1d9968c --- /dev/null +++ b/device/obf_randchars.go @@ -0,0 +1,48 @@ +package device + +import ( + "crypto/rand" + "strconv" + "unicode" +) + +const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func newRandCharObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_randdigits.go b/device/obf_randdigits.go new file mode 100644 index 0000000..4794bb1 --- /dev/null +++ b/device/obf_randdigits.go @@ -0,0 +1,48 @@ +package device + +import ( + "crypto/rand" + "strconv" + "unicode" +) + +const digits10 = "0123456789" + +func newRandDigitsObf(val string) (obf, error) { + length, err := strconv.Atoi(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 +} diff --git a/device/obf_timestamp.go b/device/obf_timestamp.go new file mode 100644 index 0000000..0a8180b --- /dev/null +++ b/device/obf_timestamp.go @@ -0,0 +1,31 @@ +package device + +import ( + "encoding/binary" + "time" +) + +func newTimestampObf(_ string) (obf, error) { + return ×tampObf{}, 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 +} diff --git a/device/receive.go b/device/receive.go index 3d42639..8064f61 100644 --- a/device/receive.go +++ b/device/receive.go @@ -76,7 +76,10 @@ func (peer *Peer) keepKeyFreshReceiving() { * Every time the bind is updated a new routine is started for * 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() defer func() { device.log.Verbosef("Routine: receive incoming %s - stopped", recvName) @@ -139,9 +142,14 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive } // check size of packet - 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 { @@ -283,7 +291,6 @@ func (device *Device) RoutineHandshake(id int) { device.log.Verbosef("Routine: handshake worker %d - started", id) for elem := range device.queue.handshake.c { - // handle cookie fields and ratelimiting switch elem.msgType { @@ -310,9 +317,14 @@ func (device *Device) RoutineHandshake(id int) { // consume reply 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) { - device.log.Verbosef("Could not decrypt invalid cookie response") + device.log.Verbosef( + "Could not decrypt invalid cookie response", + ) } } @@ -354,9 +366,7 @@ func (device *Device) RoutineHandshake(id int) { switch elem.msgType { case MessageInitiationType: - // unmarshal - var msg MessageInitiation err := msg.unmarshal(elem.packet) if err != nil { @@ -364,7 +374,8 @@ func (device *Device) RoutineHandshake(id int) { goto skip } - // consume initiation + // have to reassign msgType for ranged msgType to work + msg.Type = elem.msgType peer := device.ConsumeMessageInitiation(&msg, elem.endpoint) if peer == nil { @@ -396,6 +407,9 @@ func (device *Device) RoutineHandshake(id int) { goto skip } + // have to reassign msgType for ranged msgType to work + msg.Type = elem.msgType + // consume response peer := device.ConsumeMessageResponse(&msg) @@ -573,3 +587,57 @@ func (peer *Peer) processInboundContainer(elemsContainer *QueueInboundElementsCo 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 +} diff --git a/device/send.go b/device/send.go index d434788..aea1b74 100644 --- a/device/send.go +++ b/device/send.go @@ -6,9 +6,12 @@ package device import ( + "bytes" + "crypto/rand" "encoding/binary" "errors" "fmt" + "math/big" "net" "net/netip" "os" @@ -199,15 +202,48 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } - buf := make([]byte, MessageEncapsulatingTransportSize+MessageInitiationSize) - packet := buf[MessageEncapsulatingTransportSize:] - _ = msg.marshal(packet) + 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 + + 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 + writer := bytes.NewBuffer(buf[:0]) + binary.Write(writer, binary.LittleEndian, msg) + packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketSent() - err = peer.SendBuffers([][]byte{buf}) + 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 { peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) } @@ -229,9 +265,11 @@ func (peer *Peer) SendHandshakeResponse() error { return err } - buf := make([]byte, MessageEncapsulatingTransportSize+MessageResponseSize) - packet := buf[MessageEncapsulatingTransportSize:] - _ = response.marshal(packet) + var buf [MessageResponseSize]byte + writer := bytes.NewBuffer(buf[:0]) + + binary.Write(writer, binary.LittleEndian, response) + packet := writer.Bytes() peer.cookieGenerator.AddMacs(packet) err = peer.BeginSymmetricSession() @@ -244,8 +282,15 @@ func (peer *Peer) SendHandshakeResponse() error { peer.timersAnyAuthenticatedPacketTraversal() 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 - err = peer.SendBuffers([][]byte{buf}) + err = peer.SendBuffers([][]byte{packet}) if err != nil { peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) } @@ -256,18 +301,33 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) 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 { device.log.Errorf("Failed to create cookie reply: %v", err) return err } - buf := make([]byte, MessageEncapsulatingTransportSize+MessageCookieReplySize) - packet := buf[MessageEncapsulatingTransportSize:] - _ = reply.marshal(packet) - // TODO: allocation could be avoided - device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint, MessageEncapsulatingTransportSize) + var buf [MessageCookieReplySize]byte + writer := bytes.NewBuffer(buf[:0]) + 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 + device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint, 0) return nil } @@ -687,7 +747,9 @@ func (device *Device) RoutineEncryption(id int) { fieldReceiver := header[4:8] 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.PutUint64(fieldNonce, elem.nonce) @@ -704,9 +766,6 @@ func (device *Device) RoutineEncryption(id int) { elem.packet, nil, ) - - // re-slice packet to include encapsulating transport space - elem.packet = elem.buffer[:MessageEncapsulatingTransportSize+len(elem.packet)] } elemsContainer.filling.Done() } @@ -775,6 +834,19 @@ func (peer *Peer) processOutboundContainer(elemsContainer *QueueOutboundElements 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) } diff --git a/device/uapi.go b/device/uapi.go index cba371d..4f295c1 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -97,6 +97,56 @@ func (device *Device) IpcGetOperation(w io.Writer) error { 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 { // Serialize peer state. peer.handshake.mutex.RLock() @@ -147,6 +197,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { } }() + ipcDev := new(ipcSetDevice) peer := new(ipcSetPeer) deviceConfig := true @@ -155,12 +206,20 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { line := scanner.Text() if line == "" { // 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() return nil } key, value, ok := strings.Cut(line, "=") 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" { @@ -186,6 +245,10 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) { return err } } + err = ipcDev.mergeWithDevice(device) + if err != nil { + return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err) + } peer.handlePostConfig() if err := scanner.Err(); err != nil { @@ -235,11 +298,155 @@ func (device *Device) handleDeviceLine(key, value string) error { case "replace_peers": 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.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: 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. var publicKey NoisePublicKey err := publicKey.FromHex(value) @@ -301,12 +511,19 @@ func (device *Device) handlePublicKeyLine(peer *ipcSetPeer, value string) error return nil } -func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error { +func (device *Device) handlePeerLine( + peer *ipcSetPeer, + key, value string, +) error { switch key { case "update_only": // allow disabling of creation 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 { 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) 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)) @@ -363,7 +584,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error case "replace_allowed_ips": device.log.Verbosef("%v - UAPI: Removing all allowedips", peer.Peer) 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 { return nil @@ -442,7 +667,11 @@ func (device *Device) IpcHandle(socket net.Conn) { return } 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 } err = device.IpcGetOperation(buffered.Writer) @@ -466,3 +695,49 @@ func (device *Device) IpcHandle(socket net.Conn) { 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 +} From ee7ff1b77f410391c16c349f1adfdf79d8aa1259 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:45:56 +0300 Subject: [PATCH 168/173] 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. --- device/magic-header.go | 4 +- device/obf.go | 15 ++ device/obf_datasize.go | 4 +- device/obf_guards_test.go | 108 +++++++++ device/obf_rand.go | 3 +- device/obf_randchars.go | 3 +- device/obf_randdigits.go | 3 +- device/send.go | 13 +- device/transport_padding_test.go | 361 +++++++++++++++++++++++++++++++ 9 files changed, 502 insertions(+), 12 deletions(-) create mode 100644 device/obf_guards_test.go create mode 100644 device/transport_padding_test.go diff --git a/device/magic-header.go b/device/magic-header.go index 78e59d6..6ea0ce5 100644 --- a/device/magic-header.go +++ b/device/magic-header.go @@ -57,7 +57,9 @@ func (h *magicHeader) Validate(val uint32) bool { } func (h *magicHeader) Generate() uint32 { - high := int64(h.end - h.start + 1) + // 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()) } diff --git a/device/obf.go b/device/obf.go index 53c55ff..269007c 100644 --- a/device/obf.go +++ b/device/obf.go @@ -3,11 +3,26 @@ 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, diff --git a/device/obf_datasize.go b/device/obf_datasize.go index 7267e2a..8ad1a71 100644 --- a/device/obf_datasize.go +++ b/device/obf_datasize.go @@ -1,9 +1,7 @@ package device -import "strconv" - func newDataSizeObf(val string) (obf, error) { - length, err := strconv.Atoi(val) + length, err := parseObfLen(val) if err != nil { return nil, err } diff --git a/device/obf_guards_test.go b/device/obf_guards_test.go new file mode 100644 index 0000000..4775e75 --- /dev/null +++ b/device/obf_guards_test.go @@ -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}) + }) +} diff --git a/device/obf_rand.go b/device/obf_rand.go index edf461e..1560460 100644 --- a/device/obf_rand.go +++ b/device/obf_rand.go @@ -2,11 +2,10 @@ package device import ( "crypto/rand" - "strconv" ) func newRandObf(val string) (obf, error) { - length, err := strconv.Atoi(val) + length, err := parseObfLen(val) if err != nil { return nil, err } diff --git a/device/obf_randchars.go b/device/obf_randchars.go index 1d9968c..470ca6f 100644 --- a/device/obf_randchars.go +++ b/device/obf_randchars.go @@ -2,14 +2,13 @@ package device import ( "crypto/rand" - "strconv" "unicode" ) const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func newRandCharObf(val string) (obf, error) { - length, err := strconv.Atoi(val) + length, err := parseObfLen(val) if err != nil { return nil, err } diff --git a/device/obf_randdigits.go b/device/obf_randdigits.go index 4794bb1..d3585a0 100644 --- a/device/obf_randdigits.go +++ b/device/obf_randdigits.go @@ -2,14 +2,13 @@ package device import ( "crypto/rand" - "strconv" "unicode" ) const digits10 = "0123456789" func newRandDigitsObf(val string) (obf, error) { - length, err := strconv.Atoi(val) + length, err := parseObfLen(val) if err != nil { return nil, err } diff --git a/device/send.go b/device/send.go index aea1b74..982d61f 100644 --- a/device/send.go +++ b/device/send.go @@ -215,6 +215,11 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { 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))) @@ -518,7 +523,9 @@ func (device *Device) InputPacket(destination []byte, packetSlices [][]byte) { for _, packetSlice := range packetSlices { totalLength += len(packetSlice) } - allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + // 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 } @@ -564,7 +571,9 @@ func (device *Device) InputPackets(packets []*InputPacketRef) []*InputPacketRef for _, packetSlice := range packetRef.PacketSlices { totalLength += len(packetSlice) } - allocLength := MessageEncapsulatingTransportSize + MessageTransportHeaderSize + totalLength + PaddingMultiple + chacha20poly1305.Overhead + // 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 } diff --git a/device/transport_padding_test.go b/device/transport_padding_test.go new file mode 100644 index 0000000..77061e4 --- /dev/null +++ b/device/transport_padding_test.go @@ -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) +} From 1e787bb3e0772a3773c434335547a63d95209a51 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:32:40 +0200 Subject: [PATCH 169/173] lx: gate reserved-byte clear on receive so AmneziaWG magic survives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- conn/bind_std.go | 16 +- conn/bind_windows.go | 16 +- conn/msgx_darwin.go | 4 +- conn/reserved_gate_lx_test.go | 56 +++++++ device/awg_stdnetbind_reserved_lx_test.go | 190 ++++++++++++++++++++++ device/obf_guards_test.go | 4 +- device/transport_padding_test.go | 8 +- 7 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 conn/reserved_gate_lx_test.go create mode 100644 device/awg_stdnetbind_reserved_lx_test.go diff --git a/conn/bind_std.go b/conn/bind_std.go index 0a15de0..c3a11a0 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -359,7 +359,7 @@ func (s *StdNetBind) receiveIP( if sizes[i] == 0 { continue } - if msg.N > 3 { + if msg.N > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved) common.ClearArray(bufs[i][1:4]) } ep := &StdNetEndpoint{AddrPort: M.AddrPortFromNet(msg.Addr)} // TODO: remove allocation @@ -539,6 +539,20 @@ func (s *StdNetBind) SetReservedForEndpoint(destination netip.AddrPort, reserved 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 { var ( n int diff --git a/conn/bind_windows.go b/conn/bind_windows.go index 121079f..c31bb35 100644 --- a/conn/bind_windows.go +++ b/conn/bind_windows.go @@ -461,7 +461,7 @@ func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen) - if n > 3 { + if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved) common.ClearArray(bufs[0][1:4]) } sizes[0] = n @@ -473,7 +473,7 @@ func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint) bind.mu.RLock() defer bind.mu.RUnlock() n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen) - if n > 3 { + if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved) common.ClearArray(bufs[0][1:4]) } sizes[0] = n @@ -576,6 +576,18 @@ func (bind *WinRingBind) SetReservedForEndpoint(destination netip.AddrPort, rese 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 { s.mu.Lock() defer s.mu.Unlock() diff --git a/conn/msgx_darwin.go b/conn/msgx_darwin.go index 138c8a8..da9bb07 100644 --- a/conn/msgx_darwin.go +++ b/conn/msgx_darwin.go @@ -234,7 +234,7 @@ func (s *StdNetBind) receiveSingle(conn *net.UDPConn, bufs [][]byte, sizes []int return 0, err } sizes[0] = n - if n > 3 { + 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 @@ -299,7 +299,7 @@ func (s *StdNetBind) makeReceiveMsgX(conn *net.UDPConn, isV6 bool) (ReceiveFunc, numMsgs := int(n) for i := 0; i < numMsgs; i++ { sizes[i] = int(state.hdrs[i].DataLen) - if sizes[i] > 3 { + 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 diff --git a/conn/reserved_gate_lx_test.go b/conn/reserved_gate_lx_test.go new file mode 100644 index 0000000..3502a79 --- /dev/null +++ b/conn/reserved_gate_lx_test.go @@ -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") + } +} diff --git a/device/awg_stdnetbind_reserved_lx_test.go b/device/awg_stdnetbind_reserved_lx_test.go new file mode 100644 index 0000000..0d431e1 --- /dev/null +++ b/device/awg_stdnetbind_reserved_lx_test.go @@ -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= 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)") + } + } +} diff --git a/device/obf_guards_test.go b/device/obf_guards_test.go index 4775e75..5080196 100644 --- a/device/obf_guards_test.go +++ b/device/obf_guards_test.go @@ -23,8 +23,8 @@ func TestParseObfLen(t *testing.T) { {"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 + {"-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}, } diff --git a/device/transport_padding_test.go b/device/transport_padding_test.go index 77061e4..c235846 100644 --- a/device/transport_padding_test.go +++ b/device/transport_padding_test.go @@ -188,10 +188,10 @@ func (t *chanTun) Write(bufs [][]byte, offset int) (int, error) { 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) 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() { From 9a23f367481858e3c33c1fb80ad492131e4d3136 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:26:09 +0200 Subject: [PATCH 170/173] lx: re-graft egress-provider API onto AWG2 base (upstream 6f5e8b1947ae) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- conn/bind_std.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index c3a11a0..eb27e10 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -272,7 +272,7 @@ again: return 0, err } sizes[0] = dataLength - if dataLength > 3 { + 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} From 37bc7b9f555285390b84556a78665a94102a1efc Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:08:52 +0200 Subject: [PATCH 171/173] 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. --- device/lx_ipcget_awg_test.go | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 device/lx_ipcget_awg_test.go diff --git a/device/lx_ipcget_awg_test.go b/device/lx_ipcget_awg_test.go new file mode 100644 index 0000000..a5954e6 --- /dev/null +++ b/device/lx_ipcget_awg_test.go @@ -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=", "i3=", "i5=", + "", + }, "\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=", "i3=", "i5=", + } { + 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) + } + } +} From c4e0bcf7683aa576c891bc2243955a781bde691e Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:49:57 +0300 Subject: [PATCH 172/173] 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. --- device/device.go | 67 ++++++++++++ device/lx_giveup_rebind_test.go | 83 ++++++++++++++ device/lx_giveup_selfheal_test.go | 172 ++++++++++++++++++++++++++++++ device/timers.go | 8 ++ 4 files changed, 330 insertions(+) create mode 100644 device/lx_giveup_rebind_test.go create mode 100644 device/lx_giveup_selfheal_test.go diff --git a/device/device.go b/device/device.go index 654447a..4358980 100644 --- a/device/device.go +++ b/device/device.go @@ -122,6 +122,23 @@ type Device struct { } ipackets [5]*obfChain + + // lx: SPEC 041 — passive self-heal on handshake give-up. When a peer's + // handshake retry cycle exhausts (the give-up branch of + // expiredRetransmitHandshake), the device reopens its bind once — with a + // fresh ephemeral port when freshPort is set — and immediately + // re-initiates. Heals 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. Zero cost while healthy: no + // timers, no goroutines — the trigger is the existing give-up event, + // which only fires under traffic demand after ~90s of unanswered + // initiations. 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. @@ -326,6 +343,7 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error { func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *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.closed = make(chan struct{}) device.log = logger @@ -785,6 +803,55 @@ func (device *Device) BindUpdate() error { return nil } +// lx: SPEC 041 — configure the handshake give-up self-heal (see the +// giveUpRebind field comment). 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) +} + +// lx: SPEC 041 — invoked from the give-up branch of +// expiredRetransmitHandshake: ~90s of initiations went unanswered, so the +// current socket's 5-tuple is proven dead. Reopen the bind (fresh ephemeral +// port when allowed) and kick a new handshake cycle immediately. Runs the +// heavy part in a goroutine so the timer callback never blocks on +// BindUpdate's worker drain. Debounced to one rebind per RekeyAttemptTime +// per device (CAS on `last` settles concurrent multi-peer give-ups). 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) handleHandshakeGiveUp(peer *Peer) { + if !device.giveUpRebind.enabled.Load() { + return + } + if device.isClosed() { + return + } + now := time.Now().Unix() + last := device.giveUpRebind.last.Load() + if now-last < int64(RekeyAttemptTime/time.Second) { + return + } + if !device.giveUpRebind.last.CompareAndSwap(last, now) { + return + } + 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 to rebind after handshake give-up: %v", peer, err) + return + } + device.log.Verbosef("%v - Rebound socket after handshake give-up (fresh port=%v)", peer, fresh) + peer.SendHandshakeInitiation(false) + }() +} + func (device *Device) BindClose() error { device.net.Lock() err := closeBindLocked(device) diff --git a/device/lx_giveup_rebind_test.go b/device/lx_giveup_rebind_test.go new file mode 100644 index 0000000..bbd4706 --- /dev/null +++ b/device/lx_giveup_rebind_test.go @@ -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) + } +} diff --git a/device/lx_giveup_selfheal_test.go b/device/lx_giveup_selfheal_test.go new file mode 100644 index 0000000..2f59471 --- /dev/null +++ b/device/lx_giveup_selfheal_test.go @@ -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) +} diff --git a/device/timers.go b/device/timers.go index d30f26b..05a5abd 100644 --- a/device/timers.go +++ b/device/timers.go @@ -99,6 +99,14 @@ func expiredRetransmitHandshake(peer *Peer) { 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 { 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) From 3909adbf87f2007d7ce87c6c3ca74a1afa243da3 Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:40:01 +0300 Subject: [PATCH 173/173] 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. --- device/device.go | 68 ++-------- device/lx_early_rebind_test.go | 58 +++++++++ device/lx_giveup_rebind.go | 143 +++++++++++++++++++++ device/lx_stale_rebind_test.go | 228 +++++++++++++++++++++++++++++++++ device/timers.go | 7 + 5 files changed, 444 insertions(+), 60 deletions(-) create mode 100644 device/lx_early_rebind_test.go create mode 100644 device/lx_giveup_rebind.go create mode 100644 device/lx_stale_rebind_test.go diff --git a/device/device.go b/device/device.go index 4358980..fe11b7a 100644 --- a/device/device.go +++ b/device/device.go @@ -123,17 +123,14 @@ type Device struct { ipackets [5]*obfChain - // lx: SPEC 041 — passive self-heal on handshake give-up. When a peer's - // handshake retry cycle exhausts (the give-up branch of - // expiredRetransmitHandshake), the device reopens its bind once — with a - // fresh ephemeral port when freshPort is set — and immediately - // re-initiates. Heals 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. Zero cost while healthy: no - // timers, no goroutines — the trigger is the existing give-up event, - // which only fires under traffic demand after ~90s of unanswered - // initiations. Enabled by default; sing-box decides freshPort from - // whether the user pinned listen_port. + // 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 @@ -803,55 +800,6 @@ func (device *Device) BindUpdate() error { return nil } -// lx: SPEC 041 — configure the handshake give-up self-heal (see the -// giveUpRebind field comment). 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) -} - -// lx: SPEC 041 — invoked from the give-up branch of -// expiredRetransmitHandshake: ~90s of initiations went unanswered, so the -// current socket's 5-tuple is proven dead. Reopen the bind (fresh ephemeral -// port when allowed) and kick a new handshake cycle immediately. Runs the -// heavy part in a goroutine so the timer callback never blocks on -// BindUpdate's worker drain. Debounced to one rebind per RekeyAttemptTime -// per device (CAS on `last` settles concurrent multi-peer give-ups). 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) handleHandshakeGiveUp(peer *Peer) { - if !device.giveUpRebind.enabled.Load() { - return - } - if device.isClosed() { - return - } - now := time.Now().Unix() - last := device.giveUpRebind.last.Load() - if now-last < int64(RekeyAttemptTime/time.Second) { - return - } - if !device.giveUpRebind.last.CompareAndSwap(last, now) { - return - } - 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 to rebind after handshake give-up: %v", peer, err) - return - } - device.log.Verbosef("%v - Rebound socket after handshake give-up (fresh port=%v)", peer, fresh) - peer.SendHandshakeInitiation(false) - }() -} - func (device *Device) BindClose() error { device.net.Lock() err := closeBindLocked(device) diff --git a/device/lx_early_rebind_test.go b/device/lx_early_rebind_test.go new file mode 100644 index 0000000..30910c1 --- /dev/null +++ b/device/lx_early_rebind_test.go @@ -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) +} diff --git a/device/lx_giveup_rebind.go b/device/lx_giveup_rebind.go new file mode 100644 index 0000000..60d6684 --- /dev/null +++ b/device/lx_giveup_rebind.go @@ -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 +} diff --git a/device/lx_stale_rebind_test.go b/device/lx_stale_rebind_test.go new file mode 100644 index 0000000..91b8a31 --- /dev/null +++ b/device/lx_stale_rebind_test.go @@ -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() + } +} diff --git a/device/timers.go b/device/timers.go index 05a5abd..9ec3d18 100644 --- a/device/timers.go +++ b/device/timers.go @@ -111,6 +111,13 @@ func expiredRetransmitHandshake(peer *Peer) { 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) + /* 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. */ peer.markEndpointSrcForClearing()