diff --git a/shared/config.go b/shared/config.go index 391a056..fc5a434 100644 --- a/shared/config.go +++ b/shared/config.go @@ -1,8 +1,12 @@ package shared import ( + "encoding/json" "fmt" + "net" "net/netip" + "net/url" + "strings" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" @@ -28,29 +32,36 @@ type ConfigOptions struct { ClashApiPort uint16 `json:"clash-api-port"` EnableTun bool `json:"enable-tun"` SetSystemProxy bool `json:"set-system-proxy"` + BypassLAN bool `json:"bypass-lan"` + Rules []Rule `json:"rules"` } +// TODO add fake dns +// TODO add bypass outbound +// TODO include selectors func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { if configOpt.ExecuteAsIs { return applyOverrides(configOpt, input) } - var options option.Options + fmt.Printf("config options: %+v\n", configOpt) - fmt.Printf("%+v\n", configOpt) + var options option.Options + directDNSDomains := []string{} if configOpt.EnableClashApi { options.Experimental = &option.ExperimentalOptions{ ClashAPI: &option.ClashAPIOptions{ ExternalController: fmt.Sprintf("%s:%d", "127.0.0.1", configOpt.ClashApiPort), StoreSelected: true, + CacheFile: "clash.db", }, } } options.Log = &option.LogOptions{ Level: configOpt.LogLevel, - Output: "box.log", + Output: "./logs/box.log", Disabled: false, Timestamp: false, DisableColor: true, @@ -66,7 +77,6 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { Address: configOpt.RemoteDnsAddress, AddressResolver: "dns-direct", Strategy: configOpt.RemoteDnsDomainStrategy, - Detour: "select", }, { Tag: "dns-direct", @@ -85,37 +95,6 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { Address: "rcode://success", }, }, - Rules: []option.DNSRule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultDNSRule{ - Outbound: []string{"any"}, - // Server: "dns-direct", - Server: "dns-local", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultDNSRule{ - ClashMode: "Direct", - Server: "dns-local", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultDNSRule{ - ClashMode: "Global", - Server: "dns-remote", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultDNSRule{ - DomainSuffix: []string{"ir"}, - Server: "dns-local", - }, - }, - }, } var inboundDomainStrategy option.DomainStrategy @@ -182,7 +161,8 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { }, ) - options.Inbounds = append(options.Inbounds, + options.Inbounds = append( + options.Inbounds, option.Inbound{ Type: C.TypeDirect, Tag: "dns-in", @@ -197,59 +177,104 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { }, ) - options.Route = &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Inbound: []string{"dns-in"}, - Outbound: "dns-out", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Port: []uint16{53}, - Outbound: "dns-out", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Protocol: []string{"dns"}, - Outbound: "dns-out", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - ClashMode: "Direct", - Outbound: "direct", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - ClashMode: "Global", - Outbound: "select", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Geosite: []string{"category-ads-all"}, - Outbound: "block", - }, - }, - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - GeoIP: []string{"ir", "private"}, - DomainSuffix: []string{"ir"}, - Outbound: "direct", - }, + remoteDNSAddress := configOpt.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)) + } + + routeRules := []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + Inbound: []string{"dns-in"}, + Outbound: "dns-out", }, }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + Port: []uint16{53}, + Outbound: "dns-out", + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + ClashMode: "Direct", + Outbound: "direct", + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + ClashMode: "Global", + Outbound: "select", + }, + }, + } + + if configOpt.BypassLAN { + routeRules = append( + routeRules, + option.Rule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + GeoIP: []string{"private"}, + Outbound: "direct", + }, + }, + ) + } + + for _, rule := range configOpt.Rules { + routeRule := rule.MakeRule() + switch rule.Outbound { + case "bypass": + routeRule.Outbound = "direct" + case "block": + routeRule.Outbound = "block" + case "proxy": + routeRule.Outbound = "dns-out" + } + + if routeRule.IsValid() { + routeRules = append( + routeRules, + option.Rule{ + Type: C.RuleTypeDefault, + DefaultOptions: routeRule, + }, + ) + } + + dnsRule := rule.MakeDNSRule() + switch rule.Outbound { + case "bypass": + dnsRule.Server = "dns-direct" + case "block": + dnsRule.Server = "dns-block" + dnsRule.DisableCache = true + case "proxy": + dnsRule.Server = "dns-remote" + } + + if dnsRule.IsValid() { + options.DNS.Rules = append( + options.DNS.Rules, + option.DNSRule{ + Type: C.RuleTypeDefault, + DefaultOptions: dnsRule, + }, + ) + } + } + + options.Route = &option.RouteOptions{ + Rules: routeRules, AutoDetectInterface: true, OverrideAndroidVPN: true, } @@ -257,6 +282,20 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { var outbounds []option.Outbound var tags []string for _, out := range input.Outbounds { + jsonData, err := out.MarshalJSON() + if err == nil { + var obj map[string]interface{} + err = json.Unmarshal(jsonData, &obj) + if err == nil { + if value, ok := obj["server"]; ok { + server := value.(string) + if server != "" && net.ParseIP(server) == nil { + directDNSDomains = append(directDNSDomains, fmt.Sprintf("full:%s", server)) + } + } + } + } + switch out.Type { case C.TypeDirect, C.TypeBlock, C.TypeDNS: continue @@ -307,6 +346,14 @@ func BuildConfig(configOpt ConfigOptions, input option.Options) option.Options { }..., ) + if len(directDNSDomains) > 0 { + domains := strings.Join(removeDuplicateStr(directDNSDomains), ",") + directRule := Rule{Domains: domains, Outbound: "bypass"} + dnsRule := directRule.MakeDNSRule() + dnsRule.Server = "dns-direct" + options.DNS.Rules = append([]option.DNSRule{{Type: C.RuleTypeDefault, DefaultOptions: dnsRule}}, options.DNS.Rules...) + } + return options } @@ -335,3 +382,15 @@ func applyOverrides(overrides ConfigOptions, options option.Options) option.Opti return options } + +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 +} diff --git a/shared/rules.go b/shared/rules.go new file mode 100644 index 0000000..19672eb --- /dev/null +++ b/shared/rules.go @@ -0,0 +1,95 @@ +package shared + +import ( + "strconv" + "strings" + + "github.com/sagernet/sing-box/option" +) + +type Rule struct { + Domains string `json:"domains"` + IP string `json:"ip"` + Port string `json:"port"` + Network string `json:"network"` + Protocol string `json:"protocol"` + Outbound string `json:"outbound"` +} + +func (r *Rule) MakeRule() option.DefaultRule { + rule := option.DefaultRule{} + if len(r.Domains) > 0 { + rule = makeDomainRule(rule, strings.Split(r.Domains, ",")) + } + if len(r.IP) > 0 { + rule = makeIpRule(rule, strings.Split(r.IP, ",")) + } + if len(r.Port) > 0 { + rule = makePortRule(rule, strings.Split(r.Port, ",")) + } + if len(r.Network) > 0 { + rule.Network = append(rule.Network, r.Network) + } + if len(r.Protocol) > 0 { + rule.Protocol = append(rule.Protocol, strings.Split(r.Protocol, ",")...) + } + return rule +} + +func (r *Rule) MakeDNSRule() option.DefaultDNSRule { + rule := option.DefaultDNSRule{} + domains := strings.Split(r.Domains, ",") + for _, item := range domains { + if strings.HasPrefix(item, "geosite:") { + rule.Geosite = append(rule.Geosite, strings.TrimPrefix(item, "geosite:")) + } else if strings.HasPrefix(item, "full:") { + rule.Domain = append(rule.Domain, strings.ToLower(strings.TrimPrefix(item, "full:"))) + } else if strings.HasPrefix(item, "domain:") { + rule.DomainSuffix = append(rule.DomainSuffix, strings.ToLower(strings.TrimPrefix(item, "domain:"))) + } else if strings.HasPrefix(item, "regexp:") { + rule.DomainRegex = append(rule.DomainRegex, strings.ToLower(strings.TrimPrefix(item, "regexp:"))) + } else if strings.HasPrefix(item, "keyword:") { + rule.DomainKeyword = append(rule.DomainKeyword, strings.ToLower(strings.TrimPrefix(item, "keyword:"))) + } + } + return rule +} + +func makeDomainRule(options option.DefaultRule, list []string) option.DefaultRule { + for _, item := range list { + if strings.HasPrefix(item, "geosite:") { + options.Geosite = append(options.Geosite, strings.TrimPrefix(item, "geosite:")) + } else if strings.HasPrefix(item, "full:") { + options.Domain = append(options.Domain, strings.ToLower(strings.TrimPrefix(item, "full:"))) + } else if strings.HasPrefix(item, "domain:") { + options.DomainSuffix = append(options.DomainSuffix, strings.ToLower(strings.TrimPrefix(item, "domain:"))) + } else if strings.HasPrefix(item, "regexp:") { + options.DomainRegex = append(options.DomainRegex, strings.ToLower(strings.TrimPrefix(item, "regexp:"))) + } else if strings.HasPrefix(item, "keyword:") { + options.DomainKeyword = append(options.DomainKeyword, strings.ToLower(strings.TrimPrefix(item, "keyword:"))) + } + } + return options +} + +func makeIpRule(options option.DefaultRule, list []string) option.DefaultRule { + for _, item := range list { + if strings.HasPrefix(item, "geoip:") { + options.GeoIP = append(options.GeoIP, strings.TrimPrefix(item, "geoip:")) + } else { + options.IPCIDR = append(options.IPCIDR, item) + } + } + return options +} + +func makePortRule(options option.DefaultRule, list []string) option.DefaultRule { + for _, item := range list { + if strings.Contains(item, ":") { + options.PortRange = append(options.PortRange, item) + } else if i, err := strconv.Atoi(item); err == nil { + options.Port = append(options.Port, uint16(i)) + } + } + return options +}