/* SPDX-License-Identifier: MIT * * Phobos */ package conf import ( "reflect" "strconv" "strings" "testing" "golang.zx2c4.com/wireguard/windows/phobos" ) const wireGuardModeConfig = `[Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= Address = 10.8.0.2/32, fdcc:ad94:bacf:61a3::2/128 MTU = 1420 DNS = 1.1.1.1 [Peer] PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= PresharedKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 25 Endpoint = 127.0.0.1:51822 [instance] source-if = 127.0.0.1 source-lport = 51822 target = vpn.example.com:51823 key = Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI= masking = MEDIA obfuscate-bytes = 16 max-dummy = 4 verbose = 2 ` const socks5ModeConfig = `[instance] mode = socks5 role = client source-if = 127.0.0.1 source-lport = 1080 target = vpn.example.com:51824 key = Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI= masking = STUN verbose = 2 [socks5] login = phobos-user password = s3cr3t ` func parseConfig(t *testing.T, text string) *Config { t.Helper() config, err := FromWgQuick(text, "test") if err != nil { t.Fatalf("unable to parse: %v", err) } return config } func TestParseWireGuardModeObfuscation(t *testing.T) { config := parseConfig(t, wireGuardModeConfig) if config.IsSocks5() { t.Fatal("config must not be a SOCKS5 tunnel") } if len(config.Peers) != 1 { t.Fatalf("expected one peer, got %d", len(config.Peers)) } o := config.Peers[0].Obfuscation if o == nil { t.Fatal("peer has no obfuscation") } if o.Mode != ObfuscationModeWireGuard { t.Errorf("mode = %v", o.Mode) } if o.Target.Host != "vpn.example.com" || o.Target.Port != 51823 { t.Errorf("target = %v", o.Target.String()) } if o.Key != "Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI=" { t.Errorf("key = %q", o.Key) } if o.Masking != phobos.MaskingMEDIA { t.Errorf("masking = %v", o.Masking) } if o.ObfuscateBytes != 16 || o.MaxDummy != 4 || o.SourceListenPort != 51822 { t.Errorf("obfuscate-bytes = %d, max-dummy = %d, source-lport = %d", o.ObfuscateBytes, o.MaxDummy, o.SourceListenPort) } } func TestParseSocks5Mode(t *testing.T) { config := parseConfig(t, socks5ModeConfig) if !config.IsSocks5() { t.Fatal("config must be a SOCKS5 tunnel") } if len(config.Peers) != 0 { t.Fatalf("SOCKS5 tunnels carry no peers, got %d", len(config.Peers)) } o := config.Obfuscation if o.Target.Host != "vpn.example.com" || o.Target.Port != 51824 { t.Errorf("target = %v", o.Target.String()) } if o.Masking != phobos.MaskingSTUN { t.Errorf("masking = %v", o.Masking) } if o.SourceListenPort != 1080 { t.Errorf("source-lport = %d", o.SourceListenPort) } if o.Login != "phobos-user" || o.Password != "s3cr3t" { t.Errorf("credentials = %q/%q", o.Login, o.Password) } } func TestObfuscationSurvivesRoundTrip(t *testing.T) { for name, text := range map[string]string{"wireguard": wireGuardModeConfig, "socks5": socks5ModeConfig} { t.Run(name, func(t *testing.T) { first := parseConfig(t, text) serialized := first.ToWgQuick() second := parseConfig(t, serialized) if serialized != second.ToWgQuick() { t.Fatalf("round trip is not stable:\n%s\n---\n%s", serialized, second.ToWgQuick()) } firstObfuscation, secondObfuscation := first.Obfuscation, second.Obfuscation if !first.IsSocks5() { firstObfuscation, secondObfuscation = first.Peers[0].Obfuscation, second.Peers[0].Obfuscation } firstObfuscation.Comments, firstObfuscation.Socks5Comments = SectionComments{}, SectionComments{} secondObfuscation.Comments, secondObfuscation.Socks5Comments = SectionComments{}, SectionComments{} if !reflect.DeepEqual(*firstObfuscation, *secondObfuscation) { t.Fatalf("obfuscation drifted:\n%+v\n%+v", *firstObfuscation, *secondObfuscation) } }) } } func TestSerializedSocks5HasNoInterface(t *testing.T) { serialized := parseConfig(t, socks5ModeConfig).ToWgQuick() if strings.Contains(serialized, "[Interface]") || strings.Contains(serialized, "PrivateKey") { t.Fatalf("SOCKS5 tunnel must not serialize a WireGuard interface:\n%s", serialized) } if !strings.Contains(serialized, "mode = socks5") || !strings.Contains(serialized, "[Socks5]") { t.Fatalf("missing SOCKS5 sections:\n%s", serialized) } } func TestMediaObfuscateBytesDefaultsWhenAbsent(t *testing.T) { text := strings.Replace(wireGuardModeConfig, "obfuscate-bytes = 16\n", "", 1) config := parseConfig(t, text) if got := config.Peers[0].Obfuscation.ObfuscateBytes; got != phobos.MediaObfuscateBytesDefault { t.Fatalf("obfuscate-bytes = %d, want %d", got, phobos.MediaObfuscateBytesDefault) } } func TestMediaObfuscateBytesKeepsExplicitZero(t *testing.T) { text := strings.Replace(wireGuardModeConfig, "obfuscate-bytes = 16", "obfuscate-bytes = 0", 1) config := parseConfig(t, text) if got := config.Peers[0].Obfuscation.ObfuscateBytes; got != 0 { t.Fatalf("obfuscate-bytes = %d, want 0", got) } } func TestObfuscateBytesSurvivesSerialization(t *testing.T) { for _, value := range []uint16{0, 16} { text := strings.Replace(wireGuardModeConfig, "obfuscate-bytes = 16", "obfuscate-bytes = "+strconv.Itoa(int(value)), 1) serialized := parseConfig(t, text).ToWgQuick() if got := parseConfig(t, serialized).Peers[0].Obfuscation.ObfuscateBytes; got != value { t.Fatalf("obfuscate-bytes = %d after round trip, want %d:\n%s", got, value, serialized) } } } func TestMediaSSRCAcceptsHexAndDecimal(t *testing.T) { for value, want := range map[string]uint32{"0xDEADBEEF": 0xDEADBEEF, "3735928559": 0xDEADBEEF, "0": 0} { text := strings.Replace(wireGuardModeConfig, "masking = MEDIA", "masking = MEDIA\nmedia-ssrc = "+value, 1) config := parseConfig(t, text) if got := config.Peers[0].Obfuscation.MediaSSRC; got != want { t.Errorf("media-ssrc %q parsed as %d, want %d", value, got, want) } } } func TestMediaParamsDeriveTimestampStep(t *testing.T) { o := &Obfuscation{MediaPayloadType: 102, MediaSSRC: 7, MediaClock: 30} params := o.MediaParams() if params.TimestampStep != 3000 { t.Fatalf("timestamp step = %d, want 3000", params.TimestampStep) } if params.PayloadType != 102 || params.SSRC != 7 { t.Fatalf("unexpected media params %+v", params) } if (&Obfuscation{}).MediaParams().TimestampStep != 0 { t.Fatal("timestamp step must stay zero without a media clock") } } func TestObfuscationMatchesPeerBySourcePort(t *testing.T) { text := `[Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= Address = 10.8.0.2/32 [Peer] PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= AllowedIPs = 10.0.0.0/24 Endpoint = 127.0.0.1:51001 [Peer] PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= AllowedIPs = 10.0.1.0/24 Endpoint = 127.0.0.1:51002 [instance] source-lport = 51002 target = second.example.com:1 key = second-key masking = STUN [instance] source-lport = 51001 target = first.example.com:2 key = first-key masking = none ` config := parseConfig(t, text) if config.Peers[0].Obfuscation.Target.Host != "first.example.com" { t.Errorf("first peer got %v", config.Peers[0].Obfuscation.Target) } if config.Peers[1].Obfuscation.Target.Host != "second.example.com" { t.Errorf("second peer got %v", config.Peers[1].Obfuscation.Target) } } func TestObfuscationRejectsBadInput(t *testing.T) { cases := map[string]string{ "unknown key": strings.Replace(wireGuardModeConfig, "max-dummy = 4", "max-dumy = 4", 1), "bad masking": strings.Replace(wireGuardModeConfig, "masking = MEDIA", "masking = quic", 1), "server role": strings.Replace(socks5ModeConfig, "role = client", "role = server", 1), "missing target": strings.Replace(wireGuardModeConfig, "target = vpn.example.com:51823\n", "", 1), "missing key": strings.Replace(wireGuardModeConfig, "key = Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI=\n", "", 1), "socks5 key": strings.Replace(socks5ModeConfig, "login = phobos-user", "user = phobos-user", 1), "orphan instance": strings.Replace(wireGuardModeConfig, "masking = MEDIA", "mode = socks5", 1), } for name, text := range cases { if _, err := FromWgQuick(text, "test"); err == nil { t.Errorf("%s: expected a parse error", name) } } } func TestObfuscationAcceptsBinaryOnlyOptions(t *testing.T) { text := strings.Replace(wireGuardModeConfig, "max-dummy = 4", "max-dummy = 4\nthreads = 2\nidle-timeout = 300\nfwmark = 51820", 1) if _, err := FromWgQuick(text, "test"); err != nil { t.Fatalf("options meant for the obfuscator binary must be accepted: %v", err) } } func TestRedactClearsObfuscationSecrets(t *testing.T) { config := parseConfig(t, socks5ModeConfig) config.Redact() o := config.Obfuscation if o.Key != "" || o.Login != "" || o.Password != "" { t.Fatalf("secrets survived redaction: %+v", *o) } if o.Target.Host != "vpn.example.com" { t.Fatal("redaction must keep the target") } }