diff --git a/README.md b/README.md index b4ce9108..bae87c2c 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,88 @@ make # Install ```bash -go install github.com/artem-russkikh/wireproxy-awg/cmd/wireproxy@v1.0.17 # or @latest +go install github.com/artem-russkikh/wireproxy-awg/cmd/wireproxy@v1.0.18 # or @latest ``` # Use with VPN Instructions for using wireproxy with Firefox container tabs and auto-start on MacOS can be found [here](/UseWithVPN.md). +# AmneziaWG parameters + +This fork supports AmneziaWG 1.0, 2.0, 3.0, and 3.1. The obfuscation parameters go +into the `[Interface]` section, next to the usual wireguard ones, and use the +same names as an `awg-quick` configuration, so a config exported from the +Amnezia client can be pasted in as is. Every parameter is optional: with none +of them set wireproxy behaves like plain wireguard. + +Values written as a *range* accept either a single number (`25`) or an interval +(`15-25`), in which case a random value inside the interval is picked for every +packet. + +### Junk packets (AmneziaWG 1.0) + +| Parameter | Value | Meaning | +| --- | --- | --- | +| `Jc` | 1-128 | number of junk packets sent before every handshake | +| `Jmin` | bytes | minimum junk packet size | +| `Jmax` | bytes, <= 1280 | maximum junk packet size | +| `S1` | bytes | random padding prepended to the handshake initiation message | +| `S2` | bytes | random padding prepended to the handshake response message | +| `H1` | range | message type of the handshake initiation message | +| `H2` | range | message type of the handshake response message | +| `H3` | range | message type of the cookie reply message | +| `H4` | range | message type of the transport message | + +`H1`-`H4` must not overlap, and `S1` + 148 must differ from `S2` + 92. + +### Signature packets (AmneziaWG 2.0) + +| Parameter | Value | Meaning | +| --- | --- | --- | +| `S3` | bytes | random padding prepended to the cookie reply message | +| `S4` | bytes | random padding prepended to transport messages | +| `I1` - `I5` | tag sequence | custom packets sent before every handshake, in order | + +The `I1`-`I5` value is a sequence of tags: + +| Tag | Meaning | +| --- | --- | +| `` | the given bytes, as is | +| `` | `size` random bytes | +| `` | `size` random digits | +| `` | `size` random letters | +| `` | current time, 4 bytes, UNIX format | + +### Header protection, content padding and timings (AmneziaWG 3.0) + +| Parameter | Value | Meaning | +| --- | --- | --- | +| `HeaderProtectionKey` | base64 key | encrypts the low entropy fields of every packet header | +| `ContentPaddingAddition` | range | extra random padding added to transport messages | +| `RekeyAfterTime` | range, seconds | time after which a new handshake is started | +| `RekeyTimeout` | range, seconds | time after which a handshake is retried | +| `RejectAfterTime` | range, seconds | time after which the keys are no longer used | +| `KeepaliveTimeout` | range, seconds | idle time after which a keepalive is sent | +| `MaxHandshakeAttempts` | range | how many times a handshake is retried | + +`HeaderProtectionKey` is generated with `awg genkey` and has to be the same on +both sides. It uses the `S1`-`S4` padding as its nonce, so all four of them have +to be set to at least 12 when it is in use. + +In the `[Peer]` section `PersistentKeepalive` also accepts a range. + +### Random trailers and disabled cookies (AmneziaWG 3.1) + +| Parameter | Value | Meaning | +| --- | --- | --- | +| `RandomTrailers` | `on` / `off` | appends random trailing bytes to protocol packets | +| `DisableCookies` | `on` / `off` | disables sending WireGuard cookie replies | + +AWG 3.1 has to be enabled on the server as well. After upgrading an Amnezia +Self-hosted server, generate a new client configuration instead of reusing or +converting an AWG 2.0 configuration. + # Sample config file ```ini @@ -94,11 +169,34 @@ PrivateKey = uCTIK+56CPyCvwJxmU5dBfuyJvPuSXAq1FzHdnIxe1Q= # PrivateKey = $MY_WIREGUARD_PRIVATE_KEY # Alternatively, reference environment variables DNS = 10.200.200.1 +# AmneziaWG parameters, all optional. See the section above for what they mean. +#Jc = 5 +#Jmin = 50 +#Jmax = 1000 +#S1 = 12 +#S2 = 15 +#S3 = 18 +#S4 = 21 +#H1 = 1234567 +#H2 = 2345678 +#H3 = 3456789 +#H4 = 4567890 +#I1 = +#HeaderProtectionKey = 6DPqLDkFO7mFvPKGvIY0zpk4iVwPQBHCFY2iVLdPGmE= +#ContentPaddingAddition = 10-100 +#RekeyAfterTime = 100-120 +#RekeyTimeout = 5 +#RejectAfterTime = 180-200 +#KeepaliveTimeout = 10-15 +#MaxHandshakeAttempts = 18-20 +#RandomTrailers = on +#DisableCookies = on + [Peer] PublicKey = QP+A67Z2UBrMgvNIdHv8gPel5URWNLS4B3ZQ2hQIZlg= # PresharedKey = UItQuvLsyh50ucXHfjF0bbR4IIpVBd74lwKc8uIPXXs= (optional) Endpoint = my.ddns.example.com:51820 -# PersistentKeepalive = 25 (optional) +# PersistentKeepalive = 25 (optional, a range like 15-25 also works) # TCPClientTunnel is a tunnel listening on your machine, # and it forwards any TCP traffic received to the specified target via wireguard. diff --git a/awg_config.go b/awg_config.go index bb8b0c71..f5ed8300 100644 --- a/awg_config.go +++ b/awg_config.go @@ -2,12 +2,18 @@ package wireproxy import ( "errors" + "fmt" "strconv" "strings" "github.com/go-ini/ini" ) +// Header protection uses the S1-S4 crypto padding as the cipher nonce, so every +// padding has to be at least as large as that nonce. +// Mirrors device.HeaderCipherNonceSize of amneziawg-go. +const headerCipherNonceSize = 12 + type ASecConfigType struct { junkPacketCount int // Jc junkPacketMinSize int // Jmin @@ -40,6 +46,29 @@ type ASecConfigType struct { i3 *string i4 *string i5 *string + headerProtectionKey *string // HeaderProtectionKey, hex-encoded + contentPaddingAddition *uintRange // ContentPaddingAddition + rekeyAfterTime *uintRange // RekeyAfterTime, seconds + rekeyTimeout *uintRange // RekeyTimeout, seconds + rejectAfterTime *uintRange // RejectAfterTime, seconds + keepaliveTimeout *uintRange // KeepaliveTimeout, seconds + maxHandshakeAttempts *uintRange // MaxHandshakeAttempts + randomTrailers *bool // RandomTrailers + disableCookies *bool // DisableCookies +} + +// uintRange is an AmneziaWG interval parameter, written as either "a" or "a-b". +// The device picks a random value inside the interval for every packet it sends. +type uintRange struct { + min uint32 + max uint32 +} + +func (r uintRange) String() string { + if r.min == r.max { + return strconv.FormatUint(uint64(r.min), 10) + } + return strconv.FormatUint(uint64(r.min), 10) + "-" + strconv.FormatUint(uint64(r.max), 10) } func ParseASecConfig(section *ini.Section) (*ASecConfigType, error) { @@ -130,54 +159,54 @@ func ParseASecConfig(section *ini.Section) (*ASecConfigType, error) { } if sectionKey, err := section.GetKey("H1"); err == nil { - minValue, maxValue, err := parseMagicHeaderInterval(sectionKey.String()) + value, err := parseUintRange(sectionKey.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid H1 value: %w", err) } if aSecConfig == nil { aSecConfig = &ASecConfigType{} } - aSecConfig.initPacketMagicHeader = minValue - aSecConfig.initPacketMagicHeaderMax = maxValue + aSecConfig.initPacketMagicHeader = value.min + aSecConfig.initPacketMagicHeaderMax = value.max aSecConfig.hasInitPacketMagicHeader = true } if sectionKey, err := section.GetKey("H2"); err == nil { - minValue, maxValue, err := parseMagicHeaderInterval(sectionKey.String()) + value, err := parseUintRange(sectionKey.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid H2 value: %w", err) } if aSecConfig == nil { aSecConfig = &ASecConfigType{} } - aSecConfig.responsePacketMagicHeader = minValue - aSecConfig.responsePacketMagicHeaderMax = maxValue + aSecConfig.responsePacketMagicHeader = value.min + aSecConfig.responsePacketMagicHeaderMax = value.max aSecConfig.hasResponsePacketMagicHeader = true } if sectionKey, err := section.GetKey("H3"); err == nil { - minValue, maxValue, err := parseMagicHeaderInterval(sectionKey.String()) + value, err := parseUintRange(sectionKey.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid H3 value: %w", err) } if aSecConfig == nil { aSecConfig = &ASecConfigType{} } - aSecConfig.underloadPacketMagicHeader = minValue - aSecConfig.underloadPacketMagicHeaderMax = maxValue + aSecConfig.underloadPacketMagicHeader = value.min + aSecConfig.underloadPacketMagicHeaderMax = value.max aSecConfig.hasUnderloadPacketMagicHeader = true } if sectionKey, err := section.GetKey("H4"); err == nil { - minValue, maxValue, err := parseMagicHeaderInterval(sectionKey.String()) + value, err := parseUintRange(sectionKey.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid H4 value: %w", err) } if aSecConfig == nil { aSecConfig = &ASecConfigType{} } - aSecConfig.transportPacketMagicHeader = minValue - aSecConfig.transportPacketMagicHeaderMax = maxValue + aSecConfig.transportPacketMagicHeader = value.min + aSecConfig.transportPacketMagicHeaderMax = value.max aSecConfig.hasTransportPacketMagicHeader = true } @@ -221,6 +250,67 @@ func ParseASecConfig(section *ini.Section) (*ASecConfigType, error) { aSecConfig.i5 = &value } + if sectionKey, err := section.GetKey("HeaderProtectionKey"); err == nil { + value, err := encodeBase64ToHex(sectionKey.String()) + if err != nil { + return nil, fmt.Errorf("invalid HeaderProtectionKey value: %w", err) + } + if aSecConfig == nil { + aSecConfig = &ASecConfigType{} + } + aSecConfig.headerProtectionKey = &value + } + + rangeKeys := []struct { + name string + dst func(*ASecConfigType) **uintRange + }{ + {"ContentPaddingAddition", func(c *ASecConfigType) **uintRange { return &c.contentPaddingAddition }}, + {"RekeyAfterTime", func(c *ASecConfigType) **uintRange { return &c.rekeyAfterTime }}, + {"RekeyTimeout", func(c *ASecConfigType) **uintRange { return &c.rekeyTimeout }}, + {"RejectAfterTime", func(c *ASecConfigType) **uintRange { return &c.rejectAfterTime }}, + {"KeepaliveTimeout", func(c *ASecConfigType) **uintRange { return &c.keepaliveTimeout }}, + {"MaxHandshakeAttempts", func(c *ASecConfigType) **uintRange { return &c.maxHandshakeAttempts }}, + } + + for _, rangeKey := range rangeKeys { + sectionKey, err := section.GetKey(rangeKey.name) + if err != nil { + continue + } + value, err := parseUintRange(sectionKey.String()) + if err != nil { + return nil, fmt.Errorf("invalid %s value: %w", rangeKey.name, err) + } + if aSecConfig == nil { + aSecConfig = &ASecConfigType{} + } + *rangeKey.dst(aSecConfig) = &value + } + + boolKeys := []struct { + name string + dst func(*ASecConfigType) **bool + }{ + {"RandomTrailers", func(c *ASecConfigType) **bool { return &c.randomTrailers }}, + {"DisableCookies", func(c *ASecConfigType) **bool { return &c.disableCookies }}, + } + + for _, boolKey := range boolKeys { + sectionKey, err := section.GetKey(boolKey.name) + if err != nil { + continue + } + value, err := sectionKey.Bool() + if err != nil { + return nil, fmt.Errorf("invalid %s value: %w", boolKey.name, err) + } + if aSecConfig == nil { + aSecConfig = &ASecConfigType{} + } + *boolKey.dst(aSecConfig) = &value + } + if err := ValidateASecConfig(aSecConfig); err != nil { return nil, err } @@ -290,6 +380,22 @@ func ValidateASecConfig(config *ASecConfigType) error { return errors.New("values of the H1-H4 fields must be unique") } + if config.headerProtectionKey != nil { + for _, padding := range []packetSizeCheck{ + {isSet: config.hasInitPacketJunkSize, size: config.initPacketJunkSize}, + {isSet: config.hasResponsePacketJunkSize, size: config.responsePacketJunkSize}, + {isSet: config.hasCookieReplyPacketJunkSize, size: config.cookieReplyPacketJunkSize}, + {isSet: config.hasTransportPacketJunkSize, size: config.transportPacketJunkSize}, + } { + if !padding.isSet || padding.size < headerCipherNonceSize { + return fmt.Errorf( + "values of the S1-S4 fields must all be at least %d when HeaderProtectionKey is set", + headerCipherNonceSize, + ) + } + } + } + return nil } @@ -306,40 +412,40 @@ const ( defaultTransportPacketMagicHeader uint32 = 4 ) -func parseMagicHeaderInterval(value string) (uint32, uint32, error) { +func parseUintRange(value string) (uintRange, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { - return 0, 0, errors.New("empty magic header value") + return uintRange{}, errors.New("empty range value") } parts := strings.Split(trimmed, "-") if len(parts) == 0 || len(parts) > 2 || parts[0] == "" { - return 0, 0, errors.New("invalid magic header range format") + return uintRange{}, errors.New("invalid range format") } minRaw, err := strconv.ParseUint(parts[0], 10, 32) if err != nil { - return 0, 0, err + return uintRange{}, err } minValue := uint32(minRaw) if len(parts) == 1 { - return minValue, minValue, nil + return uintRange{min: minValue, max: minValue}, nil } if parts[1] == "" { - return 0, 0, errors.New("invalid magic header range format") + return uintRange{}, errors.New("invalid range format") } maxRaw, err := strconv.ParseUint(parts[1], 10, 32) if err != nil { - return 0, 0, err + return uintRange{}, err } maxValue := uint32(maxRaw) if minValue > maxValue { - return 0, 0, errors.New("invalid magic header range: lower bound cannot exceed upper bound") + return uintRange{}, errors.New("invalid range: lower bound cannot exceed upper bound") } - return minValue, maxValue, nil + return uintRange{min: minValue, max: maxValue}, nil } func collectEffectiveHeaderIntervals(config *ASecConfigType) []headerInterval { @@ -386,8 +492,5 @@ func hasOverlappingHeaderIntervals(intervals []headerInterval) bool { } func formatMagicHeaderInterval(minValue uint32, maxValue uint32) string { - if minValue == maxValue { - return strconv.FormatUint(uint64(minValue), 10) - } - return strconv.FormatUint(uint64(minValue), 10) + "-" + strconv.FormatUint(uint64(maxValue), 10) + return uintRange{min: minValue, max: maxValue}.String() } diff --git a/awg_handshake_test.go b/awg_handshake_test.go new file mode 100644 index 00000000..ae0a0216 --- /dev/null +++ b/awg_handshake_test.go @@ -0,0 +1,299 @@ +package wireproxy + +import ( + "bytes" + "fmt" + "net/netip" + "strings" + "testing" + "time" + + "github.com/amnezia-vpn/amneziawg-go/v3/conn" + "github.com/amnezia-vpn/amneziawg-go/v3/conn/bindtest" + "github.com/amnezia-vpn/amneziawg-go/v3/device" + "github.com/amnezia-vpn/amneziawg-go/v3/tun/tuntest" +) + +// The obfuscation parameters have to match on both ends, and a mistake in one +// of them does not show up in the IPC request: the device takes the request, +// and only the handshake that follows fails. These tests therefore run our +// generated request against a second device configured as the peer, and check +// that a packet actually makes it through the tunnel. +// +// clientKey/serverKey are a matching private/public pair each, base64 as a +// config file spells them. +const ( + clientPrivateKey = "YKPH3Kd6yGqw8wbubzDDfQFE3iVOLqSfK2CIUFB9vFs=" + clientPublicKey = "EPOSjEbCQZttA+BiG6iIDqyRu/xrTT9Jlvi65xHVRAk=" + serverPrivateKey = "2A8O4sFopoHJToUX7XdQ50+gHvoBM/Mq+yrtt654plU=" + serverPublicKey = "fX1+LM/maMFUfF1A7s0KcEKlNIJbkMFECfSYvcRkyz8=" + + clientIP = "1.0.0.1" + serverIP = "1.0.0.2" + + // bindtest routes by endpoint port; this is the one the client's bind + // delivers to the server's bind. + serverEndpoint = "127.0.0.1:1" +) + +// tunnelPair is a client device configured through the wireproxy config +// pipeline, wired to a server device configured directly. +type tunnelPair struct { + clientTUN *tuntest.ChannelTUN + serverTUN *tuntest.ChannelTUN + client *device.Device + server *device.Device +} + +// newTunnelPair brings up both ends. clientConfig is an ini config parsed the +// way wireproxy parses a real one; serverParams are the [Interface] level UAPI +// lines the server needs to mirror the client's obfuscation. +func newTunnelPair(t *testing.T, clientConfig string, serverParams ...string) *tunnelPair { + t.Helper() + + iniData, err := loadIniConfig(clientConfig) + if err != nil { + t.Fatal(err) + } + + cfg := DeviceConfig{MTU: device.DefaultMTU} + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + if err = ParsePeers(iniData, &cfg.Peers); err != nil { + t.Fatal(err) + } + + setting, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + + var serverRequest strings.Builder + fmt.Fprintf(&serverRequest, "private_key=%s\n", mustHexKey(t, serverPrivateKey)) + for _, param := range serverParams { + fmt.Fprintf(&serverRequest, "%s\n", param) + } + fmt.Fprintf(&serverRequest, "public_key=%s\n", mustHexKey(t, clientPublicKey)) + fmt.Fprintf(&serverRequest, "allowed_ip=%s/32\n", clientIP) + + binds := bindtest.NewChannelBinds() + pair := &tunnelPair{ + clientTUN: tuntest.NewChannelTUN(), + serverTUN: tuntest.NewChannelTUN(), + } + + pair.client = startDevice(t, pair.clientTUN, binds[0], setting.IpcRequest, "client: ") + pair.server = startDevice(t, pair.serverTUN, binds[1], serverRequest.String(), "server: ") + + pair.warmUp(t) + + return pair +} + +// warmUp spends the first data packet, which amneziawg-go loses whenever +// S4 is set: its TUN reader samples the transport padding once, before it +// blocks on the first read, and that happens while the device is still +// unconfigured. The packet then goes out without the padding and the peer, +// which by now expects it, cannot find the message type and drops it. Only the +// first one is affected - the reader picks up the real padding on its next +// pass. Reproducible against the library alone, so nothing here can avoid it. +func (pair *tunnelPair) warmUp(t *testing.T) { + t.Helper() + + packet := tuntest.Ping(netip.MustParseAddr(serverIP), netip.MustParseAddr(clientIP)) + + select { + case pair.clientTUN.Outbound <- packet: + case <-time.After(5 * time.Second): + t.Fatal("the client tunnel did not accept the warm-up packet") + } + + // It may or may not arrive, depending on whether the library still has the + // bug; both are fine, the tunnel is warm either way. + select { + case <-pair.serverTUN.Inbound: + case <-time.After(2 * time.Second): + } +} + +func startDevice( + t *testing.T, + channelTUN *tuntest.ChannelTUN, + bind conn.Bind, + request string, + logPrefix string, +) *device.Device { + t.Helper() + + logLevel := device.LogLevelSilent + if testing.Verbose() { + logLevel = device.LogLevelVerbose + } + + dev := device.NewDevice(channelTUN.TUN(), bind, device.NewLogger(logLevel, logPrefix)) + t.Cleanup(dev.Close) + + if err := dev.IpcSet(request); err != nil { + t.Fatalf("%sdevice rejected the IPC request: %v\n%s", logPrefix, err, request) + } + if err := dev.Up(); err != nil { + t.Fatalf("%sfailed to bring the device up: %v", logPrefix, err) + } + + return dev +} + +// handshakeDone reports whether the device has completed a handshake with its +// only peer. Used to tell a failed handshake from a lost packet. +func handshakeDone(t *testing.T, dev *device.Device) bool { + t.Helper() + + state, err := dev.IpcGet() + if err != nil { + t.Fatal(err) + } + return strings.Contains(state, "last_handshake_time_sec=") && + !strings.Contains(state, "last_handshake_time_sec=0\n") +} + +// sendThrough pushes a packet into the client tunnel and expects it out of the +// server tunnel unchanged. The packet is also what triggers the handshake: +// wireguard only starts one once there is something to send. +func (pair *tunnelPair) sendThrough(t *testing.T) { + t.Helper() + + sent := tuntest.Ping(netip.MustParseAddr(serverIP), netip.MustParseAddr(clientIP)) + + select { + case pair.clientTUN.Outbound <- sent: + case <-time.After(5 * time.Second): + t.Fatal("the client tunnel did not accept the packet") + } + + select { + case received := <-pair.serverTUN.Inbound: + if !bytes.Equal(sent, received) { + t.Fatal("the packet came out of the tunnel altered") + } + case <-time.After(15 * time.Second): + if !handshakeDone(t, pair.client) { + t.Fatal("the handshake never completed") + } + t.Fatal("the handshake completed but the packet did not make it through") + } + + if !handshakeDone(t, pair.client) { + t.Fatal("the packet arrived without a completed handshake") + } +} + +func mustHexKey(t *testing.T, base64Key string) string { + t.Helper() + + hexKey, err := encodeBase64ToHex(base64Key) + if err != nil { + t.Fatal(err) + } + return hexKey +} + +func clientConfig(awgParams string) string { + return fmt.Sprintf(` +[Interface] +PrivateKey = %s +Address = %s/32 +%s + +[Peer] +PublicKey = %s +AllowedIPs = %s/32 +Endpoint = %s +`, clientPrivateKey, clientIP, awgParams, serverPublicKey, serverIP, serverEndpoint) +} + +func TestTunnelWithAWG3Params(t *testing.T) { + pair := newTunnelPair(t, clientConfig(` +Jc = 4 +Jmin = 50 +Jmax = 500 +S1 = 12 +S2 = 15 +S3 = 18 +S4 = 21 +H1 = 123456 +H2 = 234567 +H3 = 345678 +H4 = 456789 +I1 = +HeaderProtectionKey = efpsQ5dEcds7RJIU6wZ5VdMItHGQTL/OBbza0xuVV1w= +ContentPaddingAddition = 10-100 +RekeyAfterTime = 100-120 +RekeyTimeout = 5 +RejectAfterTime = 180-200 +KeepaliveTimeout = 10-15 +MaxHandshakeAttempts = 18-20 +`), + "s1=12", "s2=15", "s3=18", "s4=21", + "h1=123456", "h2=234567", "h3=345678", "h4=456789", + "header_protection_key="+mustHexKey(t, "efpsQ5dEcds7RJIU6wZ5VdMItHGQTL/OBbza0xuVV1w="), + "content_padding_addition=10-100", + ) + + pair.sendThrough(t) +} + +func TestTunnelWithAWG31Params(t *testing.T) { + pair := newTunnelPair(t, clientConfig(` +S1 = 12 +S2 = 15 +S3 = 18 +S4 = 21 +H1 = 123456 +H2 = 234567 +H3 = 345678 +H4 = 456789 +HeaderProtectionKey = efpsQ5dEcds7RJIU6wZ5VdMItHGQTL/OBbza0xuVV1w= +ContentPaddingAddition = 10-100 +RandomTrailers = on +DisableCookies = on +`), + "s1=12", "s2=15", "s3=18", "s4=21", + "h1=123456", "h2=234567", "h3=345678", "h4=456789", + "header_protection_key="+mustHexKey(t, "efpsQ5dEcds7RJIU6wZ5VdMItHGQTL/OBbza0xuVV1w="), + "content_padding_addition=10-100", + "random_trailers=true", "disable_cookies=true", + ) + + pair.sendThrough(t) +} + +func TestTunnelWithAWG2Params(t *testing.T) { + pair := newTunnelPair(t, clientConfig(` +Jc = 4 +Jmin = 50 +Jmax = 500 +S1 = 15 +S2 = 18 +S3 = 20 +S4 = 23 +H1 = 100-101 +H2 = 102-103 +H3 = 104 +H4 = 105-106 +I1 = +`), + "s1=15", "s2=18", "s3=20", "s4=23", + "h1=100-101", "h2=102-103", "h3=104", "h4=105-106", + ) + + pair.sendThrough(t) +} + +// A config with no obfuscation at all has to keep talking plain wireguard, +// which is what an AmneziaWG 3.0 build must not break. +func TestTunnelWithPlainWireguard(t *testing.T) { + pair := newTunnelPair(t, clientConfig("")) + + pair.sendThrough(t) +} diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index c801096d..9b6dc9a2 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -14,7 +14,7 @@ import ( "syscall" "github.com/akamensky/argparse" - "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/v3/device" wireproxyawg "github.com/artem-russkikh/wireproxy-awg" "suah.dev/protect" ) @@ -28,7 +28,7 @@ var default_config_paths = []string { os.Getenv("HOME")+"/.config/wireproxy.conf", } -var version = "1.0.17-dev" +var version = "1.0.18-dev" func panicIfError(err error) { if err != nil { diff --git a/config.go b/config.go index 3d8fa3da..07607225 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/hex" "errors" + "fmt" "net" "os" "strings" @@ -14,11 +15,13 @@ import ( ) type PeerConfig struct { - PublicKey string - PreSharedKey string - Endpoint *string - KeepAlive int - AllowedIPs []netip.Prefix + PublicKey string + PreSharedKey string + Endpoint *string + KeepAlive int // PersistentKeepalive in seconds; lower bound when a range is configured + KeepAliveMax int // upper bound of the PersistentKeepalive range, ignored when below KeepAlive + AllowedIPs []netip.Prefix + keepAliveRange *uintRange // exact parsed range; preserves uint32 values on 32-bit platforms } // DeviceConfig contains the information to initiate a wireguard connection @@ -358,11 +361,18 @@ func ParsePeers(cfg *ini.File, peers *[]PeerConfig) error { } if sectionKey, err := section.GetKey("PersistentKeepalive"); err == nil { - value, err := sectionKey.Int() + // AmneziaWG 3.0 also accepts a range, and wg-quick accepts "off". + raw := strings.TrimSpace(sectionKey.String()) + if strings.EqualFold(raw, "off") { + raw = "0" + } + value, err := parseUintRange(raw) if err != nil { - return err + return fmt.Errorf("invalid PersistentKeepalive value: %w", err) } - peer.KeepAlive = value + peer.KeepAlive = int(value.min) + peer.KeepAliveMax = int(value.max) + peer.keepAliveRange = &value } peer.AllowedIPs, err = parseAllowedIPs(section) diff --git a/config_test.go b/config_test.go index cd5ce871..d5e48d03 100644 --- a/config_test.go +++ b/config_test.go @@ -680,3 +680,301 @@ H1 = 2 t.Fatalf("unexpected error: %v", err) } } + +func TestWireguardConfWithAWG3Params(t *testing.T) { + const config = ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 +Jc = 5 +Jmin = 10 +Jmax = 50 +S1 = 12 +S2 = 15 +S3 = 18 +S4 = 21 +H1 = 100 +H2 = 200 +H3 = 300 +H4 = 400 +HeaderProtectionKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +ContentPaddingAddition = 10-100 +RekeyAfterTime = 100-120 +RekeyTimeout = 5 +RejectAfterTime = 180-200 +KeepaliveTimeout = 10-15 +MaxHandshakeAttempts = 18-20 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 15-25 +` + + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + if err = ParsePeers(iniData, &cfg.Peers); err != nil { + t.Fatal(err) + } + + ipcReq, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + + for _, line := range []string{ + "header_protection_key=7bc2ca01cf9ff71133abd02befe31f291aecfa067fe32cefa5124b449fd5275c", + "content_padding_addition=10-100", + "rekey_after_time=100-120", + "rekey_timeout=5", + "reject_after_time=180-200", + "keepalive_timeout=10-15", + "max_handshake_attempts=18-20", + "persistent_keepalive_interval=15-25", + } { + if !strings.Contains(ipcReq.IpcRequest, line) { + t.Fatalf("%q should be present in IPC request:\n%s", line, ipcReq.IpcRequest) + } + } +} + +func TestWireguardConfWithAWG31Params(t *testing.T) { + const config = ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +RandomTrailers = on +DisableCookies = true + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +` + + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + if err = ParsePeers(iniData, &cfg.Peers); err != nil { + t.Fatal(err) + } + + ipcReq, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + for _, line := range []string{"random_trailers=true", "disable_cookies=true"} { + if !strings.Contains(ipcReq.IpcRequest, line) { + t.Fatalf("%q should be present in IPC request:\n%s", line, ipcReq.IpcRequest) + } + } +} + +func TestWireguardConfWithDisabledAWG31Params(t *testing.T) { + iniData, err := loadIniConfig(`[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +RandomTrailers = off +DisableCookies = false +`) + if err != nil { + t.Fatal(err) + } + + var cfg DeviceConfig + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + ipcReq, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + for _, line := range []string{"random_trailers=false", "disable_cookies=false"} { + if !strings.Contains(ipcReq.IpcRequest, line) { + t.Fatalf("%q should be present in IPC request:\n%s", line, ipcReq.IpcRequest) + } + } +} + +func TestWireguardConfRejectsInvalidAWG31Params(t *testing.T) { + iniData, err := loadIniConfig(`[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +RandomTrailers = sometimes +`) + if err != nil { + t.Fatal(err) + } + + var cfg DeviceConfig + err = ParseInterface(iniData, &cfg) + if err == nil || !strings.Contains(err.Error(), "invalid RandomTrailers value") { + t.Fatalf("expected invalid RandomTrailers error, got %v", err) + } +} + +func TestWireguardConfWithoutAWG3ParamsEmitsNothing(t *testing.T) { + const config = ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 +Jc = 5 +Jmin = 10 +Jmax = 50 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 25 +` + + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + if err = ParsePeers(iniData, &cfg.Peers); err != nil { + t.Fatal(err) + } + ipcReq, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + + for _, key := range []string{ + "header_protection_key=", + "content_padding_addition=", + "rekey_after_time=", + "rekey_timeout=", + "reject_after_time=", + "keepalive_timeout=", + "max_handshake_attempts=", + "random_trailers=", + "disable_cookies=", + } { + if strings.Contains(ipcReq.IpcRequest, key) { + t.Fatalf("%q should not be emitted when it is not set", key) + } + } + if !strings.Contains(ipcReq.IpcRequest, "persistent_keepalive_interval=25\n") { + t.Fatalf("single PersistentKeepalive value should stay unchanged:\n%s", ipcReq.IpcRequest) + } +} + +func TestWireguardConfRejectsInvalidAWG3Params(t *testing.T) { + tests := []struct { + name string + parameters string + wantError string + }{ + { + name: "small header nonce padding", + parameters: `S1 = 12 +S2 = 15 +S3 = 18 +S4 = 11 +HeaderProtectionKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w=`, + wantError: "values of the S1-S4 fields must all be at least 12 when HeaderProtectionKey is set", + }, + { + name: "missing header nonce padding", + parameters: `S1 = 12 +S2 = 15 +S3 = 18 +HeaderProtectionKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w=`, + wantError: "values of the S1-S4 fields must all be at least 12 when HeaderProtectionKey is set", + }, + { + name: "descending range", + parameters: "RekeyTimeout = 30-10", + wantError: "invalid RekeyTimeout value: invalid range: lower bound cannot exceed upper bound", + }, + { + name: "invalid header protection key", + parameters: "HeaderProtectionKey = not-a-key", + wantError: "invalid HeaderProtectionKey value:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := `[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 +` + tt.parameters + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + var cfg DeviceConfig + err = ParseInterface(iniData, &cfg) + if err == nil || !strings.Contains(err.Error(), tt.wantError) { + t.Fatalf("expected error containing %q, got %v", tt.wantError, err) + } + }) + } +} + +func TestWireguardConfWithPersistentKeepaliveOff(t *testing.T) { + const config = ` +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = off +` + + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + var peers []PeerConfig + if err = ParsePeers(iniData, &peers); err != nil { + t.Fatal(err) + } + if peers[0].KeepAlive != 0 || peers[0].KeepAliveMax != 0 { + t.Fatalf("off should disable keepalive, got %d-%d", peers[0].KeepAlive, peers[0].KeepAliveMax) + } +} + +func TestPersistentKeepaliveRangeCrossesInt32Boundary(t *testing.T) { + iniData, err := loadIniConfig(`[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +PersistentKeepalive = 2147483647-2147483648 +`) + if err != nil { + t.Fatal(err) + } + + var peers []PeerConfig + if err = ParsePeers(iniData, &peers); err != nil { + t.Fatal(err) + } + setting, err := CreateIPCRequest(&DeviceConfig{Peers: peers}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(setting.IpcRequest, "persistent_keepalive_interval=2147483647-2147483648\n") { + t.Fatalf("range corrupted: %s", setting.IpcRequest) + } +} diff --git a/go.mod b/go.mod index a6844ef7..42381b5e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/akamensky/argparse v1.4.0 - github.com/amnezia-vpn/amneziawg-go v0.2.19 + github.com/amnezia-vpn/amneziawg-go/v3 v3.1.20260814 github.com/go-ini/ini v1.67.0 github.com/landlock-lsm/go-landlock v0.6.0 github.com/things-go/go-socks5 v0.0.5 diff --git a/go.sum b/go.sum index 92e753f0..fadbeabb 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZ github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= -github.com/amnezia-vpn/amneziawg-go v0.2.19 h1:l3rOmrA4o5z38kpgnA5iSk1yOm7Cv3AafIi4vxpSEV0= -github.com/amnezia-vpn/amneziawg-go v0.2.19/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A= +github.com/amnezia-vpn/amneziawg-go/v3 v3.1.20260814 h1:l2AhBD+sFycU8Im81n/bZORMxW7fWtlZJEuJ4Hh0+z0= +github.com/amnezia-vpn/amneziawg-go/v3 v3.1.20260814/go.mod h1:YoPc6qcOZqD7TXZ1xpedD8Sx3aSKsxN05ZqEFmXDNHk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= diff --git a/keepalive_validation_test.go b/keepalive_validation_test.go new file mode 100644 index 00000000..0dcaf7fd --- /dev/null +++ b/keepalive_validation_test.go @@ -0,0 +1,37 @@ +package wireproxy + +import ( + "strconv" + "testing" +) + +func TestCreateIPCRequestRejectsInvalidProgrammaticKeepalive(t *testing.T) { + tests := []struct { + name string + peer PeerConfig + }{ + {name: "negative minimum", peer: PeerConfig{KeepAlive: -1}}, + {name: "negative maximum", peer: PeerConfig{KeepAlive: 1, KeepAliveMax: -1}}, + } + if strconv.IntSize == 64 { + tooLargeUint32 := uint64(1) << 32 + tests = append(tests, + struct { + name string + peer PeerConfig + }{name: "minimum exceeds uint32", peer: PeerConfig{KeepAlive: int(tooLargeUint32)}}, + struct { + name string + peer PeerConfig + }{name: "maximum exceeds uint32", peer: PeerConfig{KeepAlive: 1, KeepAliveMax: int(tooLargeUint32)}}, + ) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := CreateIPCRequest(&DeviceConfig{Peers: []PeerConfig{tt.peer}}); err == nil { + t.Fatal("expected invalid PersistentKeepalive error") + } + }) + } +} diff --git a/routine.go b/routine.go index cc2b4f4a..c779c5d7 100644 --- a/routine.go +++ b/routine.go @@ -8,7 +8,7 @@ import ( "encoding/binary" "encoding/json" "errors" - "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/v3/device" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" @@ -29,7 +29,7 @@ import ( "net/netip" - "github.com/amnezia-vpn/amneziawg-go/tun/netstack" + "github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack" ) // errorLogger is the logger to print error message diff --git a/wireguard.go b/wireguard.go index 6a0bca2d..0c194024 100644 --- a/wireguard.go +++ b/wireguard.go @@ -9,9 +9,9 @@ import ( "net/netip" "github.com/MakeNowJust/heredoc/v2" - "github.com/amnezia-vpn/amneziawg-go/conn" - "github.com/amnezia-vpn/amneziawg-go/device" - "github.com/amnezia-vpn/amneziawg-go/tun/netstack" + "github.com/amnezia-vpn/amneziawg-go/v3/conn" + "github.com/amnezia-vpn/amneziawg-go/v3/device" + "github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack" ) // DeviceSetting contains the parameters for setting up a tun interface @@ -22,6 +22,28 @@ type DeviceSetting struct { MTU int } +func persistentKeepaliveRange(peer PeerConfig) (uintRange, error) { + if peer.keepAliveRange != nil && + int(peer.keepAliveRange.min) == peer.KeepAlive && + int(peer.keepAliveRange.max) == peer.KeepAliveMax { + return *peer.keepAliveRange, nil + } + + maxUint32 := uint64(^uint32(0)) + if peer.KeepAlive < 0 || uint64(peer.KeepAlive) > maxUint32 { + return uintRange{}, fmt.Errorf("PersistentKeepalive must be between 0 and %d", maxUint32) + } + if peer.KeepAliveMax < 0 || uint64(peer.KeepAliveMax) > maxUint32 { + return uintRange{}, fmt.Errorf("PersistentKeepalive maximum must be between 0 and %d", maxUint32) + } + + keepAlive := uintRange{min: uint32(peer.KeepAlive), max: uint32(peer.KeepAlive)} + if peer.KeepAliveMax > peer.KeepAlive { + keepAlive.max = uint32(peer.KeepAliveMax) + } + return keepAlive, nil +} + // CreateIPCRequest serialize the config into an IPC request and DeviceSetting func CreateIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { var request bytes.Buffer @@ -59,25 +81,25 @@ func CreateIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { fmt.Fprintf(&aSecBuilder, "s4=%d\n", aSecConfig.transportPacketJunkSize) } if aSecConfig.hasInitPacketMagicHeader { - fmt.Fprintf(&aSecBuilder, + fmt.Fprintf(&aSecBuilder, "h1=%s\n", formatMagicHeaderInterval(aSecConfig.initPacketMagicHeader, aSecConfig.initPacketMagicHeaderMax), ) } if aSecConfig.hasResponsePacketMagicHeader { - fmt.Fprintf(&aSecBuilder, + fmt.Fprintf(&aSecBuilder, "h2=%s\n", formatMagicHeaderInterval(aSecConfig.responsePacketMagicHeader, aSecConfig.responsePacketMagicHeaderMax), ) } if aSecConfig.hasUnderloadPacketMagicHeader { - fmt.Fprintf(&aSecBuilder, + fmt.Fprintf(&aSecBuilder, "h3=%s\n", formatMagicHeaderInterval(aSecConfig.underloadPacketMagicHeader, aSecConfig.underloadPacketMagicHeaderMax), ) } if aSecConfig.hasTransportPacketMagicHeader { - fmt.Fprintf(&aSecBuilder, + fmt.Fprintf(&aSecBuilder, "h4=%s\n", formatMagicHeaderInterval(aSecConfig.transportPacketMagicHeader, aSecConfig.transportPacketMagicHeaderMax), ) @@ -99,16 +121,49 @@ func CreateIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { fmt.Fprintf(&aSecBuilder, "i5=%s\n", *aSecConfig.i5) } + if aSecConfig.headerProtectionKey != nil { + fmt.Fprintf(&aSecBuilder, "header_protection_key=%s\n", *aSecConfig.headerProtectionKey) + } + if aSecConfig.contentPaddingAddition != nil { + fmt.Fprintf(&aSecBuilder, "content_padding_addition=%s\n", aSecConfig.contentPaddingAddition) + } + if aSecConfig.rekeyAfterTime != nil { + fmt.Fprintf(&aSecBuilder, "rekey_after_time=%s\n", aSecConfig.rekeyAfterTime) + } + if aSecConfig.rekeyTimeout != nil { + fmt.Fprintf(&aSecBuilder, "rekey_timeout=%s\n", aSecConfig.rekeyTimeout) + } + if aSecConfig.rejectAfterTime != nil { + fmt.Fprintf(&aSecBuilder, "reject_after_time=%s\n", aSecConfig.rejectAfterTime) + } + if aSecConfig.keepaliveTimeout != nil { + fmt.Fprintf(&aSecBuilder, "keepalive_timeout=%s\n", aSecConfig.keepaliveTimeout) + } + if aSecConfig.maxHandshakeAttempts != nil { + fmt.Fprintf(&aSecBuilder, "max_handshake_attempts=%s\n", aSecConfig.maxHandshakeAttempts) + } + if aSecConfig.randomTrailers != nil { + fmt.Fprintf(&aSecBuilder, "random_trailers=%t\n", *aSecConfig.randomTrailers) + } + if aSecConfig.disableCookies != nil { + fmt.Fprintf(&aSecBuilder, "disable_cookies=%t\n", *aSecConfig.disableCookies) + } + request.WriteString(aSecBuilder.String()) } for _, peer := range conf.Peers { + keepAlive, err := persistentKeepaliveRange(peer) + if err != nil { + return nil, err + } + fmt.Fprintf(&request, heredoc.Doc(` public_key=%s - persistent_keepalive_interval=%d + persistent_keepalive_interval=%s preshared_key=%s `), - peer.PublicKey, peer.KeepAlive, peer.PreSharedKey, + peer.PublicKey, keepAlive, peer.PreSharedKey, ) if peer.Endpoint != nil { fmt.Fprintf(&request, "endpoint=%s\n", *peer.Endpoint) diff --git a/wireguard_test.go b/wireguard_test.go new file mode 100644 index 00000000..344bdc67 --- /dev/null +++ b/wireguard_test.go @@ -0,0 +1,136 @@ +package wireproxy + +import ( + "testing" + + "github.com/amnezia-vpn/amneziawg-go/v3/conn" + "github.com/amnezia-vpn/amneziawg-go/v3/device" + "github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack" +) + +// The IPC request is a plain string that amneziawg-go parses key by key, so a +// wrong key name or value format is only caught by the device itself. Feed the +// generated request to a real device to keep the two in sync. +func applyIPCRequest(t *testing.T, config string) { + t.Helper() + + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + + cfg := DeviceConfig{MTU: 1420} + if err = ParseInterface(iniData, &cfg); err != nil { + t.Fatal(err) + } + if err = ParsePeers(iniData, &cfg.Peers); err != nil { + t.Fatal(err) + } + + setting, err := CreateIPCRequest(&cfg) + if err != nil { + t.Fatal(err) + } + + tun, _, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU) + if err != nil { + t.Fatal(err) + } + + dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, "")) + defer dev.Close() + + if err = dev.IpcSet(setting.IpcRequest); err != nil { + t.Fatalf("device rejected the IPC request: %v\n%s", err, setting.IpcRequest) + } +} + +func TestIPCRequestWithAWG3ParamsIsAcceptedByDevice(t *testing.T) { + applyIPCRequest(t, ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 +Jc = 5 +Jmin = 10 +Jmax = 50 +S1 = 12 +S2 = 15 +S3 = 18 +S4 = 21 +H1 = 100 +H2 = 200 +H3 = 300 +H4 = 400 +I1 = +HeaderProtectionKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +ContentPaddingAddition = 10-100 +RekeyAfterTime = 100-120 +RekeyTimeout = 5 +RejectAfterTime = 180-200 +KeepaliveTimeout = 10-15 +MaxHandshakeAttempts = 18-20 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 15-25 +`) +} + +func TestIPCRequestWithAWG31ParamsIsAcceptedByDevice(t *testing.T) { + applyIPCRequest(t, ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +RandomTrailers = on +DisableCookies = on + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +`) +} + +func TestIPCRequestWithAWG2ParamsIsAcceptedByDevice(t *testing.T) { + applyIPCRequest(t, ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 +Jc = 5 +Jmin = 10 +Jmax = 50 +S1 = 15 +S2 = 18 +S3 = 20 +S4 = 23 +H1 = 100-101 +H2 = 102-103 +H3 = 104 +H4 = 105-106 +I1 = + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 25 +`) +} + +func TestIPCRequestWithPlainWireguardIsAcceptedByDevice(t *testing.T) { + applyIPCRequest(t, ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0 +Endpoint = 94.140.11.15:51820 +`) +}