package config import ( "bytes" "encoding/json" "fmt" "net" "net/netip" "net/url" "strings" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" dns "github.com/sagernet/sing-dns" ) const ( DNSRemoteTag = "dns-remote" DNSLocalTag = "dns-local" DNSDirectTag = "dns-direct" DNSBlockTag = "dns-block" DNSFakeTag = "dns-fake" DNSTricksDirectTag = "dns-trick-direct" OutboundDirectTag = "direct" OutboundBypassTag = "bypass" OutboundBlockTag = "block" OutboundSelectTag = "select" OutboundURLTestTag = "auto" OutboundDNSTag = "dns-out" OutboundDirectFragmentTag = "direct-fragment" InboundTUNTag = "tun-in" InboundMixedTag = "mixed-in" InboundDNSTag = "dns-in" ) var OutboundMainProxyTag = OutboundSelectTag func BuildConfigJson(configOpt ConfigOptions, input option.Options) (string, error) { options, err := BuildConfig(configOpt, input) if err != nil { return "", err } var buffer bytes.Buffer json.NewEncoder(&buffer) encoder := json.NewEncoder(&buffer) encoder.SetIndent("", " ") err = encoder.Encode(options) if err != nil { return "", err } return buffer.String(), nil } // TODO include selectors func BuildConfig(opt ConfigOptions, input option.Options) (*option.Options, error) { fmt.Printf("config options: %+v\n", opt) var options option.Options directDNSDomains := make(map[string]bool) dnsRules := []option.DefaultDNSRule{} var bind string if opt.AllowConnectionFromLAN { bind = "0.0.0.0" } else { bind = "127.0.0.1" } if opt.EnableClashApi { options.Experimental = &option.ExperimentalOptions{ ClashAPI: &option.ClashAPIOptions{ ExternalController: fmt.Sprintf("%s:%d", "127.0.0.1", opt.ClashApiPort), }, CacheFile: &option.CacheFileOptions{ Enabled: true, Path: "clash.db", }, } } options.Log = &option.LogOptions{ Level: opt.LogLevel, Output: "box.log", Disabled: false, Timestamp: true, DisableColor: true, } options.DNS = &option.DNSOptions{ StaticIPs: map[string][]string{ "sky.rethinkdns.com": getIPs([]string{"zula.ir", "www.speedtest.net", "sky.rethinkdns.com"}), }, DNSClientOptions: option.DNSClientOptions{ IndependentCache: opt.IndependentDNSCache, }, Final: DNSRemoteTag, Servers: []option.DNSServerOptions{ { Tag: DNSRemoteTag, Address: opt.RemoteDnsAddress, AddressResolver: DNSDirectTag, Strategy: opt.RemoteDnsDomainStrategy, }, { Tag: DNSTricksDirectTag, Address: "https://sky.rethinkdns.com/", // AddressResolver: "dns-local", Strategy: opt.DirectDnsDomainStrategy, Detour: OutboundDirectFragmentTag, }, { Tag: DNSDirectTag, Address: opt.DirectDnsAddress, AddressResolver: DNSLocalTag, Strategy: opt.DirectDnsDomainStrategy, Detour: OutboundDirectTag, }, { Tag: DNSLocalTag, Address: "local", Detour: OutboundDirectTag, }, { Tag: DNSBlockTag, Address: "rcode://success", }, }, } var inboundDomainStrategy option.DomainStrategy if !opt.ResolveDestination { inboundDomainStrategy = option.DomainStrategy(dns.DomainStrategyAsIS) } else { inboundDomainStrategy = opt.IPv6Mode } if opt.EnableTunService { ActivateTunnelService(opt) } else if opt.EnableTun { tunInbound := option.Inbound{ Type: C.TypeTun, Tag: InboundTUNTag, TunOptions: option.TunInboundOptions{ Stack: opt.TUNStack, MTU: opt.MTU, AutoRoute: true, StrictRoute: opt.StrictRoute, EndpointIndependentNat: true, // GSO: runtime.GOOS != "windows", InboundOptions: option.InboundOptions{ SniffEnabled: true, SniffOverrideDestination: true, DomainStrategy: inboundDomainStrategy, }, }, } switch opt.IPv6Mode { case option.DomainStrategy(dns.DomainStrategyUseIPv4): tunInbound.TunOptions.Inet4Address = []netip.Prefix{ netip.MustParsePrefix("172.19.0.1/28"), } case option.DomainStrategy(dns.DomainStrategyUseIPv6): tunInbound.TunOptions.Inet6Address = []netip.Prefix{ netip.MustParsePrefix("fdfe:dcba:9876::1/126"), } default: tunInbound.TunOptions.Inet4Address = []netip.Prefix{ netip.MustParsePrefix("172.19.0.1/28"), } tunInbound.TunOptions.Inet6Address = []netip.Prefix{ netip.MustParsePrefix("fdfe:dcba:9876::1/126"), } } options.Inbounds = append(options.Inbounds, tunInbound) } options.Inbounds = append( options.Inbounds, option.Inbound{ Type: C.TypeMixed, Tag: InboundMixedTag, MixedOptions: option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: option.NewListenAddress(netip.MustParseAddr(bind)), ListenPort: opt.MixedPort, InboundOptions: option.InboundOptions{ SniffEnabled: true, SniffOverrideDestination: true, DomainStrategy: inboundDomainStrategy, }, }, SetSystemProxy: opt.SetSystemProxy, }, }, ) options.Inbounds = append( options.Inbounds, option.Inbound{ Type: C.TypeDirect, Tag: InboundDNSTag, DirectOptions: option.DirectInboundOptions{ ListenOptions: option.ListenOptions{ Listen: option.NewListenAddress(netip.MustParseAddr(bind)), ListenPort: opt.LocalDnsPort, }, OverrideAddress: "1.1.1.1", OverridePort: 53, }, }, ) remoteDNSAddress := opt.RemoteDnsAddress if strings.Contains(remoteDNSAddress, "://") { remoteDNSAddress = strings.SplitAfter(remoteDNSAddress, "://")[1] } parsedUrl, err := url.Parse(fmt.Sprintf("https://%s", remoteDNSAddress)) if err == nil && net.ParseIP(parsedUrl.Host) == nil { directDNSDomains["full:"+parsedUrl.Host] = true //TODO: IS it really needed } routeRules := []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ Inbound: []string{InboundDNSTag}, Outbound: OutboundDNSTag, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ Port: []uint16{53}, Outbound: OutboundDNSTag, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ ClashMode: "Direct", Outbound: OutboundDirectTag, }, }, { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ ClashMode: "Global", Outbound: OutboundMainProxyTag, }, }, } if opt.BypassLAN { routeRules = append( routeRules, option.Rule{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ GeoIP: []string{"private"}, Outbound: OutboundBypassTag, }, }, ) } if opt.EnableFakeDNS { inet4Range := netip.MustParsePrefix("198.18.0.0/15") inet6Range := netip.MustParsePrefix("fc00::/18") options.DNS.FakeIP = &option.DNSFakeIPOptions{ Enabled: true, Inet4Range: &inet4Range, Inet6Range: &inet6Range, } options.DNS.Servers = append( options.DNS.Servers, option.DNSServerOptions{ Tag: DNSFakeTag, Address: "fakeip", Strategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), }, ) options.DNS.Rules = append( options.DNS.Rules, option.DNSRule{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ Inbound: []string{InboundTUNTag}, Server: DNSFakeTag, DisableCache: true, }, }, ) } for _, rule := range opt.Rules { routeRule := rule.MakeRule() switch rule.Outbound { case "bypass": routeRule.Outbound = OutboundBypassTag case "block": routeRule.Outbound = OutboundBlockTag case "proxy": routeRule.Outbound = OutboundDNSTag } if routeRule.IsValid() { routeRules = append( routeRules, option.Rule{ Type: C.RuleTypeDefault, DefaultOptions: routeRule, }, ) } dnsRule := rule.MakeDNSRule() switch rule.Outbound { case "bypass": dnsRule.Server = DNSDirectTag case "block": dnsRule.Server = DNSBlockTag dnsRule.DisableCache = true case "proxy": if opt.EnableFakeDNS { fakeDnsRule := dnsRule fakeDnsRule.Server = DNSFakeTag fakeDnsRule.Inbound = []string{InboundTUNTag} dnsRules = append(dnsRules, fakeDnsRule) } dnsRule.Server = DNSRemoteTag } dnsRules = append(dnsRules, dnsRule) } if opt.EnableDNSRouting { for _, dnsRule := range dnsRules { if dnsRule.IsValid() { options.DNS.Rules = append( options.DNS.Rules, option.DNSRule{ Type: C.RuleTypeDefault, DefaultOptions: dnsRule, }, ) } } } if options.DNS.Rules == nil { options.DNS.Rules = []option.DNSRule{} } var dnsCPttl uint32 = 3000 options.DNS.Rules = append( options.DNS.Rules, option.DNSRule{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultDNSRule{ Domain: []string{"cp.cloudflare.com"}, Server: DNSRemoteTag, RewriteTTL: &dnsCPttl, DisableCache: false, }, }, ) options.Route = &option.RouteOptions{ Rules: routeRules, AutoDetectInterface: true, OverrideAndroidVPN: true, // RuleSet: []option.RuleSet{ // { // Type: C.RuleSetTypeRemote, // Tag: "geoip-" + opt, // RemoteOptions: option.RemoteRuleSet{ // URL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs", // UpdateInterval: option.Duration(5 * time.day), // }, // }, // }, GeoIP: &option.GeoIPOptions{ Path: opt.GeoIPPath, }, Geosite: &option.GeositeOptions{ Path: opt.GeoSitePath, }, } var outbounds []option.Outbound var tags []string OutboundMainProxyTag = OutboundSelectTag //inbound==warp over proxies //outbound==proxies over warp fmt.Printf("opt.Warp=%+v\n", opt.Warp) if opt.Warp.EnableWarp && (opt.Warp.Mode == "warp_over_proxy" || opt.Warp.Mode == "proxy_over_warp") { out, err := generateWarpSingbox(opt.Warp.WireguardConfig.ToWireguardConfig(), opt.Warp.CleanIP, opt.Warp.CleanPort, opt.Warp.FakePackets, opt.Warp.FakePacketSize, opt.Warp.FakePacketDelay) if err != nil { return nil, fmt.Errorf("failed to generate warp config: %v", err) } out.Tag = "Hiddify Warp ✅" if opt.Warp.Mode == "warp_over_proxy" { out.WireGuardOptions.Detour = OutboundURLTestTag OutboundMainProxyTag = out.Tag } else { out.WireGuardOptions.Detour = OutboundDirectTag } patchWarp(out) outbounds = append(outbounds, *out) // tags = append(tags, out.Tag) } for _, out := range input.Outbounds { outbound, serverDomain, err := patchOutbound(out, opt) if err != nil { return nil, err } if serverDomain != "" { directDNSDomains[serverDomain] = true } out = *outbound switch out.Type { case C.TypeDirect, C.TypeBlock, C.TypeDNS: continue case C.TypeSelector, C.TypeURLTest: continue case C.TypeCustom: continue default: if !strings.Contains(out.Tag, "§hide§") { tags = append(tags, out.Tag) } out = patchHiddifyWarpFromConfig(out, opt) outbounds = append(outbounds, out) } } urlTest := option.Outbound{ Type: C.TypeURLTest, Tag: OutboundURLTestTag, URLTestOptions: option.URLTestOutboundOptions{ Outbounds: tags, URL: opt.ConnectionTestUrl, Interval: option.Duration(opt.URLTestInterval.Duration()), IdleTimeout: option.Duration(opt.URLTestIdleTimeout.Duration()), }, } selector := option.Outbound{ Type: C.TypeSelector, Tag: OutboundSelectTag, SelectorOptions: option.SelectorOutboundOptions{ Outbounds: append([]string{urlTest.Tag}, tags...), Default: urlTest.Tag, }, } outbounds = append([]option.Outbound{selector, urlTest}, outbounds...) options.Outbounds = append( outbounds, []option.Outbound{ { Tag: OutboundDNSTag, Type: C.TypeDNS, }, { Tag: OutboundDirectTag, Type: C.TypeDirect, }, { Tag: OutboundDirectFragmentTag, Type: C.TypeDirect, DirectOptions: option.DirectOutboundOptions{ DialerOptions: option.DialerOptions{ TLSFragment: &option.TLSFragmentOptions{ Enabled: true, Size: opt.TLSTricks.FragmentSize, Sleep: opt.TLSTricks.FragmentSleep, }, }, }, }, { Tag: OutboundBypassTag, Type: C.TypeDirect, }, { Tag: OutboundBlockTag, Type: C.TypeBlock, }, }..., ) if len(directDNSDomains) > 0 { // trickDnsDomains := []string{} // directDNSDomains = removeDuplicateStr(directDNSDomains) // b, _ := batch.New(context.Background(), batch.WithConcurrencyNum[bool](10)) // for _, d := range directDNSDomains { // b.Go(d, func() (bool, error) { // return isBlockedDomain(d), nil // }) // } // b.Wait() // for domain, isBlock := range b.Result() { // if isBlock.Value { // trickDnsDomains = append(trickDnsDomains, domain) // } // } // trickDomains := strings.Join(trickDnsDomains, ",") // trickRule := Rule{Domains: trickDomains, Outbound: OutboundBypassTag} // trickDnsRule := trickRule.MakeDNSRule() // trickDnsRule.Server = DNSTricksDirectTag // options.DNS.Rules = append([]option.DNSRule{{Type: C.RuleTypeDefault, DefaultOptions: trickDnsRule}}, options.DNS.Rules...) directDNSDomainskeys := make([]string, 0, len(directDNSDomains)) for key := range directDNSDomains { directDNSDomainskeys = append(directDNSDomainskeys, key) } domains := strings.Join(directDNSDomainskeys, ",") directRule := Rule{Domains: domains, Outbound: OutboundBypassTag} dnsRule := directRule.MakeDNSRule() dnsRule.Server = DNSDirectTag options.DNS.Rules = append([]option.DNSRule{{Type: C.RuleTypeDefault, DefaultOptions: dnsRule}}, options.DNS.Rules...) } options.Route.Final = OutboundMainProxyTag return &options, nil } func patchHiddifyWarpFromConfig(out option.Outbound, opt ConfigOptions) option.Outbound { if opt.Warp.EnableWarp && opt.Warp.Mode == "proxy_over_warp" { if out.DirectOptions.Detour == "" { out.DirectOptions.Detour = "Hiddify Warp ✅" } if out.HTTPOptions.Detour == "" { out.HTTPOptions.Detour = "Hiddify Warp ✅" } if out.Hysteria2Options.Detour == "" { out.Hysteria2Options.Detour = "Hiddify Warp ✅" } if out.HysteriaOptions.Detour == "" { out.HysteriaOptions.Detour = "Hiddify Warp ✅" } if out.SSHOptions.Detour == "" { out.SSHOptions.Detour = "Hiddify Warp ✅" } if out.ShadowTLSOptions.Detour == "" { out.ShadowTLSOptions.Detour = "Hiddify Warp ✅" } if out.ShadowsocksOptions.Detour == "" { out.ShadowsocksOptions.Detour = "Hiddify Warp ✅" } if out.ShadowsocksROptions.Detour == "" { out.ShadowsocksROptions.Detour = "Hiddify Warp ✅" } if out.SocksOptions.Detour == "" { out.SocksOptions.Detour = "Hiddify Warp ✅" } if out.TUICOptions.Detour == "" { out.TUICOptions.Detour = "Hiddify Warp ✅" } if out.TorOptions.Detour == "" { out.TorOptions.Detour = "Hiddify Warp ✅" } if out.TrojanOptions.Detour == "" { out.TrojanOptions.Detour = "Hiddify Warp ✅" } if out.VLESSOptions.Detour == "" { out.VLESSOptions.Detour = "Hiddify Warp ✅" } if out.VMessOptions.Detour == "" { out.VMessOptions.Detour = "Hiddify Warp ✅" } if out.WireGuardOptions.Detour == "" { out.WireGuardOptions.Detour = "Hiddify Warp ✅" } } return out } func getIPs(domains []string) []string { res := []string{} for _, d := range domains { ips, err := net.LookupHost(d) if err != nil { continue } for _, ip := range ips { if !strings.HasPrefix(ip, "10.") { res = append(res, ip) } } } return res } func isBlockedDomain(domain string) bool { if strings.HasPrefix("full:", domain) { return false } ips, err := net.LookupHost(domain) if err != nil { // fmt.Println(err) return true } // Print the IP addresses associated with the domain fmt.Printf("IP addresses for %s:\n", domain) for _, ip := range ips { if strings.HasPrefix(ip, "10.") { return true } } return false } func removeDuplicateStr(strSlice []string) []string { allKeys := make(map[string]bool) list := []string{} for _, item := range strSlice { if _, value := allKeys[item]; !value { allKeys[item] = true list = append(list, item) } } return list }