diff options
| author | SergeiEU <39683682+SergeiEU@users.noreply.github.com> | 2026-04-01 10:17:15 +0400 |
|---|---|---|
| committer | SergeiEU <39683682+SergeiEU@users.noreply.github.com> | 2026-04-01 10:17:15 +0400 |
| commit | 1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 (patch) | |
| tree | d8c85273ede547e03a5727bf185f5d07e87b4a08 /internal/config | |
| download | vpnem-main.tar.gz vpnem-main.tar.bz2 vpnem-main.zip | |
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/builder.go | 182 | ||||
| -rw-r--r-- | internal/config/builder_test.go | 232 | ||||
| -rw-r--r-- | internal/config/bypass.go | 139 | ||||
| -rw-r--r-- | internal/config/modes.go | 176 |
4 files changed, 729 insertions, 0 deletions
diff --git a/internal/config/builder.go b/internal/config/builder.go new file mode 100644 index 0000000..a9e513b --- /dev/null +++ b/internal/config/builder.go @@ -0,0 +1,182 @@ +package config + +import ( + "vpnem/internal/models" +) + +type SingBoxConfig struct { + DNS map[string]any `json:"dns"` + Inbounds []map[string]any `json:"inbounds"` + Outbounds []map[string]any `json:"outbounds"` + Route map[string]any `json:"route"` + Experimental map[string]any `json:"experimental,omitempty"` +} + +var blockedDomains = []string{ + // Telegram + "telegram.org", "t.me", "telegram.me", "telegra.ph", "telegram.dog", + "web.telegram.org", + // Discord + "discord.com", "discord.gg", "discordapp.com", "discordapp.net", + // Meta + "instagram.com", "cdninstagram.com", "ig.me", "igcdn.com", + "facebook.com", "fb.com", "fbcdn.net", "fbsbx.com", "fb.me", + "whatsapp.com", "whatsapp.net", + // Twitter/X + "twitter.com", "x.com", "twimg.com", "t.co", + // AI + "openai.com", "chatgpt.com", "oaistatic.com", "oaiusercontent.com", + "claude.ai", "anthropic.com", + // YouTube/Google + "youtube.com", "googlevideo.com", "youtu.be", "ggpht.com", "ytimg.com", + "gstatic.com", "doubleclick.net", "googleadservices.com", + // Cam sites + "stripchat.com", "stripchat.global", "ststandard.com", "strpssts-ana.com", + "strpst.com", "striiiipst.com", + "chaturbate.com", "highwebmedia.com", "cb.dev", + "camsoda.com", "cam4.com", "cam101.com", + "bongamodels.com", "flirt4free.com", "privatecams.com", + "streamray.com", "cams.com", "homelivesex.com", + "skyprivate.com", "mywebcamroom.com", "livemediahost.com", + // Cam CDNs + "xcdnpro.com", "mmcdn.com", "vscdns.com", "bgicdn.com", "bgmicdn.com", + "doppiocdn.com", "doppiocdn.net", "doppiostreams.com", + "fanclubs.tech", "my.club", "chapturist.com", + // Cam analytics/services + "moengage.com", "amplitude.com", "dwin1.com", + "eizzih.com", "loo3laej.com", "iesnare.com", + "hytto.com", "zendesk.com", + // Lovense + "lovense.com", "lovense-api.com", "lovense.club", + // Bitrix + "bitrix24.ru", "bitrix24.com", + // Cloudflare + "cloudflare.com", + // Other blocked + "viber.com", "linkedin.com", "spotify.com", + "ntc.party", "ipify.org", + "rutracker.org", "rutracker.net", "rutracker.me", + "4pda.to", "kinozal.tv", "nnmclub.to", + "protonmail.com", "proton.me", "tutanota.com", + "medium.com", "archive.org", "soundcloud.com", "twitch.tv", + // IP check + "ifconfig.me", "ifconfig.co", "icanhazip.com", "ipinfo.io", + // Email + "em-mail.ru", +} + +func BuildConfig(server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string) SingBoxConfig { + return BuildConfigFull(server, mode, ruleSets, serverIPs, nil) +} + +// BuildConfigFull — exact vpn.py config. Fast, proven. +func BuildConfigFull(server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string) SingBoxConfig { + bypassIPs := BuildBypassIPs(serverIPs) + bypassProcs := BuildBypassProcesses(customBypass) + + var rules []map[string]any + rules = append(rules, map[string]any{"ip_cidr": bypassIPs, "outbound": "direct"}) + rules = append(rules, map[string]any{"process_name": bypassProcs, "outbound": "direct"}) + rules = append(rules, map[string]any{"domain_suffix": LocalDomainSuffixes, "outbound": "direct"}) + // Bypass own infrastructure — prevent double proxying when OmegaSwitcher is also active + rules = append(rules, map[string]any{"domain_suffix": []string{"em-sysadmin.xyz"}, "outbound": "direct"}) + rules = append(rules, map[string]any{"process_path_regex": LovenseProcessRegex, "outbound": "proxy"}) + rules = append(rules, map[string]any{"ip_cidr": ForcedProxyIPs, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_suffix": TelegramDomains, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_regex": TelegramDomainRegex, "outbound": "proxy"}) + rules = append(rules, map[string]any{"ip_cidr": TelegramIPs, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_suffix": blockedDomains, "outbound": "proxy"}) + + for _, r := range mode.Rules { + rule := map[string]any{"outbound": r.Outbound} + if len(r.DomainSuffix) > 0 { rule["domain_suffix"] = r.DomainSuffix } + if len(r.DomainRegex) > 0 { rule["domain_regex"] = r.DomainRegex } + if len(r.IPCIDR) > 0 { rule["ip_cidr"] = r.IPCIDR } + if len(r.RuleSet) > 0 { rule["rule_set"] = r.RuleSet } + if len(r.Network) > 0 { rule["network"] = r.Network } + if len(r.PortRange) > 0 { rule["port_range"] = r.PortRange } + rules = append(rules, rule) + } + + var ruleSetDefs []map[string]any + for _, rs := range ruleSets { + if rs.URL == "" { continue } + ruleSetDefs = append(ruleSetDefs, map[string]any{ + "tag": rs.Tag, "type": "remote", "format": rs.Format, + "url": rs.URL, "download_detour": "direct", "update_interval": "1d", + }) + } + + route := map[string]any{ + "auto_detect_interface": true, + "final": mode.Final, + "rules": rules, + } + if len(ruleSetDefs) > 0 { + route["rule_set"] = ruleSetDefs + } + + return SingBoxConfig{ + DNS: map[string]any{ + "servers": []map[string]any{ + {"tag": "proxy-dns", "address": "https://8.8.8.8/dns-query", "detour": "proxy"}, + {"tag": "direct-dns", "address": "https://1.1.1.1/dns-query", "detour": "direct"}, + }, + "rules": []map[string]any{ + {"outbound": "proxy", "server": "proxy-dns"}, + {"outbound": "direct", "server": "direct-dns"}, + }, + "strategy": "ipv4_only", + }, + Inbounds: []map[string]any{ + { + "type": "tun", + "interface_name": "singbox", + "address": []string{"172.19.0.1/30"}, + "auto_route": true, + "strict_route": false, + "stack": "gvisor", + "sniff": true, + "sniff_override_destination": true, + }, + }, + Outbounds: []map[string]any{ + buildOutbound(server), + {"type": "direct", "tag": "direct"}, + }, + Route: route, + Experimental: map[string]any{ + "cache_file": map[string]any{ + "enabled": true, + "path": "cache.db", + }, + }, + } +} + + + +func buildOutbound(s models.Server) map[string]any { + switch s.Type { + case "vless": + out := map[string]any{ + "type": "vless", "tag": "proxy", + "server": s.Server, "server_port": s.ServerPort, "uuid": s.UUID, + } + if s.TLS != nil { out["tls"] = map[string]any{"enabled": s.TLS.Enabled, "server_name": s.TLS.ServerName} } + if s.Transport != nil { out["transport"] = map[string]any{"type": s.Transport.Type, "path": s.Transport.Path} } + return out + case "shadowsocks": + return map[string]any{ + "type": "shadowsocks", "tag": "proxy", + "server": s.Server, "server_port": s.ServerPort, + "method": s.Method, "password": s.Password, + } + default: + return map[string]any{ + "type": "socks", "tag": "proxy", + "server": s.Server, "server_port": s.ServerPort, + "udp_over_tcp": s.UDPOverTCP, + } + } +} diff --git a/internal/config/builder_test.go b/internal/config/builder_test.go new file mode 100644 index 0000000..d5c0cb5 --- /dev/null +++ b/internal/config/builder_test.go @@ -0,0 +1,232 @@ +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") + } + // Verify bypass processes present + if !strings.Contains(s, "chrome.exe") { + t.Error("expected chrome.exe in bypass processes") + } + if !strings.Contains(s, "obs64.exe") { + t.Error("expected obs64.exe in bypass processes") + } + // 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") + } + // 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, "singbox") { + t.Error("expected TUN interface name singbox") + } + // Verify DNS + if !strings.Contains(s, "dns-proxy") { + t.Error("expected dns-proxy server") + } + // Verify cache_file + if !strings.Contains(s, "cache_file") { + t.Error("expected cache_file in experimental") + } + // sing-box 1.13: sniff action in route rules, not inbound + if strings.Contains(s, `"sniff":true`) { + t.Error("sniff should NOT be in inbound (removed in 1.13)") + } + if !strings.Contains(s, `"action":"sniff"`) { + t.Error("expected action:sniff in route rules") + } + // sing-box 1.13: hijack-dns action + if !strings.Contains(s, `"action":"hijack-dns"`) { + t.Error("expected hijack-dns action") + } + // sing-box 1.12: new DNS server format (type+server, not address) + if strings.Contains(s, `dns-query`) { + t.Error("DNS should use type+server, not address URL (deprecated in 1.12)") + } + if !strings.Contains(s, `"type":"https"`) { + t.Error("expected https DNS server") + } + if !strings.Contains(s, `"1.1.1.1"`) { + t.Error("expected 1.1.1.1 DoH server") + } + // sing-box 1.12: domain_resolver on outbounds + if !strings.Contains(s, "domain_resolver") { + t.Error("expected domain_resolver on outbounds") + } +} + +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 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 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([]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 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") + } +} diff --git a/internal/config/bypass.go b/internal/config/bypass.go new file mode 100644 index 0000000..6232af0 --- /dev/null +++ b/internal/config/bypass.go @@ -0,0 +1,139 @@ +package config + +// BYPASS_PROCESSES — processes that go direct, bypassing TUN. +// Ported 1:1 from vpn.py. +var BypassProcesses = []string{ + "QTranslate.exe", + "aspia_host.exe", + "aspia_host_service.exe", + "aspia_desktop_agent.exe", + "chrome.exe", + "firefox.exe", + "Performer Application v5.x.exe", + "chromium.exe", + "msedgewebview2.exe", + "Яндекс Музыка.exe", + "obs64.exe", +} + +// LovenseProcessRegex — force Lovense through proxy regardless of mode. +var LovenseProcessRegex = []string{"(?i).*lovense.*"} + +// BYPASS_IPS — VPN server IPs + service IPs, always direct. +// NL servers, RU servers, misc. +var StaticBypassIPs = []string{ + // NL servers + "5.180.97.200/32", "5.180.97.199/32", "5.180.97.198/32", + "5.180.97.197/32", "5.180.97.181/32", + // RU servers + "84.252.100.166/32", "84.252.100.165/32", "84.252.100.161/32", + "84.252.100.117/32", "84.252.100.103/32", + // Misc + "109.107.175.41/32", "146.103.104.48/32", "77.105.138.163/32", + "91.84.113.225/32", "146.103.98.171/32", "94.103.88.252/32", + "178.20.44.93/32", "89.124.70.47/32", +} + +// ReservedCIDRs — ranges not covered by ip_is_private. +var ReservedCIDRs = []string{ + "100.64.0.0/10", // CGNAT / Tailscale + "192.0.0.0/24", // IETF protocol assignments + "192.0.2.0/24", // TEST-NET-1 + "198.51.100.0/24", // TEST-NET-2 + "203.0.113.0/24", // TEST-NET-3 + "240.0.0.0/4", // Reserved (Class E) + "255.255.255.255/32", // Broadcast +} + +// LocalDomainSuffixes — local/mDNS domains, always direct. +var LocalDomainSuffixes = []string{ + "local", "localhost", "lan", "internal", "home.arpa", + "corp", "intranet", "test", "invalid", "example", + "home", "localdomain", +} + +// WindowsNCSIDomains — Windows Network Connectivity Status Indicator. +// Without these going direct, Windows shows "No Internet" warnings. +var WindowsNCSIDomains = []string{ + "msftconnecttest.com", + "msftncsi.com", +} + +// ForcedProxyIPs — IPs that must always go through proxy. +var ForcedProxyIPs = []string{ + "65.21.33.248/32", + "91.132.135.38/32", +} + +// Telegram — hardcoded, applied to ALL modes. +var TelegramDomains = []string{ + "telegram.org", "telegram.me", "t.me", "telegra.ph", "telegram.dog", +} + +var TelegramDomainRegex = []string{ + ".*telegram.*", `.*t\.me.*`, +} + +var TelegramIPs = []string{ + "91.108.56.0/22", "91.108.4.0/22", "91.108.8.0/22", + "91.108.16.0/22", "91.108.12.0/22", "149.154.160.0/20", + "91.105.192.0/23", "91.108.20.0/22", "185.76.151.0/24", +} + +// ProxyDNSDomains — domains NOT in refilter-domains.srs but must resolve via proxy DNS. +// refilter-domains.srs (81k+ domains) covers all RKN-blocked domains. +// This list only has domains missing from .srs that we still need through proxy. +var ProxyDNSDomains = []string{ + // Business-specific (not RKN-blocked) + "lovense.com", "lovense-api.com", "lovense.club", + // Not in refilter but needed + "anthropic.com", + "igcdn.com", "fbsbx.com", + // IP check services (must show proxy exit IP) + "ifconfig.me", "ifconfig.co", "icanhazip.com", "ipinfo.io", "ipify.org", +} + +// IPCheckDomains — domains used for exit IP verification. +var IPCheckDomains = []string{ + "ifconfig.me", "ifconfig.co", "icanhazip.com", "ipinfo.io", +} + +// BuildBypassProcesses merges default + custom bypass processes. +func BuildBypassProcesses(custom []string) []string { + seen := make(map[string]bool, len(BypassProcesses)+len(custom)) + result := make([]string, 0, len(BypassProcesses)+len(custom)) + for _, p := range BypassProcesses { + if !seen[p] { + seen[p] = true + result = append(result, p) + } + } + for _, p := range custom { + if p != "" && !seen[p] { + seen[p] = true + result = append(result, p) + } + } + return result +} + +// BuildBypassIPs merges static bypass IPs with dynamic server IPs. +func BuildBypassIPs(serverIPs []string) []string { + seen := make(map[string]bool, len(StaticBypassIPs)+len(serverIPs)) + result := make([]string, 0, len(StaticBypassIPs)+len(serverIPs)) + + for _, ip := range StaticBypassIPs { + if !seen[ip] { + seen[ip] = true + result = append(result, ip) + } + } + for _, ip := range serverIPs { + cidr := ip + "/32" + if !seen[cidr] { + seen[cidr] = true + result = append(result, cidr) + } + } + return result +} diff --git a/internal/config/modes.go b/internal/config/modes.go new file mode 100644 index 0000000..22f1d2e --- /dev/null +++ b/internal/config/modes.go @@ -0,0 +1,176 @@ +package config + +// Mode defines a routing mode with its specific rules. +type Mode struct { + Name string + Final string // "direct" or "proxy" + Rules []Rule +} + +// Rule represents a single sing-box routing rule. +type Rule struct { + DomainSuffix []string `json:"domain_suffix,omitempty"` + DomainRegex []string `json:"domain_regex,omitempty"` + IPCIDR []string `json:"ip_cidr,omitempty"` + RuleSet []string `json:"rule_set,omitempty"` + Network []string `json:"network,omitempty"` + PortRange []string `json:"port_range,omitempty"` + Outbound string `json:"outbound"` +} + +// Discord IPs — ported 1:1 from vpn.py. +var DiscordIPs = []string{ + "162.159.130.234/32", "162.159.134.234/32", "162.159.133.234/32", + "162.159.135.234/32", "162.159.136.234/32", "162.159.137.232/32", + "162.159.135.232/32", "162.159.136.232/32", "162.159.138.232/32", + "162.159.128.233/32", "198.244.231.90/32", "162.159.129.233/32", + "162.159.130.233/32", "162.159.133.233/32", "162.159.134.233/32", + "162.159.135.233/32", "162.159.138.234/32", "162.159.137.234/32", + "162.159.134.232/32", "162.159.130.235/32", "162.159.129.235/32", + "162.159.129.232/32", "162.159.128.235/32", "162.159.130.232/32", + "162.159.133.232/32", "162.159.128.232/32", "34.126.226.51/32", + // Voice + "66.22.243.0/24", "64.233.165.94/32", "35.207.188.57/32", + "35.207.81.249/32", "35.207.171.222/32", "195.62.89.0/24", + "66.22.192.0/18", "66.22.196.0/24", "66.22.197.0/24", + "66.22.198.0/24", "66.22.199.0/24", "66.22.216.0/24", + "66.22.217.0/24", "66.22.237.0/24", "66.22.238.0/24", + "66.22.241.0/24", "66.22.242.0/24", "66.22.244.0/24", + "64.71.8.96/29", "34.0.240.0/24", "34.0.241.0/24", + "34.0.242.0/24", "34.0.243.0/24", "34.0.244.0/24", + "34.0.245.0/24", "34.0.246.0/24", "34.0.247.0/24", + "34.0.248.0/24", "34.0.249.0/24", "34.0.250.0/24", + "34.0.251.0/24", "12.129.184.160/29", "138.128.136.0/21", + "162.158.0.0/15", "172.64.0.0/13", "34.0.0.0/15", + "34.2.0.0/15", "35.192.0.0/12", "35.208.0.0/12", + "5.200.14.128/25", +} + +var DiscordDomains = []string{ + "discord.com", "discord.gg", "discordapp.com", + "discord.media", "discordapp.net", "discord.net", +} + +var DiscordDomainRegex = []string{".*discord.*"} + +var TeamsDomains = []string{ + "teams.microsoft.com", "teams.cloud.microsoft", "lync.com", + "skype.com", "keydelivery.mediaservices.windows.net", + "streaming.mediaservices.windows.net", +} + +var TeamsIPs = []string{ + "52.112.0.0/14", "52.122.0.0/15", +} + +var TeamsDomainRegex = []string{ + `.*teams\.microsoft.*`, ".*lync.*", ".*skype.*", +} + +var LovenseDomains = []string{ + "lovense-api.com", "lovense.com", "lovense.club", +} + +var LovenseDomainRegex = []string{".*lovense.*"} + +var OBSDomains = []string{"obsproject.com"} +var OBSDomainRegex = []string{".*obsproject.*"} + +var AnyDeskDomains = []string{ + "anydesk.com", "anydesk.com.cn", "net.anydesk.com", +} + +var AnyDeskDomainRegex = []string{".*anydesk.*"} + +// AllModes returns all available routing modes. +func AllModes() []Mode { + baseDomains := append(append(append( + LovenseDomains, OBSDomains...), AnyDeskDomains...), IPCheckDomains...) + baseRegex := append(append( + LovenseDomainRegex, OBSDomainRegex...), AnyDeskDomainRegex...) + + discordDomains := append(append([]string{}, baseDomains...), DiscordDomains...) + discordRegex := append(append([]string{}, baseRegex...), DiscordDomainRegex...) + + teamsDomains := append(append([]string{}, discordDomains...), TeamsDomains...) + teamsRegex := append(append([]string{}, discordRegex...), TeamsDomainRegex...) + + return []Mode{ + { + Name: "Lovense + OBS + AnyDesk", + Final: "direct", + Rules: []Rule{ + {DomainSuffix: baseDomains, DomainRegex: baseRegex, Outbound: "proxy"}, + }, + }, + { + Name: "Lovense + OBS + AnyDesk + Discord", + Final: "direct", + Rules: []Rule{ + {DomainSuffix: discordDomains, DomainRegex: discordRegex, Outbound: "proxy"}, + {IPCIDR: DiscordIPs, Outbound: "proxy"}, + {Network: []string{"udp"}, PortRange: []string{"50000:65535"}, Outbound: "proxy"}, + }, + }, + { + Name: "Lovense + OBS + AnyDesk + Discord + Teams", + Final: "direct", + Rules: []Rule{ + {DomainSuffix: teamsDomains, DomainRegex: teamsRegex, Outbound: "proxy"}, + {IPCIDR: append(append([]string{}, DiscordIPs...), TeamsIPs...), Outbound: "proxy"}, + {Network: []string{"udp"}, PortRange: []string{"3478:3481", "50000:65535"}, Outbound: "proxy"}, + }, + }, + { + Name: "Discord Only", + Final: "direct", + Rules: []Rule{ + {DomainSuffix: append(append([]string{}, DiscordDomains...), IPCheckDomains...), DomainRegex: DiscordDomainRegex, Outbound: "proxy"}, + {IPCIDR: DiscordIPs, Outbound: "proxy"}, + {Network: []string{"udp"}, PortRange: []string{"50000:65535"}, Outbound: "proxy"}, + }, + }, + { + Name: "Full (All Traffic)", + Final: "proxy", + Rules: nil, + }, + { + Name: "Re-filter (обход блокировок РФ)", + Final: "direct", + Rules: []Rule{ + {RuleSet: []string{"refilter-domains", "refilter-ip", "discord-voice"}, Outbound: "proxy"}, + }, + }, + { + Name: "Комбо (приложения + Re-filter)", + Final: "direct", + Rules: []Rule{ + {DomainSuffix: discordDomains, DomainRegex: discordRegex, Outbound: "proxy"}, + {IPCIDR: DiscordIPs, Outbound: "proxy"}, + {Network: []string{"udp"}, PortRange: []string{"50000:65535"}, Outbound: "proxy"}, + {RuleSet: []string{"refilter-domains", "refilter-ip", "discord-voice"}, Outbound: "proxy"}, + }, + }, + } +} + +// ModeByName finds a mode by name, returns nil if not found. +func ModeByName(name string) *Mode { + for _, m := range AllModes() { + if m.Name == name { + return &m + } + } + return nil +} + +// ModeNames returns all available mode names. +func ModeNames() []string { + modes := AllModes() + names := make([]string, len(modes)) + for i, m := range modes { + names[i] = m.Name + } + return names +} |
