feat: compat mode with previous AWG impls of H1-H4

This commit is contained in:
Yaroslav Gurov
2026-01-20 23:22:02 +00:00
parent e69a0c191f
commit c0d6e66c59
9 changed files with 207 additions and 87 deletions
+103 -59
View File
@@ -10,18 +10,19 @@ import (
)
type FramedOpts struct {
H1 *rangedHeader
H2 *rangedHeader
H3 *rangedHeader
H4 *rangedHeader
S1 int
S2 int
S3 int
S4 int
H1 *RangedHeader
H2 *RangedHeader
H3 *RangedHeader
H4 *RangedHeader
S1 int
S2 int
S3 int
S4 int
HeaderCompat bool
}
func (o *FramedOpts) HasIntersections() bool {
headers := []*rangedHeader{o.H1, o.H2, o.H3, o.H4}
headers := []*RangedHeader{o.H1, o.H2, o.H3, o.H4}
for i := range len(headers) {
left := headers[i]
@@ -51,28 +52,28 @@ func newFrameEncoding(opts FramedOpts) (e frameEncoding, ok bool) {
e.header.initial = *opts.H1
ok = true
} else {
e.header.initial = rangedHeader{WireguardMsgInitiationType, WireguardMsgInitiationType}
e.header.initial = RangedHeader{WireguardMsgInitiationType, WireguardMsgInitiationType}
}
if opts.H2 != nil {
e.header.response = *opts.H2
ok = true
} else {
e.header.response = rangedHeader{WireguardMsgResponseType, WireguardMsgResponseType}
e.header.response = RangedHeader{WireguardMsgResponseType, WireguardMsgResponseType}
}
if opts.H3 != nil {
e.header.cookie = *opts.H3
ok = true
} else {
e.header.cookie = rangedHeader{WireguardMsgCookieReplyType, WireguardMsgCookieReplyType}
e.header.cookie = RangedHeader{WireguardMsgCookieReplyType, WireguardMsgCookieReplyType}
}
if opts.H4 != nil {
e.header.transport = *opts.H4
ok = true
} else {
e.header.transport = rangedHeader{WireguardMsgTransportType, WireguardMsgTransportType}
e.header.transport = RangedHeader{WireguardMsgTransportType, WireguardMsgTransportType}
}
if opts.S1 != 0 {
@@ -95,15 +96,16 @@ func newFrameEncoding(opts FramedOpts) (e frameEncoding, ok bool) {
ok = true
}
e.compat = opts.HeaderCompat
return e, ok
}
type frameEncoding struct {
header struct {
initial rangedHeader
response rangedHeader
cookie rangedHeader
transport rangedHeader
initial RangedHeader
response RangedHeader
cookie RangedHeader
transport RangedHeader
}
padding struct {
initial int
@@ -114,87 +116,127 @@ type frameEncoding struct {
compat bool
}
func (e *frameEncoding) encodeOne(dst, src []byte, header rangedHeader, padding int) int {
func encodeOne(dst, src []byte, header RangedHeader, padding int) int {
rand.Read(dst[:padding])
dst = dst[padding:]
binary.LittleEndian.PutUint32(dst[:4], header.Generate())
dst = dst[4:]
n := copy(dst, src[4:])
return padding + 4 + n
}
func encodeOneCompat(dst, src []byte, padding int) int {
rand.Read(dst[:padding])
n := copy(dst[padding:], src)
if !e.compat {
binary.LittleEndian.PutUint32(dst[padding:padding+4], header.Generate())
}
return padding + n
}
func (e *frameEncoding) Encode(dst, src []byte) int {
if len(src) < 4 {
return 0
}
header := binary.LittleEndian.Uint32(src[:4])
if e.compat {
if e.header.initial.Validate(header) {
return e.encodeOne(dst, src, e.header.initial, e.padding.initial)
return encodeOneCompat(dst, src, e.padding.initial)
} else if e.header.response.Validate(header) {
return e.encodeOne(dst, src, e.header.response, e.padding.response)
return encodeOneCompat(dst, src, e.padding.response)
} else if e.header.cookie.Validate(header) {
return e.encodeOne(dst, src, e.header.cookie, e.padding.cookie)
return encodeOneCompat(dst, src, e.padding.cookie)
} else if e.header.transport.Validate(header) {
return e.encodeOne(dst, src, e.header.transport, e.padding.transport)
return encodeOneCompat(dst, src, e.padding.transport)
}
} else {
switch src[0] {
case WireguardMsgInitiationType:
return e.encodeOne(dst, src, e.header.initial, e.padding.initial)
return encodeOne(dst, src, e.header.initial, e.padding.initial)
case WireguardMsgResponseType:
return e.encodeOne(dst, src, e.header.response, e.padding.response)
return encodeOne(dst, src, e.header.response, e.padding.response)
case WireguardMsgCookieReplyType:
return e.encodeOne(dst, src, e.header.cookie, e.padding.cookie)
return encodeOne(dst, src, e.header.cookie, e.padding.cookie)
case WireguardMsgTransportType:
return e.encodeOne(dst, src, e.header.transport, e.padding.transport)
return encodeOne(dst, src, e.header.transport, e.padding.transport)
}
}
return 0
}
func (e *frameEncoding) decodeOne(b []byte, header rangedHeader, padding int, originalHeader uint32) (res []byte, ok bool) {
func decodeOneCompat(b []byte, header RangedHeader, padding int) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return nil, false
return 0
}
n := copy(b, bb)
b = b[:n]
if !e.compat {
binary.LittleEndian.PutUint32(b[:4], originalHeader)
}
return b, true
return copy(b, bb)
}
func (e *frameEncoding) Decode(b []byte) []byte {
func decodeOne(b []byte, header RangedHeader, padding int, originalHeader uint32) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return 0
}
binary.LittleEndian.PutUint32(b[:4], originalHeader)
n := copy(b[4:], bb[4:])
return 4 + n
}
func (e *frameEncoding) Decode(b []byte) int {
if len(b) == WireguardMsgInitiationSize+e.padding.initial {
if bb, ok := e.decodeOne(b, e.header.initial, e.padding.initial, WireguardMsgInitiationType); ok {
return bb
if e.compat {
if n := decodeOneCompat(b, e.header.initial, e.padding.initial); n != 0 {
return n
}
} else {
if n := decodeOne(b, e.header.initial, e.padding.initial, WireguardMsgInitiationType); n != 0 {
return n
}
}
}
if len(b) == WireguardMsgResponseSize+e.padding.response {
if bb, ok := e.decodeOne(b, e.header.response, e.padding.response, WireguardMsgResponseType); ok {
return bb
if e.compat {
if n := decodeOneCompat(b, e.header.response, e.padding.response); n != 0 {
return n
}
} else {
if n := decodeOne(b, e.header.response, e.padding.response, WireguardMsgResponseType); n != 0 {
return n
}
}
}
if len(b) == WireguardMsgCookieReplySize+e.padding.cookie {
if bb, ok := e.decodeOne(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplyType); ok {
return bb
if e.compat {
if n := decodeOneCompat(b, e.header.cookie, e.padding.cookie); n != 0 {
return n
}
} else {
if n := decodeOne(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplyType); n != 0 {
return n
}
}
}
if len(b) >= WireguardMsgTransportMinSize+e.padding.transport {
if bb, ok := e.decodeOne(b, e.header.transport, e.padding.transport, WireguardMsgTransportType); ok {
return bb
if e.compat {
if n := decodeOneCompat(b, e.header.transport, e.padding.transport); n != 0 {
return n
}
} else {
if n := decodeOne(b, e.header.transport, e.padding.transport, WireguardMsgTransportType); n != 0 {
return n
}
}
}
return b
return len(b)
}
func NewFramedConn(conn net.Conn, pool *sync.Pool, opts FramedOpts) (c *FramedConn, ok bool) {
@@ -218,8 +260,8 @@ type FramedConn struct {
func (c *FramedConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
b = c.enc.Decode(b[:n])
return len(b), err
n = c.enc.Decode(b[:n])
return n, err
}
func (c *FramedConn) Write(b []byte) (n int, err error) {
@@ -254,8 +296,8 @@ type FramedUDPConn struct {
func (c *FramedUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
b = c.enc.Decode(b[:n])
return len(b), oobn, flags, addr, err
n = c.enc.Decode(b[:n])
return n, oobn, flags, addr, err
}
func (c *FramedUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
@@ -290,14 +332,16 @@ type FramedBatchConn struct {
func (c *FramedBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
n, err = c.BatchConn.ReadBatch(ms, flags)
if err != nil {
return 0, err
}
for i := range ms[:n] {
b := ms[i].Buffers[0][:ms[i].N]
b = c.enc.Decode(b)
ms[i].N = len(b)
ms[i].N = c.enc.Decode(b)
}
return n, err
return n, nil
}
func (c *FramedBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
@@ -309,7 +353,7 @@ func (c *FramedBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err e
ms[i].Buffers[0] = t[:n]
}
// ms[x].N has incorrect N because the original data was modifier above
// ms[i].N has incorrect N because the original data was modifier above
// however, WG does not check this field, so this is fine
return c.BatchConn.WriteBatch(ms, flags)
}
+6 -6
View File
@@ -9,12 +9,12 @@ import (
"strings"
)
type rangedHeader struct {
type RangedHeader struct {
start uint32
end uint32
}
func NewRangedHeader(spec string) (*rangedHeader, error) {
func NewRangedHeader(spec string) (*RangedHeader, error) {
parts := strings.Split(spec, "-")
if len(parts) < 1 || len(parts) > 2 {
return nil, errors.New("bad format")
@@ -39,24 +39,24 @@ func NewRangedHeader(spec string) (*rangedHeader, error) {
return nil, errors.New("wrong range specified")
}
return &rangedHeader{
return &RangedHeader{
start: uint32(start),
end: uint32(end),
}, nil
}
func (h *rangedHeader) GenSpec() string {
func (h *RangedHeader) GenSpec() string {
if h.start == h.end {
return fmt.Sprintf("%d", h.start)
}
return fmt.Sprintf("%d-%d", h.start, h.end)
}
func (h *rangedHeader) Validate(val uint32) bool {
func (h *RangedHeader) Validate(val uint32) bool {
return h.start <= val && val <= h.end
}
func (h *rangedHeader) Generate() uint32 {
func (h *RangedHeader) Generate() uint32 {
high := int64(h.end - h.start + 1)
r, _ := rand.Int(rand.Reader, big.NewInt(high))
return h.start + uint32(r.Int64())
+2 -1
View File
@@ -118,6 +118,7 @@ func (st *CookieChecker) CreateReply(
msg []byte,
recv uint32,
src []byte,
typ 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 = typ
reply.Receiver = recv
_, err := rand.Read(reply.Nonce[:])
+1 -1
View File
@@ -99,7 +99,7 @@ func TestCookieMAC1(t *testing.T) {
0x8c, 0xe1, 0xe8, 0xfa, 0x67, 0x20, 0x80, 0x6d,
}
generator.AddMacs(msg)
reply, err := checker.CreateReply(msg, 1377, src)
reply, err := checker.CreateReply(msg, 1377, src, MessageCookieReplyType)
if err != nil {
t.Fatal("Failed to create cookie reply:", err)
}
+6 -6
View File
@@ -41,12 +41,11 @@ type Device struct {
net struct {
stopping sync.WaitGroup
sync.RWMutex
bind conn.Bind // bind interface
netlinkCancel *rwcancel.RWCancel
port uint16 // listening port
fwmark uint32 // mark value (0 = disabled)
brokenRoaming bool
bind conn.Bind // bind interface
netlinkCancel *rwcancel.RWCancel
port uint16 // listening port
fwmark uint32 // mark value (0 = disabled)
brokenRoaming bool
network string
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
@@ -306,6 +305,7 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
device.indexTable.Init()
device.net.network = "udp"
device.net.framedOpts.HeaderCompat = true
device.PopulatePools()
+36 -6
View File
@@ -193,8 +193,16 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
handshake.mixHash(handshake.remoteStatic[:])
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H1 != nil {
typ = opts.H1.Generate()
} else {
typ = MessageInitiationType
}
msg := MessageInitiation{
Type: MessageInitiationType,
Type: typ,
Ephemeral: handshake.localEphemeral.publicKey(),
}
@@ -250,8 +258,15 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
chainKey [blake2s.Size]byte
)
if msg.Type != MessageInitiationType {
return nil
opts := &device.net.framedOpts
if opts.HeaderCompat && opts.H1 != nil {
if !opts.H1.Validate(msg.Type) {
return nil
}
} else {
if msg.Type != MessageInitiationType {
return nil
}
}
device.staticIdentity.RLock()
@@ -366,8 +381,16 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
return nil, err
}
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H2 != nil {
typ = opts.H2.Generate()
} else {
typ = MessageResponseType
}
var msg MessageResponse
msg.Type = MessageResponseType
msg.Type = typ
msg.Sender = handshake.localIndex
msg.Receiver = handshake.remoteIndex
@@ -417,8 +440,15 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
}
func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
if msg.Type != MessageResponseType {
return nil
opts := &device.net.framedOpts
if opts.HeaderCompat && opts.H2 != nil {
if !opts.H2.Validate(msg.Type) {
return nil
}
} else {
if msg.Type != MessageResponseType {
return nil
}
}
// lookup handshake by receiver
+13
View File
@@ -137,6 +137,19 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
packet := bufsArrs[i][:size]
msgType := binary.LittleEndian.Uint32(packet[:4])
opts := &device.net.framedOpts
if opts.HeaderCompat {
if opts.H1 != nil && opts.H1.Validate(msgType) {
msgType = MessageInitiationType
} else if opts.H2 != nil && opts.H2.Validate(msgType) {
msgType = MessageResponseType
} else if opts.H3 != nil && opts.H3.Validate(msgType) {
msgType = MessageCookieReplyType
} else if opts.H4 != nil && opts.H4.Validate(msgType) {
msgType = MessageTransportType
}
}
switch msgType {
// check if transport
+18 -2
View File
@@ -183,8 +183,16 @@ func (peer *Peer) SendHandshakeResponse() error {
func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) error {
device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString())
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H3 != nil {
typ = opts.H3.Generate()
} else {
typ = MessageCookieReplyType
}
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes())
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes(), typ)
if err != nil {
device.log.Errorf("Failed to create cookie reply: %v", err)
return err
@@ -460,7 +468,15 @@ func (device *Device) RoutineEncryption(id int) {
fieldReceiver := header[4:8]
fieldNonce := header[8:16]
binary.LittleEndian.PutUint32(fieldType, MessageTransportType)
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H4 != nil {
typ = opts.H4.Generate()
} else {
typ = MessageTransportType
}
binary.LittleEndian.PutUint32(fieldType, typ)
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
+22 -6
View File
@@ -160,6 +160,8 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
sendf("format_out=%s", device.net.masqueradeOpts.RulesOut.Spec())
}
sendf("header_compat=%s", strconv.FormatBool(device.net.framedOpts.HeaderCompat))
for _, peer := range device.peers.keyMap {
// Serialize peer state.
peer.handshake.mutex.RLock()
@@ -573,29 +575,43 @@ func (device *Device) handleDeviceLine(key, value string) error {
case "format_in":
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse obfuscators: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
}
device.log.Verbosef("UAPI: Updating fmt_in")
device.log.Verbosef("UAPI: Updating format_in")
device.net.masqueradeOpts.RulesIn = rules
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set fmt_in: %w", err)
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set format_in: %w", err)
}
case "format_out":
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse obfuscators: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
}
device.log.Verbosef("UAPI: Updating fmt_out")
device.log.Verbosef("UAPI: Updating format_out")
device.net.masqueradeOpts.RulesOut = rules
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set fmt_out: %w", err)
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set format_out: %w", err)
}
case "header_compat":
compat, err := strconv.ParseBool(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse header_compat: %w", err)
}
device.log.Verbosef("UAPI: Updating header_compat")
device.net.framedOpts.HeaderCompat = compat
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set header_compat: %w", err)
}
default: