From e5feca7d61f3e88fd34b7b8bda377d35fbd4157e Mon Sep 17 00:00:00 2001 From: Leadaxe <247031499+Leadaxe@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:31:21 +0300 Subject: [PATCH] lx: re-graft AmneziaWG 2.0 obfuscation onto sagernet/wireguard-go v0.0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the AmneziaWG graft from the old sagernet base (506b763) onto v0.0.3, the base sing-box 1.14 uses. Strategy: start from v0.0.3, copy the net-new obfuscation files verbatim from the proven graft (27290b6), take the 6 modified device/ files wholesale from the graft, keep all tun/* and conn/* from v0.0.3. Net-new (copied from 27290b6): device/magic-header.go + device/obf*.go (9). Modified (from 27290b6): device/{send,receive,uapi,device,noise-protocol,cookie}.go. - receive.go: reverted two Go-1.22 'range int' loops to the v0.0.3 form (go 1.20). - cookie.go: keeps the 4-arg CreateReply(msgType) the grafted send.go calls. - noise-protocol.go: MessageEncapsulatingTransportSize=0 (AWG composes without the sagernet Bind.Send headroom prepend). §010 android UDP_GRO guard (fb8d8d8) intentionally DROPPED: v0.0.3 fixes the receive split-brain at source — bind_std.go now gates all 5 RX/offload paths on linux||android, and gso_linux.go splits coalesced packets on android. Re-inserting the guard would disable a now-working android RX-offload path. Requires on-device re-verification before release. go.mod: go 1.20, golang.org/x/sys v0.21.0 (v0.0.3 baseline). No amnezia-vpn import paths remain. Builds clean for linux/android/windows/darwin (library packages). --- 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 | 107 ++++++++++++-- device/send.go | 107 +++++++++++--- device/uapi.go | 291 +++++++++++++++++++++++++++++++++++++-- 16 files changed, 1018 insertions(+), 48 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 8afa6fd..140a5df 100644 --- a/device/device.go +++ b/device/device.go @@ -93,6 +93,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. @@ -166,7 +189,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 } @@ -303,6 +327,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 ed4d82a..745edd1 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(), } @@ -466,7 +469,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 e13c987..93117c4 100644 --- a/device/receive.go +++ b/device/receive.go @@ -69,7 +69,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) @@ -132,9 +135,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 { @@ -276,7 +284,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 { @@ -303,9 +310,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", + ) } } @@ -347,9 +359,7 @@ func (device *Device) RoutineHandshake(id int) { switch elem.msgType { case MessageInitiationType: - // unmarshal - var msg MessageInitiation err := msg.unmarshal(elem.packet) if err != nil { @@ -357,7 +367,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 { @@ -389,6 +400,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) @@ -505,11 +519,28 @@ 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)], + ) + } + + 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) @@ -536,3 +567,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 264e964..7653f62 100644 --- a/device/send.go +++ b/device/send.go @@ -6,8 +6,11 @@ package device import ( + "bytes" + "crypto/rand" "encoding/binary" "errors" + "math/big" "net" "os" "sync" @@ -127,15 +130,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) } @@ -157,9 +193,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() @@ -172,8 +210,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) } @@ -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()) 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 } @@ -477,13 +537,15 @@ func (device *Device) RoutineEncryption(id int) { for elemsContainer := range device.queue.encryption.c { for _, elem := range elemsContainer.elems { // populate header fields - header := elem.buffer[MessageEncapsulatingTransportSize : MessageEncapsulatingTransportSize+MessageTransportHeaderSize] + header := elem.buffer[:MessageTransportHeaderSize] fieldType := header[0:4] 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) @@ -500,9 +562,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.Unlock() } @@ -544,6 +603,15 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { 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)] + } bufs = append(bufs, elem.packet) } @@ -554,6 +622,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) { 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 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 +}