diff options
Diffstat (limited to 'internal/config/builder_test.go')
| -rw-r--r-- | internal/config/builder_test.go | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/internal/config/builder_test.go b/internal/config/builder_test.go new file mode 100644 index 0000000..0d46659 --- /dev/null +++ b/internal/config/builder_test.go @@ -0,0 +1,431 @@ +package config_test + +import ( + "encoding/json" + "strings" + "testing" + + "vpnem/internal/config" + "vpnem/internal/models" +) + +func TestBuildConfigSocks(t *testing.T) { + server := models.Server{ + Tag: "nl-1", Region: "NL", Type: "socks", + Server: "5.180.97.200", ServerPort: 54101, UDPOverTCP: true, + } + mode := *config.ModeByName("Lovense + OBS + AnyDesk + Discord") + ruleSets := []models.RuleSet{} + + cfg := config.BuildConfig(server, mode, ruleSets, []string{"5.180.97.200"}) + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + s := string(data) + + // Verify outbound type + if !strings.Contains(s, `"type":"socks"`) { + t.Error("expected socks outbound") + } + if !strings.Contains(s, `"type":"socks"`) { + t.Error("expected local socks inbound") + } + if !strings.Contains(s, `"listen":"127.0.0.1"`) { + t.Error("expected local socks proxy listen host") + } + if !strings.Contains(s, `"listen_port":10800`) && !strings.Contains(s, `"listen_port": 10800`) { + t.Error("expected local socks proxy on port 10800") + } + // Verify bypass processes present + if !strings.Contains(s, "chromium.exe") { + t.Error("expected chromium.exe in direct bypass list") + } + if !strings.Contains(s, "Performer Application v5.x.exe") { + t.Error("expected Performer Application v5.x.exe in direct bypass list") + } + if !strings.Contains(s, "Яндекс Музыка.exe") { + t.Error("expected Яндекс Музыка.exe in direct bypass list") + } + if strings.Contains(s, "chrome.exe") { + t.Error("did not expect chrome.exe in direct bypass list") + } + if strings.Contains(s, "firefox.exe") { + t.Error("did not expect firefox.exe in direct bypass list") + } + if strings.Contains(s, "msedgewebview2.exe") { + t.Error("did not expect msedgewebview2.exe in direct bypass list") + } + if !strings.Contains(s, "obs64.exe") { + t.Error("expected obs64.exe in config rules") + } + // Verify Lovense regex + if !strings.Contains(s, "lovense") { + t.Error("expected lovense process regex") + } + // Verify ip_is_private + if !strings.Contains(s, "ip_is_private") { + t.Error("expected ip_is_private rule") + } + // Verify NCSI domains + if !strings.Contains(s, "msftconnecttest.com") { + t.Error("expected NCSI domain") + } + // Verify Telegram + if !strings.Contains(s, "telegram.org") { + t.Error("expected telegram domains") + } + if !strings.Contains(s, "Telegram.exe") { + t.Error("expected Telegram.exe process rule") + } + // Verify Discord IPs + if !strings.Contains(s, "162.159.130.234/32") { + t.Error("expected discord IPs") + } + // Verify final is direct + if !strings.Contains(s, `"final":"direct"`) { + t.Error("expected final: direct") + } + // Verify TUN config + if !strings.Contains(s, "vpnem") { + t.Error("expected TUN interface name vpnem") + } + // Verify DNS + if !strings.Contains(s, "proxy-dns") { + t.Error("expected proxy-dns server") + } + // Verify cache_file + if !strings.Contains(s, "cache_file") { + t.Error("expected cache_file in experimental") + } + // sing-box 1.12: sniff/hijack-dns are route actions, not inbound flags. + if strings.Contains(s, `"sniff":true`) { + t.Error("did not expect legacy inbound sniff flags in 1.12 config") + } + if strings.Contains(s, `"sniff_override_destination":true`) { + t.Error("did not expect legacy sniff_override_destination in 1.12 config") + } + if !strings.Contains(s, `"action":"sniff"`) { + t.Error("expected route sniff action in 1.12 config") + } + if !strings.Contains(s, `"action":"hijack-dns"`) { + t.Error("expected route hijack-dns action in 1.12 config") + } + // sing-box 1.12: DoH servers use type+server, not address URLs. + if strings.Contains(s, `dns-query`) { + t.Error("did not expect legacy dns-query URLs in 1.12 config") + } + if !strings.Contains(s, `"type":"https"`) { + t.Error("expected https DNS server type") + } + if !strings.Contains(s, `"server":"1.1.1.1"`) { + t.Error("expected 1.1.1.1 DoH server") + } + if !strings.Contains(s, "default_domain_resolver") { + t.Error("expected default_domain_resolver in route") + } +} + +func TestBuildConfigVLESS(t *testing.T) { + server := models.Server{ + Tag: "nl-vless", Region: "NL", Type: "vless", + Server: "5.180.97.200", ServerPort: 443, UUID: "test-uuid", + TLS: &models.TLS{Enabled: true, ServerName: "test.example.com"}, + Transport: &models.Transport{Type: "ws", Path: "/test"}, + } + mode := *config.ModeByName("Full (All Traffic)") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"type":"vless"`) { + t.Error("expected vless outbound") + } + if !strings.Contains(s, "test-uuid") { + t.Error("expected uuid") + } + if !strings.Contains(s, `"final":"proxy"`) { + t.Error("expected final: proxy for Full mode") + } +} + +func TestBuildConfigVLESSReality(t *testing.T) { + server := models.Server{ + Tag: "nl-reality", + Region: "NL", + Type: "vless-reality", + Server: "203.0.113.20", + ServerPort: 443, + UUID: "33333333-3333-3333-3333-333333333333", + TLS: &models.TLS{ + Enabled: true, + ServerName: "login.microsoftonline.com", + Reality: &models.Reality{ + Enabled: true, + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + ShortID: "0123456789abcdef", + Fingerprint: "chrome", + }, + }, + } + mode := *config.ModeByName("Full (All Traffic)") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"type":"vless"`) { + t.Error("expected vless outbound for reality") + } + if !strings.Contains(s, `"public_key":"jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0"`) { + t.Error("expected reality public key") + } + if !strings.Contains(s, `"short_id":"0123456789abcdef"`) { + t.Error("expected reality short id") + } + if !strings.Contains(s, `"fingerprint":"chrome"`) { + t.Error("expected reality utls fingerprint") + } +} + +func TestBuildConfigShadowsocks(t *testing.T) { + server := models.Server{ + Tag: "nl-ss", Region: "NL", Type: "shadowsocks", + Server: "5.180.97.200", ServerPort: 36728, + Method: "chacha20-ietf-poly1305", Password: "test-pass", + } + mode := *config.ModeByName("Discord Only") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"type":"shadowsocks"`) { + t.Error("expected shadowsocks outbound") + } + if !strings.Contains(s, "chacha20-ietf-poly1305") { + t.Error("expected method") + } +} + +func TestBuildConfigVMess(t *testing.T) { + server := models.Server{ + Tag: "nl-vmess", Region: "NL", Type: "vmess", + Server: "nl.example.com", ServerPort: 8444, UUID: "22222222-2222-2222-2222-222222222222", + TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com"}, + Transport: &models.Transport{Type: "ws", Path: "/vmess"}, + } + mode := *config.ModeByName("Full (All Traffic)") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"type":"vmess"`) { + t.Error("expected vmess outbound") + } + if !strings.Contains(s, "22222222-2222-2222-2222-222222222222") { + t.Error("expected vmess uuid") + } + if !strings.Contains(s, `"/vmess"`) { + t.Error("expected vmess ws path") + } +} + +func TestBuildConfigHysteria2(t *testing.T) { + server := models.Server{ + Tag: "nl-hy2", Region: "NL", Type: "hysteria2", + Server: "nl.example.com", ServerPort: 9443, Password: "hy2-secret", ObfsPassword: "obfs-secret", + UpMbps: 80, DownMbps: 90, + TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com", Insecure: true, ALPN: []string{"h3"}, MinVersion: "1.3", MaxVersion: "1.3"}, + } + mode := *config.ModeByName("Full (All Traffic)") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"type":"hysteria2"`) { + t.Error("expected hysteria2 outbound") + } + if !strings.Contains(s, `"password":"hy2-secret"`) { + t.Error("expected hysteria2 password") + } + if !strings.Contains(s, `"salamander"`) { + t.Error("expected hysteria2 obfs configuration") + } + if !strings.Contains(s, `"up_mbps":80`) && !strings.Contains(s, `"up_mbps": 80`) { + t.Error("expected hysteria2 up_mbps") + } + if !strings.Contains(s, `"insecure":true`) && !strings.Contains(s, `"insecure": true`) { + t.Error("expected hysteria2 tls.insecure") + } + if !strings.Contains(s, `"alpn":["h3"]`) && !strings.Contains(s, `"alpn": ["h3"]`) { + t.Error("expected hysteria2 tls alpn h3") + } + if !strings.Contains(s, `"min_version":"1.3"`) && !strings.Contains(s, `"min_version": "1.3"`) { + t.Error("expected hysteria2 tls min_version") + } +} + +func TestBuildConfigSplitRealityHysteria2(t *testing.T) { + server := models.Server{ + Tag: "nl-multi", + Region: "NL", + Type: "vless-reality", + Server: "203.0.113.50", + ServerPort: 443, + UUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + TLS: &models.TLS{ + Enabled: true, + ServerName: "www.microsoft.com", + Reality: &models.Reality{ + Enabled: true, + PublicKey: "pubkey", + ShortID: "abcdef1234567890", + Fingerprint: "chrome", + }, + }, + Companions: []models.Server{ + { + Tag: "nl-multi-hysteria2", + Region: "NL", + Type: "hysteria2", + Server: "203.0.113.50", + ServerPort: 443, + Password: "hy2-secret", + ObfsPassword: "obfs-secret", + UpMbps: 100, + DownMbps: 100, + TLS: &models.TLS{ + Enabled: true, + Insecure: true, + ALPN: []string{"h3"}, + MinVersion: "1.3", + MaxVersion: "1.3", + }, + }, + }, + } + mode := *config.ModeByName("Full (All Traffic)") + + cfg := config.BuildConfig(server, mode, nil, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, `"tag":"vless-out"`) { + t.Fatal("expected vless-out outbound tag") + } + if !strings.Contains(s, `"tag":"hysteria2-out"`) { + t.Fatal("expected hysteria2-out outbound tag") + } + if !strings.Contains(s, `"network":["tcp"]`) || !strings.Contains(s, `"outbound":"vless-out"`) { + t.Fatal("expected tcp split routing rule") + } + if !strings.Contains(s, `"network":["udp"]`) || !strings.Contains(s, `"outbound":"hysteria2-out"`) { + t.Fatal("expected udp split routing rule") + } + if !strings.Contains(s, `"detour":"vless-out"`) { + t.Fatal("expected proxy DNS detour via vless-out") + } +} + +func TestBuildConfigWithRuleSets(t *testing.T) { + server := models.Server{ + Tag: "nl-1", Type: "socks", Server: "1.2.3.4", ServerPort: 1080, + } + mode := *config.ModeByName("Re-filter (обход блокировок РФ)") + ruleSets := []models.RuleSet{ + {Tag: "refilter-domains", URL: "https://example.com/domains.srs", Format: "binary"}, + {Tag: "refilter-ip", URL: "https://example.com/ip.srs", Format: "binary"}, + {Tag: "discord-voice", URL: "https://example.com/discord.srs", Format: "binary"}, + } + + cfg := config.BuildConfig(server, mode, ruleSets, nil) + data, _ := json.Marshal(cfg) + s := string(data) + + if !strings.Contains(s, "refilter-domains") { + t.Error("expected refilter-domains rule_set") + } + if !strings.Contains(s, "download_detour") { + t.Error("expected download_detour in rule_set") + } + if !strings.Contains(s, "update_interval") { + t.Error("expected update_interval in rule_set") + } +} + +func TestBuildBypassIPs(t *testing.T) { + ips := config.BuildBypassIPs(nil, []string{"1.2.3.4", "5.180.97.200"}) + + found := false + for _, ip := range ips { + if ip == "1.2.3.4/32" { + found = true + } + } + if !found { + t.Error("expected dynamic server IP in bypass list") + } + + // 5.180.97.200 is already in StaticBypassIPs, should not be duplicated + count := 0 + for _, ip := range ips { + if ip == "5.180.97.200/32" { + count++ + } + } + if count != 1 { + t.Errorf("expected 5.180.97.200/32 exactly once, got %d", count) + } +} + +func TestBuildBypassIPsIgnoresHostnames(t *testing.T) { + ips := config.BuildBypassIPs(nil, []string{"xui5.em-sysadmin.xyz", "1.2.3.4"}) + + for _, ip := range ips { + if ip == "xui5.em-sysadmin.xyz/32" { + t.Fatal("expected hostname to be ignored in bypass IP list") + } + } +} + +func TestAllModes(t *testing.T) { + modes := config.AllModes() + if len(modes) != 7 { + t.Errorf("expected 7 modes, got %d", len(modes)) + } + + names := config.ModeNames() + expected := []string{ + "Lovense + OBS + AnyDesk", + "Lovense + OBS + AnyDesk + Discord", + "Lovense + OBS + AnyDesk + Discord + Teams", + "Discord Only", + "Full (All Traffic)", + "Re-filter (обход блокировок РФ)", + "Комбо (приложения + Re-filter)", + } + for i, name := range expected { + if names[i] != name { + t.Errorf("mode %d: expected %q, got %q", i, name, names[i]) + } + } +} + +func TestModeByName(t *testing.T) { + m := config.ModeByName("Full (All Traffic)") + if m == nil { + t.Fatal("expected to find Full mode") + } + if m.Final != "proxy" { + t.Errorf("Full mode final should be proxy, got %s", m.Final) + } + + if config.ModeByName("nonexistent") != nil { + t.Error("expected nil for nonexistent mode") + } +} |
