mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
feat(tunnel): параметры обфускации awg 3+
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultMTU - безопасный MTU для WireGuard поверх TURN без фрагментации.
|
||||
@@ -34,7 +36,11 @@ type Config struct {
|
||||
Amnezia AmneziaParams
|
||||
}
|
||||
|
||||
// MinHeaderProtectionPadding - минимум S1-S4, из которого AWG берёт nonce для защиты заголовка.
|
||||
const MinHeaderProtectionPadding = 12
|
||||
|
||||
// AmneziaParams содержит параметры обфускации протокола AmneziaWG.
|
||||
// Range-поля пустые либо в формате "N" / "LO-HI".
|
||||
type AmneziaParams struct {
|
||||
Jc int
|
||||
Jmin int
|
||||
@@ -51,6 +57,17 @@ type AmneziaParams struct {
|
||||
H4 string
|
||||
|
||||
I [5]string
|
||||
|
||||
// AWG 3+.
|
||||
HeaderProtectionKey Key
|
||||
ContentPaddingAddition string
|
||||
RekeyAfterTime string
|
||||
RekeyTimeout string
|
||||
RejectAfterTime string
|
||||
KeepaliveTimeout string
|
||||
MaxHandshakeAttempts string
|
||||
RandomTrailers bool
|
||||
DisableCookies bool
|
||||
}
|
||||
|
||||
func (p AmneziaParams) Enabled() bool { return p != AmneziaParams{} }
|
||||
@@ -111,5 +128,58 @@ func (p AmneziaParams) validate() error {
|
||||
return errors.New("tunnel: amnezia padding must not be negative")
|
||||
}
|
||||
}
|
||||
return p.validateAWG3()
|
||||
}
|
||||
|
||||
func (p AmneziaParams) validateAWG3() error {
|
||||
ranges := []struct {
|
||||
name string
|
||||
val string
|
||||
}{
|
||||
{"H1", p.H1}, {"H2", p.H2}, {"H3", p.H3}, {"H4", p.H4},
|
||||
{"ContentPaddingAddition", p.ContentPaddingAddition},
|
||||
{"RekeyAfterTime", p.RekeyAfterTime},
|
||||
{"RekeyTimeout", p.RekeyTimeout},
|
||||
{"RejectAfterTime", p.RejectAfterTime},
|
||||
{"KeepaliveTimeout", p.KeepaliveTimeout},
|
||||
{"MaxHandshakeAttempts", p.MaxHandshakeAttempts},
|
||||
}
|
||||
for _, r := range ranges {
|
||||
if err := ValidateRange(r.val); err != nil {
|
||||
return fmt.Errorf("tunnel: amnezia %s: %w", r.name, err)
|
||||
}
|
||||
}
|
||||
if p.HeaderProtectionKey.IsZero() {
|
||||
return nil
|
||||
}
|
||||
// Защита заголовка берёт nonce из crypto-паддинга, короткого ей не хватает.
|
||||
for _, v := range []int{p.S1, p.S2, p.S3, p.S4} {
|
||||
if v < MinHeaderProtectionPadding {
|
||||
return fmt.Errorf("tunnel: amnezia HeaderProtectionKey requires S1-S4 >= %d", MinHeaderProtectionPadding)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRange проверяет формат диапазона AWG ("N" или "LO-HI"); пустая строка - не задан.
|
||||
func ValidateRange(s string) error {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
lo, hi, hasHi := strings.Cut(s, "-")
|
||||
low, err := strconv.ParseUint(lo, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad lower bound %q", lo)
|
||||
}
|
||||
if !hasHi {
|
||||
return nil
|
||||
}
|
||||
high, err := strconv.ParseUint(hi, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad upper bound %q", hi)
|
||||
}
|
||||
if high < low {
|
||||
return fmt.Errorf("upper bound %d below lower %d", high, low)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ func writeAmnezia(line func(k, v string), p AmneziaParams) {
|
||||
val string
|
||||
}{
|
||||
{"h1", p.H1}, {"h2", p.H2}, {"h3", p.H3}, {"h4", p.H4},
|
||||
{"content_padding_addition", p.ContentPaddingAddition},
|
||||
{"rekey_after_time", p.RekeyAfterTime},
|
||||
{"rekey_timeout", p.RekeyTimeout},
|
||||
{"reject_after_time", p.RejectAfterTime},
|
||||
{"keepalive_timeout", p.KeepaliveTimeout},
|
||||
{"max_handshake_attempts", p.MaxHandshakeAttempts},
|
||||
}
|
||||
for _, kv := range strKeys {
|
||||
if kv.val != "" {
|
||||
@@ -80,4 +86,14 @@ func writeAmnezia(line func(k, v string), p AmneziaParams) {
|
||||
line("i"+strconv.Itoa(i+1), spec)
|
||||
}
|
||||
}
|
||||
if !p.HeaderProtectionKey.IsZero() {
|
||||
line("header_protection_key", hex.EncodeToString(p.HeaderProtectionKey[:]))
|
||||
}
|
||||
// Булевы шлём только во включённом виде - device и так стартует с false.
|
||||
if p.RandomTrailers {
|
||||
line("random_trailers", "true")
|
||||
}
|
||||
if p.DisableCookies {
|
||||
line("disable_cookies", "true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,3 +176,97 @@ func TestDefaultsMTU(t *testing.T) {
|
||||
t.Errorf("Defaults().MTU = %d, want %d", got, DefaultMTU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUAPIAWG3Params(t *testing.T) {
|
||||
cfg := minimalConfig()
|
||||
cfg.Amnezia = AmneziaParams{
|
||||
S1: 12, S2: 12, S3: 12, S4: 12,
|
||||
HeaderProtectionKey: testKey(0x04),
|
||||
ContentPaddingAddition: "10-40",
|
||||
RekeyAfterTime: "90-140",
|
||||
RekeyTimeout: "5",
|
||||
RejectAfterTime: "180",
|
||||
KeepaliveTimeout: "10-15",
|
||||
MaxHandshakeAttempts: "18",
|
||||
RandomTrailers: true,
|
||||
DisableCookies: true,
|
||||
}
|
||||
|
||||
got, err := UAPI(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("UAPI() error = %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"header_protection_key=" + strings.Repeat("04", KeyLen),
|
||||
"content_padding_addition=10-40",
|
||||
"rekey_after_time=90-140",
|
||||
"rekey_timeout=5",
|
||||
"reject_after_time=180",
|
||||
"keepalive_timeout=10-15",
|
||||
"max_handshake_attempts=18",
|
||||
"random_trailers=true",
|
||||
"disable_cookies=true",
|
||||
} {
|
||||
if !strings.Contains(got, want+"\n") {
|
||||
t.Errorf("UAPI() missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUAPIOmitsAWG3Defaults(t *testing.T) {
|
||||
cfg := minimalConfig()
|
||||
cfg.Amnezia = AmneziaParams{Jc: 4, Jmin: 40, Jmax: 70}
|
||||
|
||||
got, err := UAPI(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("UAPI() error = %v", err)
|
||||
}
|
||||
for _, unwanted := 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(got, unwanted) {
|
||||
t.Errorf("UAPI() contains %q for zero value:\n%s", unwanted, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRange(t *testing.T) {
|
||||
for _, ok := range []string{"", "0", "25", "10-40", "7-7"} {
|
||||
if err := ValidateRange(ok); err != nil {
|
||||
t.Errorf("ValidateRange(%q) = %v, want nil", ok, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"40-10", "a", "-5", "1-", "1-2-3", "off"} {
|
||||
if err := ValidateRange(bad); err == nil {
|
||||
t.Errorf("ValidateRange(%q) = nil, want error", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateHeaderProtectionNeedsPadding(t *testing.T) {
|
||||
cfg := minimalConfig()
|
||||
cfg.Amnezia = AmneziaParams{
|
||||
S1: MinHeaderProtectionPadding, S2: MinHeaderProtectionPadding,
|
||||
S3: MinHeaderProtectionPadding, S4: MinHeaderProtectionPadding - 1,
|
||||
HeaderProtectionKey: testKey(0x04),
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate() = nil, want error for short S4")
|
||||
}
|
||||
|
||||
cfg.Amnezia.S4 = MinHeaderProtectionPadding
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsBadRange(t *testing.T) {
|
||||
cfg := minimalConfig()
|
||||
cfg.Amnezia = AmneziaParams{RekeyTimeout: "40-10"}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate() = nil, want error for inverted range")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +123,51 @@ func applyInterface(cfg *tunnel.Config, key, value string) error {
|
||||
case "i1", "i2", "i3", "i4", "i5":
|
||||
idx := int(key[1] - '1')
|
||||
cfg.Amnezia.I[idx] = value
|
||||
case "headerprotectionkey":
|
||||
k, err := parseKey(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("headerprotectionkey: %w", err)
|
||||
}
|
||||
cfg.Amnezia.HeaderProtectionKey = k
|
||||
case "contentpaddingaddition", "rekeyaftertime", "rekeytimeout",
|
||||
"rejectaftertime", "keepalivetimeout", "maxhandshakeattempts":
|
||||
if err := tunnel.ValidateRange(value); err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
setAmneziaRange(&cfg.Amnezia, key, value)
|
||||
case "randomtrailers", "disablecookies":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
if key == "randomtrailers" {
|
||||
cfg.Amnezia.RandomTrailers = b
|
||||
} else {
|
||||
cfg.Amnezia.DisableCookies = b
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown [Interface] key %q", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setAmneziaRange(p *tunnel.AmneziaParams, key, value string) {
|
||||
switch key {
|
||||
case "contentpaddingaddition":
|
||||
p.ContentPaddingAddition = value
|
||||
case "rekeyaftertime":
|
||||
p.RekeyAfterTime = value
|
||||
case "rekeytimeout":
|
||||
p.RekeyTimeout = value
|
||||
case "rejectaftertime":
|
||||
p.RejectAfterTime = value
|
||||
case "keepalivetimeout":
|
||||
p.KeepaliveTimeout = value
|
||||
case "maxhandshakeattempts":
|
||||
p.MaxHandshakeAttempts = value
|
||||
}
|
||||
}
|
||||
|
||||
func setAmneziaInt(p *tunnel.AmneziaParams, key string, n int) {
|
||||
switch key {
|
||||
case "jc":
|
||||
|
||||
@@ -202,3 +202,56 @@ func TestParseFeedsUAPI(t *testing.T) {
|
||||
t.Errorf("UAPI() = %s", uapi)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAWG3Params(t *testing.T) {
|
||||
hpKey := base64.StdEncoding.EncodeToString(bytesOf(0x04))
|
||||
text := "[Interface]\nPrivateKey = " + privKey + "\nAddress = 10.8.0.2/32\n" +
|
||||
"S1 = 12\nS2 = 12\nS3 = 12\nS4 = 12\n" +
|
||||
"HeaderProtectionKey = " + hpKey + "\n" +
|
||||
"ContentPaddingAddition = 10-40\n" +
|
||||
"RekeyAfterTime = 90-140\nRekeyTimeout = 5\nRejectAfterTime = 180\n" +
|
||||
"KeepaliveTimeout = 10-15\nMaxHandshakeAttempts = 18\n" +
|
||||
"RandomTrailers = true\nDisableCookies = 1\n\n" +
|
||||
"[Peer]\nPublicKey = " + pubKey + "\nAllowedIPs = 0.0.0.0/0\n"
|
||||
|
||||
cfg, err := Parse(text)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
p := cfg.Amnezia
|
||||
if p.HeaderProtectionKey != tunnel.Key(bytesOf(0x04)) {
|
||||
t.Error("HeaderProtectionKey not parsed")
|
||||
}
|
||||
for _, kv := range []struct{ got, want string }{
|
||||
{p.ContentPaddingAddition, "10-40"},
|
||||
{p.RekeyAfterTime, "90-140"},
|
||||
{p.RekeyTimeout, "5"},
|
||||
{p.RejectAfterTime, "180"},
|
||||
{p.KeepaliveTimeout, "10-15"},
|
||||
{p.MaxHandshakeAttempts, "18"},
|
||||
} {
|
||||
if kv.got != kv.want {
|
||||
t.Errorf("range = %q, want %q", kv.got, kv.want)
|
||||
}
|
||||
}
|
||||
if !p.RandomTrailers || !p.DisableCookies {
|
||||
t.Errorf("bool flags = %v/%v, want true/true", p.RandomTrailers, p.DisableCookies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAWG3Errors(t *testing.T) {
|
||||
base := "[Interface]\nPrivateKey = " + privKey + "\nAddress = 10.8.0.2/32\n"
|
||||
peer := "\n[Peer]\nPublicKey = " + pubKey + "\nAllowedIPs = 0.0.0.0/0\n"
|
||||
|
||||
for name, line := range map[string]string{
|
||||
"bad range": "RekeyTimeout = 40-10\n",
|
||||
"non-numeric": "KeepaliveTimeout = soon\n",
|
||||
"bad bool": "RandomTrailers = maybe\n",
|
||||
"short hp key": "HeaderProtectionKey = " + base64.StdEncoding.EncodeToString([]byte("short")) + "\n",
|
||||
"hp without pad": "HeaderProtectionKey = " + base64.StdEncoding.EncodeToString(bytesOf(0x04)) + "\n",
|
||||
} {
|
||||
if _, err := Parse(base + line + peer); err == nil {
|
||||
t.Errorf("%s: Parse() = nil, want error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,19 @@ JSON парсится с `DisallowUnknownFields`, поэтому конфиг с
|
||||
|
||||
Текст WireGuard-конфигурации (`wg-quick`) передается в `tunnel.config`. В режиме `wg` параметры обфускации AmneziaWG игнорируются. Поле `Endpoint` из конфига не используется, так как трафик идет через TURN-релей.
|
||||
|
||||
В секции `[Interface]` понимаются параметры AmneziaWG: `Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4`, `I1`-`I5`, а также AWG 3+:
|
||||
|
||||
| Ключ | Значение | Должен совпадать с сервером |
|
||||
|---|---|---|
|
||||
| `HeaderProtectionKey` | base64-ключ 32 байта | да |
|
||||
| `ContentPaddingAddition` | `N` или `LO-HI` | нет |
|
||||
| `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime`, `KeepaliveTimeout` | `N` или `LO-HI`, секунды | нет |
|
||||
| `MaxHandshakeAttempts` | `N` или `LO-HI`, попытки | нет |
|
||||
| `RandomTrailers` | bool | да |
|
||||
| `DisableCookies` | bool | нет |
|
||||
|
||||
`HeaderProtectionKey` берёт nonce из crypto-паддинга, поэтому требует `S1`-`S4` не меньше 12 - иначе конфиг отклоняется.
|
||||
|
||||
Адреса, DNS, маршруты и MTU tun-интерфейса ядро не применяет - это работа платформы. Их отдаёт `ParseTunnelConfig` из того же текста, что уходит в `tunnel.config` (аргумент `mtu` - то же значение, что в `tunnel.mtu`; `0` - взять из конфига):
|
||||
|
||||
```kotlin
|
||||
|
||||
Reference in New Issue
Block a user