diff options
| author | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
|---|---|---|
| committer | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
| commit | 3d51aa455006903345f554a2dd90034993796114 (patch) | |
| tree | 62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/config/builder.go | |
| download | vpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2 vpnem-3d51aa455006903345f554a2dd90034993796114.zip | |
- Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5)
- Smart load balancing via recommendation API
- Windows/Linux client (Go + Wails + sing-box)
- Server API with RealIP detection and connection tracking
- Auto-deployment via vpnui control plane
- Silent Windows installer with UAC elevation
- Load-based server recommendation (no sticky sessions)
- Best Server one-click connection workflow
Diffstat (limited to 'internal/config/builder.go')
| -rw-r--r-- | internal/config/builder.go | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/internal/config/builder.go b/internal/config/builder.go new file mode 100644 index 0000000..96ccdbc --- /dev/null +++ b/internal/config/builder.go @@ -0,0 +1,340 @@ +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"` +} + +const ( + LocalProxyHost = "127.0.0.1" + LocalProxyPort = 10800 + TunInterfaceName = "vpnem" +) + +func BuildConfig(server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string) SingBoxConfig { + return BuildConfigFull(server, mode, ruleSets, serverIPs, nil, nil) +} + +// BuildConfigFull — exact vpn.py config. Fast, proven. +func BuildConfigFull(server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string, policy *models.RoutingPolicy) SingBoxConfig { + return BuildConfigFullWithLocalProxy(server, mode, ruleSets, serverIPs, customBypass, LocalProxyPort, policy) +} + +func BuildConfigFullWithLocalProxy(server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string, localProxyPort int, policy *models.RoutingPolicy) SingBoxConfig { + if hy2, ok := findCompanionProtocol(server, "hysteria2"); ok && (server.Type == "vless-reality" || server.Type == "vless") { + return BuildSplitRoutingConfig(server, hy2, mode, ruleSets, serverIPs, customBypass, localProxyPort, policy) + } + + effectivePolicy := EffectiveRoutingPolicy(policy) + bypassIPs := BuildBypassIPs(effectivePolicy, serverIPs) + bypassProcs := BuildBypassProcesses(effectivePolicy, customBypass) + + var rules []map[string]any + rules = append(rules, map[string]any{"action": "sniff"}) + rules = append(rules, map[string]any{"protocol": "dns", "action": "hijack-dns"}) + rules = append(rules, map[string]any{"ip_is_private": true, "outbound": "direct"}) + rules = append(rules, map[string]any{"ip_cidr": effectivePolicy.ReservedCIDRs, "outbound": "direct"}) + 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": effectivePolicy.WindowsNCSIDomains, "outbound": "direct"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.LocalDomainSuffixes, "outbound": "direct"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.InfraBypassDomains, "outbound": "direct"}) + rules = append(rules, map[string]any{"process_path_regex": effectivePolicy.LovenseProcessRegex, "outbound": "proxy"}) + rules = append(rules, map[string]any{"ip_cidr": effectivePolicy.ForcedProxyIPs, "outbound": "proxy"}) + rules = append(rules, map[string]any{"process_name": effectivePolicy.TelegramProcesses, "outbound": "proxy"}) + rules = append(rules, map[string]any{"process_path_regex": effectivePolicy.TelegramProcessRegex, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.TelegramDomains, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_regex": effectivePolicy.TelegramDomainRegex, "outbound": "proxy"}) + rules = append(rules, map[string]any{"ip_cidr": effectivePolicy.TelegramIPs, "outbound": "proxy"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.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) + } + + if len(effectivePolicy.PreferDirectProcesses) > 0 { + rules = append(rules, map[string]any{"process_name": effectivePolicy.PreferDirectProcesses, "outbound": "direct"}) + } + + var ruleSetDefs []map[string]any + for _, rs := range ruleSets { + if rs.URL == "" { + continue + } + ruleSetDefs = append(ruleSetDefs, map[string]any{ + "tag": rs.Tag, "type": "local", "format": rs.Format, + "path": rs.LocalPath, + }) + } + + 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", "type": "https", "server": "8.8.8.8", "detour": "proxy"}, + {"tag": "direct-dns", "type": "https", "server": "1.1.1.1"}, + }, + "rules": []map[string]any{ + {"outbound": "proxy", "server": "proxy-dns"}, + {"outbound": "direct", "server": "direct-dns"}, + }, + "strategy": "ipv4_only", + }, + Inbounds: []map[string]any{ + { + "type": "tun", + "tag": "tun-in", + "interface_name": TunInterfaceName, + "address": []string{"172.19.0.1/30"}, + "auto_route": true, + "strict_route": false, + "stack": "gvisor", + }, + { + "type": "socks", + "tag": "socks-in", + "listen": LocalProxyHost, + "listen_port": defaultInt(localProxyPort, LocalProxyPort), + }, + }, + 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 BuildSplitRoutingConfig(vlessServer models.Server, hy2Server models.Server, mode Mode, ruleSets []models.RuleSet, serverIPs []string, customBypass []string, localProxyPort int, policy *models.RoutingPolicy) SingBoxConfig { + effectivePolicy := EffectiveRoutingPolicy(policy) + bypassIPs := BuildBypassIPs(effectivePolicy, serverIPs) + bypassProcs := BuildBypassProcesses(effectivePolicy, customBypass) + + var rules []map[string]any + rules = append(rules, map[string]any{"action": "sniff"}) + rules = append(rules, map[string]any{"protocol": "dns", "action": "hijack-dns"}) + rules = append(rules, map[string]any{"ip_is_private": true, "outbound": "direct"}) + rules = append(rules, map[string]any{"ip_cidr": effectivePolicy.ReservedCIDRs, "outbound": "direct"}) + 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": effectivePolicy.WindowsNCSIDomains, "outbound": "direct"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.LocalDomainSuffixes, "outbound": "direct"}) + rules = append(rules, map[string]any{"domain_suffix": effectivePolicy.InfraBypassDomains, "outbound": "direct"}) + rules = appendSplitProxyRule(rules, map[string]any{"process_path_regex": effectivePolicy.LovenseProcessRegex}) + rules = appendSplitProxyRule(rules, map[string]any{"ip_cidr": effectivePolicy.ForcedProxyIPs}) + rules = appendSplitProxyRule(rules, map[string]any{"process_name": effectivePolicy.TelegramProcesses}) + rules = appendSplitProxyRule(rules, map[string]any{"process_path_regex": effectivePolicy.TelegramProcessRegex}) + rules = appendSplitProxyRule(rules, map[string]any{"domain_suffix": effectivePolicy.TelegramDomains}) + rules = appendSplitProxyRule(rules, map[string]any{"domain_regex": effectivePolicy.TelegramDomainRegex}) + rules = appendSplitProxyRule(rules, map[string]any{"ip_cidr": effectivePolicy.TelegramIPs}) + rules = appendSplitProxyRule(rules, map[string]any{"domain_suffix": effectivePolicy.BlockedDomains}) + + for _, r := range mode.Rules { + rule := map[string]any{} + 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 + } + if r.Outbound == "proxy" { + rules = appendSplitProxyRule(rules, rule) + } else { + rule["outbound"] = r.Outbound + rules = append(rules, rule) + } + } + + if len(effectivePolicy.PreferDirectProcesses) > 0 { + rules = append(rules, map[string]any{"process_name": effectivePolicy.PreferDirectProcesses, "outbound": "direct"}) + } + + if mode.Final == "proxy" { + rules = append(rules, + map[string]any{"network": []string{"udp"}, "outbound": "hysteria2-out"}, + map[string]any{"network": []string{"tcp"}, "outbound": "vless-out"}, + ) + } + + 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": splitFinalOutbound(mode.Final), + "rules": rules, + "default_domain_resolver": map[string]any{ + "server": "direct-dns", + "strategy": "ipv4_only", + }, + } + if len(ruleSetDefs) > 0 { + route["rule_set"] = ruleSetDefs + } + + return SingBoxConfig{ + DNS: map[string]any{ + "servers": []map[string]any{ + {"tag": "proxy-dns", "type": "https", "server": "8.8.8.8", "detour": "vless-out"}, + {"tag": "direct-dns", "type": "udp", "server": "1.1.1.1", "server_port": 53}, + }, + "rules": []map[string]any{ + {"outbound": "vless-out", "server": "proxy-dns"}, + {"outbound": "hysteria2-out", "server": "proxy-dns"}, + {"outbound": "direct", "server": "direct-dns"}, + }, + "strategy": "ipv4_only", + }, + Inbounds: []map[string]any{ + { + "type": "tun", + "tag": "tun-in", + "interface_name": TunInterfaceName, + "address": []string{"172.19.0.1/30"}, + "auto_route": true, + "strict_route": false, + "stack": "gvisor", + }, + { + "type": "socks", + "tag": "socks-in", + "listen": LocalProxyHost, + "listen_port": defaultInt(localProxyPort, LocalProxyPort), + }, + }, + Outbounds: []map[string]any{ + BuildOutboundWithTag(vlessServer, "vless-out"), + BuildOutboundWithTag(hy2Server, "hysteria2-out"), + {"type": "direct", "tag": "direct"}, + }, + Route: route, + Experimental: map[string]any{ + "cache_file": map[string]any{ + "enabled": true, + "path": "cache.db", + }, + }, + } +} + +func findCompanionProtocol(server models.Server, protocolType string) (models.Server, bool) { + for _, companion := range server.Companions { + if companion.Type == protocolType { + return companion, true + } + } + return models.Server{}, false +} + +func splitFinalOutbound(final string) string { + if final == "proxy" { + return "vless-out" + } + return final +} + +func appendSplitProxyRule(rules []map[string]any, base map[string]any) []map[string]any { + if rule, ok := splitRuleForNetwork(base, "tcp", "vless-out"); ok { + rules = append(rules, rule) + } + if rule, ok := splitRuleForNetwork(base, "udp", "hysteria2-out"); ok { + rules = append(rules, rule) + } + return rules +} + +func splitRuleForNetwork(base map[string]any, network string, outbound string) (map[string]any, bool) { + rule := copyRule(base) + if networks, ok := rule["network"].([]string); ok && len(networks) > 0 { + if !containsString(networks, network) { + return nil, false + } + rule["network"] = []string{network} + } else { + rule["network"] = []string{network} + } + rule["outbound"] = outbound + return rule, true +} + +func copyRule(in map[string]any) map[string]any { + out := make(map[string]any, len(in)+1) + for k, v := range in { + out[k] = v + } + return out +} + +func containsString(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} + +func defaultInt(value, fallback int) int { + if value > 0 { + return value + } + return fallback +} |
