lx: graft AmneziaWG 2.0 obfuscation onto sagernet/wireguard-go

3-way merge of amnezia-vpn/amneziawg-go (AWG2, I1-I5 CPS + S4 keepalive) into
sagernet/wireguard-go@506b763185 (the base sing-box uses). Keeps sagernet's
sing-box-compat additions (Send offset contract, InputPacket, conn reserved/
control) and neutralizes the encapsulating headroom (MessageEncapsulatingTransportSize=0)
so obfuscation composes cleanly. Obfuscation lives in device/ (obf*.go, magic-header.go
new files + send/receive/device/uapi grafts); conn/tun/ipc kept pure sagernet.

LIVE-VALIDATED: handshake + keepalive + traffic against a real AmneziaWG 2.0 server.
This commit is contained in:
Leadaxe 2026-06-09 16:49:21 +03:00
commit 27290b6de3
60 changed files with 1215 additions and 167 deletions

41
.github/workflows/build-if-tag.yml vendored Normal file
View file

@ -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 }}

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM golang:1.24.4 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.19
ARG AWGTOOLS_RELEASE="1.0.20250901"
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 && \
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

View file

@ -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 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 this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to the Software without restriction, including without limitation the rights to

View file

@ -9,23 +9,23 @@ MAKEFLAGS += --no-print-directory
generate-version-and-build: generate-version-and-build:
@export GIT_CEILING_DIRECTORIES="$(realpath $(CURDIR)/..)" && \ @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")" && \ ver="$$(printf 'package main\n\nconst Version = "%s"\n' "$$tag")" && \
[ "$$(cat version.go 2>/dev/null)" != "$$ver" ] && \ [ "$$(cat version.go 2>/dev/null)" != "$$ver" ] && \
echo "$$ver" > version.go && \ echo "$$ver" > version.go && \
git update-index --assume-unchanged version.go || true 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 "$@" go build -v -o "$@"
install: wireguard-go install: amneziawg-go
@install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 "$<" "$(DESTDIR)$(BINDIR)/wireguard-go" @install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 "$<" "$(DESTDIR)$(BINDIR)/amneziawg-go"
test: test:
go test ./... go test ./...
clean: clean:
rm -f wireguard-go rm -f amneziawg-go
.PHONY: all clean test install generate-version-and-build .PHONY: all clean test install generate-version-and-build

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
// Package conn implements WireGuard's network connections. // Package conn implements WireGuard's network connections.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -223,19 +223,11 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix)
} }
} }
func (table *AllowedIPs) RemoveByPeer(peer *Peer) { func (node *trieEntry) remove() {
table.mutex.Lock()
defer table.mutex.Unlock()
var next *list.Element
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
next = elem.Next()
node := elem.Value.(*trieEntry)
node.removeFromPeerEntries() node.removeFromPeerEntries()
node.peer = nil node.peer = nil
if node.child[0] != nil && node.child[1] != nil { if node.child[0] != nil && node.child[1] != nil {
continue return
} }
bit := 0 bit := 0
if node.child[0] == nil { if node.child[0] == nil {
@ -248,12 +240,12 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
*node.parent.parentBit = child *node.parent.parentBit = child
if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 { if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 {
node.zeroizePointers() node.zeroizePointers()
continue return
} }
parent := (*trieEntry)(unsafe.Pointer(uintptr(unsafe.Pointer(node.parent.parentBit)) - unsafe.Offsetof(node.child) - unsafe.Sizeof(node.child[0])*uintptr(node.parent.parentBitType))) 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 { if parent.peer != nil {
node.zeroizePointers() node.zeroizePointers()
continue return
} }
child = parent.child[node.parent.parentBitType^1] child = parent.child[node.parent.parentBitType^1]
if child != nil { if child != nil {
@ -262,6 +254,37 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
*parent.parent.parentBit = child *parent.parent.parentBit = child
node.zeroizePointers() node.zeroizePointers()
parent.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()
var next *list.Element
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
next = elem.Next()
elem.Value.(*trieEntry).remove()
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -93,6 +93,29 @@ type Device struct {
closed chan struct{} closed chan struct{}
log *Logger log *Logger
pauseManager pause.Manager 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. // deviceState represents the state of a Device.
@ -166,7 +189,8 @@ func (device *Device) changeState(want deviceState) (err error) {
err = errDown err = errDown
} }
} }
device.log.Verbosef("Interface state was %s, requested %s, now %s", old, want, device.deviceState()) device.log.Verbosef(
"Interface state was %s, requested %s, now %s", old, want, device.deviceState())
return return
} }
@ -303,6 +327,11 @@ func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger
device.rate.limiter.Init() device.rate.limiter.Init()
device.indexTable.Init() device.indexTable.Init()
device.headers.init = &magicHeader{start: MessageInitiationType, end: MessageInitiationType}
device.headers.response = &magicHeader{start: MessageResponseType, end: MessageResponseType}
device.headers.cookie = &magicHeader{start: MessageCookieReplyType, end: MessageCookieReplyType}
device.headers.transport = &magicHeader{start: MessageTransportType, end: MessageTransportType}
device.PopulatePools() device.PopulatePools()
// create queues // create queues

View file

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

View file

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

View file

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

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

@ -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())
}

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -54,10 +54,11 @@ const (
) )
const ( const (
MessageInitiationType = 1 MessageUnknownType uint32 = 0
MessageResponseType = 2 MessageInitiationType uint32 = 1
MessageCookieReplyType = 3 MessageResponseType uint32 = 2
MessageTransportType = 4 MessageCookieReplyType uint32 = 3
MessageTransportType uint32 = 4
) )
const ( const (
@ -65,7 +66,7 @@ const (
MessageResponseSize = 92 // size of response message MessageResponseSize = 92 // size of response message
MessageCookieReplySize = 64 // size of cookie reply message MessageCookieReplySize = 64 // size of cookie reply message
MessageTransportHeaderSize = 16 // size of data preceding content in transport message MessageTransportHeaderSize = 16 // size of data preceding content in transport message
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 MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport
MessageKeepaliveSize = MessageTransportSize // size of keepalive MessageKeepaliveSize = MessageTransportSize // size of keepalive
MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message
@ -287,8 +288,10 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
handshake.mixHash(handshake.remoteStatic[:]) handshake.mixHash(handshake.remoteStatic[:])
msgType := device.headers.init.Generate()
msg := MessageInitiation{ msg := MessageInitiation{
Type: MessageInitiationType, Type: msgType,
Ephemeral: handshake.localEphemeral.publicKey(), Ephemeral: handshake.localEphemeral.publicKey(),
} }
@ -466,7 +469,7 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
} }
var msg MessageResponse var msg MessageResponse
msg.Type = MessageResponseType msg.Type = device.headers.response.Generate()
msg.Sender = handshake.localIndex msg.Sender = handshake.localIndex
msg.Receiver = handshake.remoteIndex msg.Receiver = handshake.remoteIndex

View file

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

140
device/obf.go Normal file
View file

@ -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
}

47
device/obf_bytes.go Normal file
View file

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

25
device/obf_data.go Normal file
View file

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

38
device/obf_datasize.go Normal file
View file

@ -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
}

29
device/obf_datastring.go Normal file
View file

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

39
device/obf_rand.go Normal file
View file

@ -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
}

48
device/obf_randchars.go Normal file
View file

@ -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
}

48
device/obf_randdigits.go Normal file
View file

@ -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
}

31
device/obf_timestamp.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
@ -69,7 +69,10 @@ func (peer *Peer) keepKeyFreshReceiving() {
* Every time the bind is updated a new routine is started for * Every time the bind is updated a new routine is started for
* IPv4 and IPv6 (separately) * IPv4 and IPv6 (separately)
*/ */
func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.ReceiveFunc) { func (device *Device) RoutineReceiveIncoming(
maxBatchSize int,
recv conn.ReceiveFunc,
) {
recvName := recv.PrettyName() recvName := recv.PrettyName()
defer func() { defer func() {
device.log.Verbosef("Routine: receive incoming %s - stopped", recvName) device.log.Verbosef("Routine: receive incoming %s - stopped", recvName)
@ -93,13 +96,13 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize) elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize)
) )
for i := range bufsArrs { for i := range maxBatchSize {
bufsArrs[i] = device.GetMessageBuffer() bufsArrs[i] = device.GetMessageBuffer()
bufs[i] = bufsArrs[i][:] bufs[i] = bufsArrs[i][:]
} }
defer func() { defer func() {
for i := 0; i < maxBatchSize; i++ { for i := range maxBatchSize {
if bufsArrs[i] != nil { if bufsArrs[i] != nil {
device.PutMessageBuffer(bufsArrs[i]) device.PutMessageBuffer(bufsArrs[i])
} }
@ -132,9 +135,14 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
} }
// check size of packet // check size of packet
packet := bufsArrs[i][:size] packet := bufsArrs[i][:size]
msgType := binary.LittleEndian.Uint32(packet[:4])
// get message padding and type based on information from S1-S4 and H1-H4
msgType, padding := device.DeterminePacketTypeAndPadding(packet, MessageUnknownType)
if padding > 0 {
copy(packet, packet[padding:])
packet = packet[:len(packet)-padding]
}
switch msgType { switch msgType {
@ -276,7 +284,6 @@ func (device *Device) RoutineHandshake(id int) {
device.log.Verbosef("Routine: handshake worker %d - started", id) device.log.Verbosef("Routine: handshake worker %d - started", id)
for elem := range device.queue.handshake.c { for elem := range device.queue.handshake.c {
// handle cookie fields and ratelimiting // handle cookie fields and ratelimiting
switch elem.msgType { switch elem.msgType {
@ -303,9 +310,14 @@ func (device *Device) RoutineHandshake(id int) {
// consume reply // consume reply
if peer := entry.peer; peer.isRunning.Load() { if peer := entry.peer; peer.isRunning.Load() {
device.log.Verbosef("Receiving cookie response from %s", elem.endpoint.DstToString()) device.log.Verbosef(
"Receiving cookie response from %s",
elem.endpoint.DstToString(),
)
if !peer.cookieGenerator.ConsumeReply(&reply) { if !peer.cookieGenerator.ConsumeReply(&reply) {
device.log.Verbosef("Could not decrypt invalid cookie response") device.log.Verbosef(
"Could not decrypt invalid cookie response",
)
} }
} }
@ -347,9 +359,7 @@ func (device *Device) RoutineHandshake(id int) {
switch elem.msgType { switch elem.msgType {
case MessageInitiationType: case MessageInitiationType:
// unmarshal // unmarshal
var msg MessageInitiation var msg MessageInitiation
err := msg.unmarshal(elem.packet) err := msg.unmarshal(elem.packet)
if err != nil { if err != nil {
@ -357,7 +367,8 @@ func (device *Device) RoutineHandshake(id int) {
goto skip goto skip
} }
// consume initiation // have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
peer := device.ConsumeMessageInitiation(&msg, elem.endpoint) peer := device.ConsumeMessageInitiation(&msg, elem.endpoint)
if peer == nil { if peer == nil {
@ -389,6 +400,9 @@ func (device *Device) RoutineHandshake(id int) {
goto skip goto skip
} }
// have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
// consume response // consume response
peer := device.ConsumeMessageResponse(&msg) peer := device.ConsumeMessageResponse(&msg)
@ -505,11 +519,28 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
} }
default: default:
device.log.Verbosef("Packet with invalid IP version from %v", peer) device.log.Verbosef(
"Packet with invalid IP version from %v",
peer,
)
continue continue
} }
bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)]) 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()
} }
peer.rxBytes.Add(rxBytesLen) peer.rxBytes.Add(rxBytesLen)
@ -536,3 +567,57 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
device.PutInboundElementsContainer(elemsContainer) 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
}

View file

@ -1,13 +1,16 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package device package device
import ( import (
"bytes"
"crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"math/big"
"net" "net"
"os" "os"
"sync" "sync"
@ -127,15 +130,48 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err return err
} }
buf := make([]byte, MessageEncapsulatingTransportSize+MessageInitiationSize) var sendBuffer [][]byte
packet := buf[MessageEncapsulatingTransportSize:]
_ = msg.marshal(packet) 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.cookieGenerator.AddMacs(packet)
peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent() 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 { if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err) peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err)
} }
@ -157,9 +193,11 @@ func (peer *Peer) SendHandshakeResponse() error {
return err return err
} }
buf := make([]byte, MessageEncapsulatingTransportSize+MessageResponseSize) var buf [MessageResponseSize]byte
packet := buf[MessageEncapsulatingTransportSize:] writer := bytes.NewBuffer(buf[:0])
_ = response.marshal(packet)
binary.Write(writer, binary.LittleEndian, response)
packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet) peer.cookieGenerator.AddMacs(packet)
err = peer.BeginSymmetricSession() err = peer.BeginSymmetricSession()
@ -172,8 +210,15 @@ func (peer *Peer) SendHandshakeResponse() error {
peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent() peer.timersAnyAuthenticatedPacketSent()
if padding := peer.device.paddings.response; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
// TODO: allocation could be avoided // TODO: allocation could be avoided
err = peer.SendBuffers([][]byte{buf}) err = peer.SendBuffers([][]byte{packet})
if err != nil { if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err) peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err)
} }
@ -184,18 +229,33 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString()) device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString())
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8]) sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes()) msgType := device.headers.cookie.Generate()
reply, err := device.cookieChecker.CreateReply(
initiatingElem.packet,
sender,
initiatingElem.endpoint.DstToBytes(),
msgType,
)
if err != nil { if err != nil {
device.log.Errorf("Failed to create cookie reply: %v", err) device.log.Errorf("Failed to create cookie reply: %v", err)
return err return err
} }
buf := make([]byte, MessageEncapsulatingTransportSize+MessageCookieReplySize) var buf [MessageCookieReplySize]byte
packet := buf[MessageEncapsulatingTransportSize:] writer := bytes.NewBuffer(buf[:0])
_ = reply.marshal(packet) binary.Write(writer, binary.LittleEndian, reply)
// TODO: allocation could be avoided packet := writer.Bytes()
device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint, MessageEncapsulatingTransportSize)
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 return nil
} }
@ -477,13 +537,15 @@ func (device *Device) RoutineEncryption(id int) {
for elemsContainer := range device.queue.encryption.c { for elemsContainer := range device.queue.encryption.c {
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
// populate header fields // populate header fields
header := elem.buffer[MessageEncapsulatingTransportSize : MessageEncapsulatingTransportSize+MessageTransportHeaderSize] header := elem.buffer[:MessageTransportHeaderSize]
fieldType := header[0:4] fieldType := header[0:4]
fieldReceiver := header[4:8] fieldReceiver := header[4:8]
fieldNonce := header[8:16] fieldNonce := header[8:16]
binary.LittleEndian.PutUint32(fieldType, MessageTransportType) msgType := device.headers.transport.Generate()
binary.LittleEndian.PutUint32(fieldType, msgType)
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex) binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
@ -500,9 +562,6 @@ func (device *Device) RoutineEncryption(id int) {
elem.packet, elem.packet,
nil, nil,
) )
// re-slice packet to include encapsulating transport space
elem.packet = elem.buffer[:MessageEncapsulatingTransportSize+len(elem.packet)]
} }
elemsContainer.Unlock() elemsContainer.Unlock()
} }
@ -541,9 +600,18 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
dataSent := false dataSent := false
elemsContainer.Lock() elemsContainer.Lock()
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
if len(elem.packet[MessageEncapsulatingTransportSize:]) != MessageKeepaliveSize { if len(elem.packet) != MessageKeepaliveSize {
dataSent = true 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)]
}
bufs = append(bufs, elem.packet) bufs = append(bufs, elem.packet)
} }
@ -554,6 +622,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
if dataSent { if dataSent {
peer.timersDataSent() peer.timersDataSent()
} }
for _, elem := range elemsContainer.elems { for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer) device.PutMessageBuffer(elem.buffer)
device.PutOutboundElement(elem) device.PutOutboundElement(elem)

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
* *
* This is based heavily on timers.c from the kernel implementation. * This is based heavily on timers.c from the kernel implementation.
*/ */

View file

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

2
go.mod
View file

@ -1,6 +1,6 @@
module github.com/sagernet/wireguard-go module github.com/sagernet/wireguard-go
go 1.20 go 1.24
require ( require (
github.com/sagernet/sing v0.7.10 github.com/sagernet/sing v0.7.10

View file

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

View file

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

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package ipc package ipc
@ -26,7 +26,7 @@ const (
// socketDirectory is variable because it is modified by a linker // socketDirectory is variable because it is modified by a linker
// flag in wireguard-android. // flag in wireguard-android.
var socketDirectory = "/var/run/wireguard" var socketDirectory = "/var/run/amneziawg"
func sockPath(iface string) string { func sockPath(iface string) string {
return fmt.Sprintf("%s/%s.sock", socketDirectory, iface) return fmt.Sprintf("%s/%s.sock", socketDirectory, iface)

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
// Package replay implements an efficient anti-replay algorithm as specified in RFC 6479. // Package replay implements an efficient anti-replay algorithm as specified in RFC 6479.

View file

@ -2,7 +2,7 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
// Package rwcancel implements cancelable read/write operations on // Package rwcancel implements cancelable read/write operations on
@ -64,7 +64,7 @@ func (rw *RWCancel) ReadyRead() bool {
func (rw *RWCancel) ReadyWrite() bool { func (rw *RWCancel) ReadyWrite() bool {
closeFd := int32(rw.closingReader.Fd()) 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 var err error
for { for {
_, err = unix.Poll(pollFds, -1) _, err = unix.Poll(pollFds, -1)

View file

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

View file

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

View file

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

View file

@ -1,19 +1,17 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package tun package tun
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"os" "os"
"sync" "sync"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@ -30,18 +28,6 @@ type NativeTun struct {
closeOnce sync.Once 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) { func (tun *NativeTun) routineRouteListener(tunIfindex int) {
var ( var (
statusUp bool statusUp bool
@ -62,26 +48,22 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) {
return return
} }
if n < 14 { if n < 28 {
continue continue
} }
if data[3 /* type */] != unix.RTM_IFINFO { if data[3 /* ifm_type */] != unix.RTM_IFINFO {
continue continue
} }
ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifindex */]))) ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifm_index */])))
if ifindex != tunIfindex { if ifindex != tunIfindex {
continue continue
} }
iface, err := retryInterfaceByIndex(ifindex) flags := int(*(*uint32)(unsafe.Pointer(&data[8 /* ifm_flags */])))
if err != nil {
tun.errors <- err
return
}
// Up / Down event // Up / Down event
up := (iface.Flags & net.FlagUp) != 0 up := (flags & syscall.IFF_UP) != 0
if up != statusUp && up { if up != statusUp && up {
tun.events <- EventUp tun.events <- EventUp
} }
@ -90,11 +72,13 @@ func (tun *NativeTun) routineRouteListener(tunIfindex int) {
} }
statusUp = up statusUp = up
mtu := int(*(*uint32)(unsafe.Pointer(&data[24 /* ifm_data.ifi_mtu */])))
// MTU changes // MTU changes
if iface.MTU != statusMTU { if mtu != statusMTU {
tun.events <- EventMTUUpdate tun.events <- EventMTUUpdate
} }
statusMTU = iface.MTU statusMTU = mtu
} }
} }

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/ */
package tun package tun
@ -160,11 +160,10 @@ retry:
packet, err := tun.session.ReceivePacket() packet, err := tun.session.ReceivePacket()
switch err { switch err {
case nil: case nil:
packetSize := len(packet) n := copy(bufs[0][offset:], packet)
copy(bufs[0][offset:], packet) sizes[0] = n
sizes[0] = packetSize
tun.session.ReleaseReceivePacket(packet) tun.session.ReleaseReceivePacket(packet)
tun.rate.update(uint64(packetSize)) tun.rate.update(uint64(n))
return 1, nil return 1, nil
case windows.ERROR_NO_MORE_ITEMS: case windows.ERROR_NO_MORE_ITEMS:
if !shouldSpin || uint64(nanotime()-start) >= spinloopDuration { if !shouldSpin || uint64(nanotime()-start) >= spinloopDuration {

View file

@ -1,3 +1,3 @@
package main package main
const Version = "0.0.20230223" const Version = "0.0.20250522"