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