diff --git a/custom/custom.go b/custom/custom.go index 5d0cd9a..424259c 100644 --- a/custom/custom.go +++ b/custom/custom.go @@ -37,8 +37,15 @@ func create(configPath *C.char) *C.char { if err != nil { return C.CString(err.Error()) } - overrides := shared.ConfigOverrides{ExcludeTunInbound: true, IncludeMixedInbound: true, IncludeLogOutput: true, LogLevel: "info", IncludeLogTimestamp: false, ClashApiPort: 9090} - options = shared.ApplyOverrides(options, overrides) + overrides := shared.ConfigOverrides{ + LogOutput: shared.StringAddr("box.log"), + EnableTun: shared.BoolAddr(false), + SetSystemProxy: shared.BoolAddr(true), + } + template := shared.DefaultTemplate(overrides) + options = shared.ApplyOverrides(template, options, overrides) + + shared.SaveCurrentConfig(sWorkingPath, options) instance, err := NewService(options) if err != nil { diff --git a/go.mod b/go.mod index 4cde1d7..408bedd 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59 github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a github.com/sagernet/sing-box v1.3.6 + github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 github.com/xmdhs/clash2singbox v0.0.0-20230810082059-5054938e1bfd gopkg.in/yaml.v3 v3.0.1 ) @@ -54,7 +55,6 @@ require ( github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111 // indirect github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect - github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 // indirect github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21 // indirect github.com/sagernet/sing-shadowsocks v0.2.4 // indirect github.com/sagernet/sing-shadowsocks2 v0.1.3 // indirect diff --git a/mobile/mobile.go b/mobile/mobile.go index 1cc0542..63b18a7 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -1,3 +1,34 @@ +// package mobile + +// import ( +// "encoding/json" +// "os" + +// "github.com/hiddify/libcore/shared" +// _ "github.com/sagernet/gomobile/event/key" +// "github.com/sagernet/sing-box/option" +// ) + +// func Parse(path string) error { +// return shared.ParseConfig(path) +// } + +// func ApplyOverrides(path string) (string, error) { +// fileContent, err := os.ReadFile(path) +// if err != nil { +// return "", err +// } +// var options option.Options +// err = options.UnmarshalJSON(fileContent) +// if err != nil { +// return "", err +// } +// overrides := shared.ConfigOverrides{ExcludeTunInbound: false, IncludeMixedInbound: false, IncludeLogOutput: false, LogLevel: "", IncludeLogTimestamp: false, ClashApiPort: 9090} +// options = shared.ApplyOverrides(options, overrides) +// config, err := json.Marshal(options) +// return string(config), err +// } + package mobile import ( @@ -23,8 +54,13 @@ func ApplyOverrides(path string) (string, error) { if err != nil { return "", err } - overrides := shared.ConfigOverrides{ExcludeTunInbound: false, IncludeMixedInbound: false, IncludeLogOutput: false, LogLevel: "", IncludeLogTimestamp: false, ClashApiPort: 9090} - options = shared.ApplyOverrides(options, overrides) + overrides := shared.ConfigOverrides{ + EnableTun: shared.BoolAddr(true), + SetSystemProxy: shared.BoolAddr(false), + LogOutput: shared.StringAddr(""), + } + template := shared.DefaultTemplate(overrides) + options = shared.ApplyOverrides(template, options, overrides) config, err := json.Marshal(options) return string(config), err } diff --git a/shared/config.json.template b/shared/config.json.template index 3c4bdf4..06c91ad 100644 --- a/shared/config.json.template +++ b/shared/config.json.template @@ -1,127 +1,8 @@ { "log": {}, - "dns": { - "servers": [ - { - "tag": "remote", - "address_resolver": "local", - "address": "tcp://1.1.1.1", - "strategy": "prefer_ipv4", - "detour": "select" - }, - { - "tag": "local", - "address": "local", - "detour": "direct" - } - ], - "rules": [ - { - "clash_mode": "global", - "server": "remote" - }, - { - "clash_mode": "direct", - "server": "local" - }, - { - "outbound": [ - "any" - ], - "server": "local" - }, - { - "geosite": "ir", - "server": "local" - } - ], - "strategy": "ipv4_only" - }, - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "sniff": true, - "sniff_override_destination": true, - "domain_strategy": "ipv4_only", - "strict_route": true, - "mtu": 9000, - "endpoint_independent_nat": true, - "auto_route": true - }, - { - "type": "socks", - "tag": "socks-in", - "listen": "127.0.0.1", - "sniff": true, - "sniff_override_destination": true, - "domain_strategy": "ipv4_only", - "listen_port": 2333, - "users": [] - }, - { - "type": "mixed", - "tag": "mixed-in", - "sniff": true, - "sniff_override_destination": true, - "domain_strategy": "ipv4_only", - "listen": "127.0.0.1", - "listen_port": 2334, - "set_system_proxy": true, - "users": [] - } - ], - "outbounds": [ - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "geosite": "category-ads-all", - "outbound": "block" - }, - { - "protocol": "dns", - "outbound": "dns-out" - }, - { - "clash_mode": "direct", - "outbound": "direct" - }, - { - "clash_mode": "global", - "outbound": "select" - }, - { - "geoip": [ - "ir", - "private" - ], - "outbound": "direct" - }, - { - "geosite": "ir", - "outbound": "direct" - } - ], - "auto_detect_interface": true - }, - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "store_selected": true, - "secret": "" - } - } + "dns": {}, + "inbounds": [], + "outbounds": [], + "route": {}, + "experimental": {} } \ No newline at end of file diff --git a/shared/debug.go b/shared/debug.go new file mode 100644 index 0000000..683a738 --- /dev/null +++ b/shared/debug.go @@ -0,0 +1,22 @@ +package shared + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/option" +) + +func SaveCurrentConfig(path string, options option.Options) error { + var buffer bytes.Buffer + json.NewEncoder(&buffer) + encoder := json.NewEncoder(&buffer) + encoder.SetIndent("", " ") + err := encoder.Encode(options) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(path, "current-config.json"), buffer.Bytes(), 0777) +} diff --git a/shared/override.go b/shared/override.go index ea2cfb1..3474cd3 100644 --- a/shared/override.go +++ b/shared/override.go @@ -8,91 +8,95 @@ import ( ) type ConfigOverrides struct { - ExcludeTunInbound bool - IncludeMixedInbound bool - IncludeLogOutput bool - IncludeLogTimestamp bool - LogLevel string - ClashApiPort int + ClashApiPort *int `json:"clash-api-port"` + EnableTun *bool `json:"enable-tun"` + SetSystemProxy *bool `json:"set-system-proxy"` + LogLevel *string `json:"log-level"` + LogOutput *string `json:"log-output"` + DNSRemote *string `json:"dns-remote"` + MixedPort *int `json:"mixed-port"` } -func ApplyOverrides(options option.Options, overrides ConfigOverrides) option.Options { - options.Log = &option.LogOptions{ - Disabled: false, - Timestamp: overrides.IncludeLogTimestamp, - DisableColor: true, - } - if overrides.LogLevel != "" { - options.Log.Level = overrides.LogLevel - } - if overrides.IncludeLogOutput { - options.Log.Output = "box.log" +func ApplyOverrides(base option.Options, options option.Options, overrides ConfigOverrides) option.Options { + clashApiPort := pointerOrDefaultInt(overrides.ClashApiPort, 9090) + base.Experimental = &option.ExperimentalOptions{ + ClashAPI: &option.ClashAPIOptions{ + ExternalController: fmt.Sprintf("%s:%d", "127.0.0.1", clashApiPort), + StoreSelected: true, + }, } - options.Experimental.ClashAPI = &option.ClashAPIOptions{ - ExternalController: fmt.Sprintf("%s:%d", "127.0.0.1", overrides.ClashApiPort), - StoreSelected: true, - Secret: "", + base.Log = &option.LogOptions{ + Level: pointerOrDefaultString(overrides.LogLevel, "info"), + Output: pointerOrDefaultString(overrides.LogOutput, ""), + Disabled: false, + Timestamp: false, + DisableColor: true, } var inbounds []option.Inbound - for _, inb := range options.Inbounds { - if overrides.ExcludeTunInbound && inb.Type == C.TypeTun { - continue - } - if overrides.IncludeMixedInbound && inb.Type == C.TypeMixed { - inb.MixedOptions.SetSystemProxy = true + for _, inb := range base.Inbounds { + switch inb.Type { + case C.TypeTun: + if pointerOrDefaultBool(overrides.EnableTun, true) { + inbounds = append(inbounds, inb) + } + default: inbounds = append(inbounds, inb) - continue } - inbounds = append(inbounds, inb) } - options.Inbounds = inbounds + base.Inbounds = inbounds - hasSelector := false - hasUrlTest := false - var selectable []option.Outbound - var urlTests []option.Outbound + var outbounds []option.Outbound + var tags []string for _, out := range options.Outbounds { - if out.Type == C.TypeSelector { - hasSelector = true - } else if out.Type == C.TypeURLTest { - hasUrlTest = true - urlTests = append(urlTests, out) - } switch out.Type { case C.TypeDirect, C.TypeBlock, C.TypeDNS: continue + case C.TypeSelector, C.TypeURLTest: + continue + default: + tags = append(tags, out.Tag) } - selectable = append(selectable, out) - } - var generatedUrlTest *option.Outbound - if !hasUrlTest { - var urlSelectOuts []string - for _, out := range selectable { - urlSelectOuts = append(urlSelectOuts, out.Tag) - } - generatedUrlTest = &option.Outbound{Type: C.TypeURLTest, Tag: "urltest", URLTestOptions: option.URLTestOutboundOptions{Outbounds: urlSelectOuts}} - urlTests = append(urlTests, *generatedUrlTest) - } - if !hasSelector { - var selectorOuts []string - for _, out := range selectable { - selectorOuts = append(selectorOuts, out.Tag) - } - for _, out := range urlTests { - selectorOuts = append(selectorOuts, out.Tag) - } - - defaultSelector := option.Outbound{Type: C.TypeSelector, Tag: "select", SelectorOptions: option.SelectorOutboundOptions{Outbounds: selectorOuts}} - if generatedUrlTest != nil { - defaultSelector.SelectorOptions.Default = generatedUrlTest.Tag - } - options.Outbounds = append([]option.Outbound{defaultSelector}, options.Outbounds...) - } - if generatedUrlTest != nil { - options.Outbounds = append(options.Outbounds, *generatedUrlTest) + outbounds = append(outbounds, out) } - return options + urlTest := option.Outbound{ + Type: C.TypeURLTest, + Tag: "auto", + URLTestOptions: option.URLTestOutboundOptions{ + Outbounds: tags, + }, + } + + 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...) + + base.Outbounds = append( + outbounds, + []option.Outbound{ + { + Tag: "direct", + Type: C.TypeDirect, + }, + { + Tag: "block", + Type: C.TypeBlock, + }, + { + Tag: "dns-out", + Type: C.TypeDNS, + }, + }..., + ) + + return base } diff --git a/shared/template.go b/shared/template.go new file mode 100644 index 0000000..08f3c9c --- /dev/null +++ b/shared/template.go @@ -0,0 +1,168 @@ +package shared + +import ( + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + dns "github.com/sagernet/sing-dns" +) + +func DefaultTemplate(overrides ConfigOverrides) option.Options { + var options option.Options + + options.Experimental = &option.ExperimentalOptions{ + ClashAPI: &option.ClashAPIOptions{ + ExternalController: "127.0.0.1:9090", + StoreSelected: true, + }, + } + + options.Log = &option.LogOptions{ + Level: "warn", + Disabled: false, + Timestamp: false, + DisableColor: true, + } + + options.DNS = &option.DNSOptions{ + DNSClientOptions: option.DNSClientOptions{ + Strategy: option.DomainStrategy(dns.DomainStrategyPreferIPv4), + IndependentCache: true, + }, + Servers: []option.DNSServerOptions{ + { + Tag: "local", + Address: "local", + Detour: "direct", + }, + { + Tag: "dns-remote", + Address: pointerOrDefaultString(overrides.DNSRemote, "tcp://1.1.1.1"), + AddressResolver: "local", + Strategy: option.DomainStrategy(dns.DomainStrategyPreferIPv4), + Detour: "select", + }, + }, + Rules: []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + ClashMode: "direct", + Server: "local", + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + ClashMode: "global", + Server: "dns-remote", + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + DomainSuffix: []string{"ir"}, + Server: "local", + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + Outbound: []string{"any"}, + Server: "local", + }, + }, + }, + ReverseMapping: true, + Final: "dns-remote", + } + + if pointerOrDefaultBool(overrides.EnableTun, true) { + options.Inbounds = append( + options.Inbounds, + option.Inbound{ + Type: C.TypeTun, + Tag: "tun-in", + TunOptions: option.TunInboundOptions{ + Inet4Address: []option.ListenPrefix{ + option.ListenPrefix(netip.MustParsePrefix("172.19.0.1/30")), + }, + MTU: 9000, + AutoRoute: true, + StrictRoute: true, + EndpointIndependentNat: true, + InboundOptions: option.InboundOptions{ + SniffEnabled: true, + SniffOverrideDestination: true, + DomainStrategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), + }, + }, + }, + ) + } + options.Inbounds = append( + options.Inbounds, + option.Inbound{ + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.MustParseAddr("127.0.0.1")), + ListenPort: uint16(pointerOrDefaultInt(overrides.MixedPort, 2334)), + InboundOptions: option.InboundOptions{ + SniffEnabled: true, + SniffOverrideDestination: true, + DomainStrategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), + }, + }, + SetSystemProxy: pointerOrDefaultBool(overrides.SetSystemProxy, true), + }, + }, + ) + + options.Route = &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + Geosite: []string{"category-ads-all"}, + Outbound: "block", + }, + }, + { + 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{ + GeoIP: []string{"ir", "private"}, + DomainSuffix: []string{"ir"}, + Outbound: "direct", + }, + }, + }, + AutoDetectInterface: true, + OverrideAndroidVPN: true, + } + + return options +} diff --git a/shared/utils.go b/shared/utils.go new file mode 100644 index 0000000..aac986e --- /dev/null +++ b/shared/utils.go @@ -0,0 +1,35 @@ +package shared + +func StringAddr(b string) *string { + stringVar := b + return &stringVar +} + +func BoolAddr(b bool) *bool { + boolVar := b + return &boolVar +} + +func pointerOrDefaultString(p *string, def string) string { + if p != nil { + return *p + } + + return def +} + +func pointerOrDefaultInt(p *int, def int) int { + if p != nil { + return *p + } + + return def +} + +func pointerOrDefaultBool(p *bool, def bool) bool { + if p != nil { + return *p + } + + return def +}