summaryrefslogtreecommitdiff
path: root/internal/config
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config')
-rw-r--r--internal/config/builder.go182
-rw-r--r--internal/config/builder_test.go232
-rw-r--r--internal/config/bypass.go139
-rw-r--r--internal/config/modes.go176
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
+}