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" ) 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 := []string{} 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.EnableTun { if ok, _ := ActivateTunnelService(opt); !ok { tunInbound := option.Inbound{ Type: C.TypeTun, Tag: InboundTUNTag, TunOptions: option.TunInboundOptions{ Stack: opt.TUNStack, MTU: opt.MTU, AutoRoute: true, StrictRoute: opt.StrictRoute, EndpointIndependentNat: true, 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 = append(directDNSDomains, fmt.Sprintf("full:%s", parsedUrl.Host)) //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: OutboundSelectTag, }, }, } 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, GeoIP: &option.GeoIPOptions{ Path: opt.GeoIPPath, }, Geosite: &option.GeositeOptions{ Path: opt.GeoSitePath, }, } var outbounds []option.Outbound var tags []string for _, out := range input.Outbounds { outbound, serverDomain, err := patchOutbound(out, opt) if err != nil { return nil, err } if serverDomain != "" { directDNSDomains = append(directDNSDomains, serverDomain) } out = *outbound switch out.Type { case C.TypeDirect, C.TypeBlock, C.TypeDNS: continue case C.TypeSelector, C.TypeURLTest: continue case C.TypeCustom: continue default: tags = append(tags, out.Tag) outbounds = append(outbounds, out) } } urlTest := option.Outbound{ Type: C.TypeURLTest, Tag: OutboundURLTestTag, URLTestOptions: option.URLTestOutboundOptions{ Outbounds: tags, URL: opt.ConnectionTestUrl, Interval: opt.URLTestInterval, IdleTimeout: opt.URLTestIdleTimeout, }, } selector := option.Outbound{ Type: C.TypeSelector, Tag: "select", 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) for _, d := range directDNSDomains { if isBlockedDomain(d) { trickDnsDomains = append(trickDnsDomains, d) } } 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...) domains := strings.Join(directDNSDomains, ",") 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...) } return &options, nil } 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 }