Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 100 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
| `<b 0x[hex]>` | the given bytes, as is |
| `<r [size]>` | `size` random bytes |
| `<rd [size]>` | `size` random digits |
| `<rc [size]>` | `size` random letters |
| `<t>` | 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
Expand All @@ -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 = <b 0x504f5354><rc 8><t>
#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.
Expand Down
161 changes: 132 additions & 29 deletions awg_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
Loading
Loading