summaryrefslogtreecommitdiff
path: root/internal/config/builder_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/builder_test.go')
-rw-r--r--internal/config/builder_test.go431
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")
+ }
+}