Refactor shared
This commit is contained in:
465
config/config.go
Normal file
465
config/config.go
Normal file
@@ -0,0 +1,465 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func BuildConfigJson(configOpt ConfigOptions, input option.Options) (string, error) {
|
||||
options := BuildConfig(configOpt, input)
|
||||
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(configOpt ConfigOptions, input option.Options) option.Options {
|
||||
if configOpt.ExecuteAsIs {
|
||||
return applyOverrides(configOpt, input)
|
||||
}
|
||||
|
||||
fmt.Printf("config options: %+v\n", configOpt)
|
||||
|
||||
var options option.Options
|
||||
directDNSDomains := []string{}
|
||||
dnsRules := []option.DefaultDNSRule{}
|
||||
|
||||
var bind string
|
||||
if configOpt.AllowConnectionFromLAN {
|
||||
bind = "0.0.0.0"
|
||||
} else {
|
||||
bind = "127.0.0.1"
|
||||
}
|
||||
|
||||
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",
|
||||
Disabled: false,
|
||||
Timestamp: true,
|
||||
DisableColor: true,
|
||||
}
|
||||
|
||||
options.DNS = &option.DNSOptions{
|
||||
DNSClientOptions: option.DNSClientOptions{
|
||||
IndependentCache: configOpt.IndependentDNSCache,
|
||||
},
|
||||
Servers: []option.DNSServerOptions{
|
||||
{
|
||||
Tag: "dns-remote",
|
||||
Address: configOpt.RemoteDnsAddress,
|
||||
AddressResolver: "dns-direct",
|
||||
Strategy: configOpt.RemoteDnsDomainStrategy,
|
||||
},
|
||||
{
|
||||
Tag: "dns-direct",
|
||||
Address: configOpt.DirectDnsAddress,
|
||||
AddressResolver: "dns-local",
|
||||
Strategy: configOpt.DirectDnsDomainStrategy,
|
||||
Detour: "direct",
|
||||
},
|
||||
{
|
||||
Tag: "dns-local",
|
||||
Address: "local",
|
||||
Detour: "direct",
|
||||
},
|
||||
{
|
||||
Tag: "dns-block",
|
||||
Address: "rcode://success",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var inboundDomainStrategy option.DomainStrategy
|
||||
if !configOpt.ResolveDestination {
|
||||
inboundDomainStrategy = option.DomainStrategy(dns.DomainStrategyAsIS)
|
||||
} else {
|
||||
inboundDomainStrategy = configOpt.IPv6Mode
|
||||
}
|
||||
|
||||
if configOpt.EnableTun {
|
||||
tunInbound := option.Inbound{
|
||||
Type: C.TypeTun,
|
||||
Tag: "tun-in",
|
||||
TunOptions: option.TunInboundOptions{
|
||||
Stack: configOpt.TUNStack,
|
||||
MTU: configOpt.MTU,
|
||||
AutoRoute: true,
|
||||
StrictRoute: configOpt.StrictRoute,
|
||||
EndpointIndependentNat: true,
|
||||
InboundOptions: option.InboundOptions{
|
||||
SniffEnabled: true,
|
||||
SniffOverrideDestination: true,
|
||||
DomainStrategy: inboundDomainStrategy,
|
||||
},
|
||||
},
|
||||
}
|
||||
switch configOpt.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: "mixed-in",
|
||||
MixedOptions: option.HTTPMixedInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: option.NewListenAddress(netip.MustParseAddr(bind)),
|
||||
ListenPort: configOpt.MixedPort,
|
||||
InboundOptions: option.InboundOptions{
|
||||
SniffEnabled: true,
|
||||
SniffOverrideDestination: true,
|
||||
DomainStrategy: inboundDomainStrategy,
|
||||
},
|
||||
},
|
||||
SetSystemProxy: configOpt.SetSystemProxy,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
options.Inbounds = append(
|
||||
options.Inbounds,
|
||||
option.Inbound{
|
||||
Type: C.TypeDirect,
|
||||
Tag: "dns-in",
|
||||
DirectOptions: option.DirectInboundOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: option.NewListenAddress(netip.MustParseAddr(bind)),
|
||||
ListenPort: configOpt.LocalDnsPort,
|
||||
},
|
||||
OverrideAddress: "8.8.8.8",
|
||||
OverridePort: 53,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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: "bypass",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if configOpt.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: "dns-fake",
|
||||
Address: "fakeip",
|
||||
Strategy: option.DomainStrategy(dns.DomainStrategyUseIPv4),
|
||||
},
|
||||
)
|
||||
options.DNS.Rules = append(
|
||||
options.DNS.Rules,
|
||||
option.DNSRule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
Inbound: []string{"tun-in"},
|
||||
Server: "dns-fake",
|
||||
DisableCache: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, rule := range configOpt.Rules {
|
||||
routeRule := rule.MakeRule()
|
||||
switch rule.Outbound {
|
||||
case "bypass":
|
||||
routeRule.Outbound = "bypass"
|
||||
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":
|
||||
if configOpt.EnableFakeDNS {
|
||||
fakeDnsRule := dnsRule
|
||||
fakeDnsRule.Server = "dns-fake"
|
||||
fakeDnsRule.Inbound = []string{"tun-in"}
|
||||
dnsRules = append(dnsRules, fakeDnsRule)
|
||||
}
|
||||
dnsRule.Server = "dns-remote"
|
||||
}
|
||||
dnsRules = append(dnsRules, dnsRule)
|
||||
}
|
||||
|
||||
if configOpt.EnableDNSRouting {
|
||||
for _, dnsRule := range dnsRules {
|
||||
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,
|
||||
GeoIP: &option.GeoIPOptions{
|
||||
Path: configOpt.GeoIPPath,
|
||||
},
|
||||
Geosite: &option.GeositeOptions{
|
||||
Path: configOpt.GeoSitePath,
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
if value, ok := obj["tls"]; ok {
|
||||
tls := value.(map[string]interface{})
|
||||
tls["mixedcase_sni"] = configOpt.TLSTricks.EnableMixedSNICase
|
||||
if configOpt.TLSTricks.EnablePadding {
|
||||
tls["padding_size"] = configOpt.TLSTricks.PaddingSize
|
||||
} else {
|
||||
tls["padding_size"] = ""
|
||||
}
|
||||
}
|
||||
if value, ok := obj["tls_fragment"]; ok {
|
||||
tls := value.(map[string]interface{})
|
||||
tls["enabled"] = configOpt.TLSTricks.EnableFragment
|
||||
tls["size"] = configOpt.TLSTricks.FragmentSize
|
||||
tls["sleep"] = configOpt.TLSTricks.FragmentSleep
|
||||
}
|
||||
modifiedJson, err := json.Marshal(obj)
|
||||
if err == nil {
|
||||
err = out.UnmarshalJSON(modifiedJson)
|
||||
if err != nil {
|
||||
fmt.Println("error: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch out.Type {
|
||||
case C.TypeDirect, C.TypeBlock, C.TypeDNS:
|
||||
continue
|
||||
case C.TypeSelector, C.TypeURLTest:
|
||||
continue
|
||||
default:
|
||||
tags = append(tags, out.Tag)
|
||||
outbounds = append(outbounds, out)
|
||||
}
|
||||
}
|
||||
|
||||
urlTest := option.Outbound{
|
||||
Type: C.TypeURLTest,
|
||||
Tag: "auto",
|
||||
URLTestOptions: option.URLTestOutboundOptions{
|
||||
Outbounds: tags,
|
||||
URL: configOpt.ConnectionTestUrl,
|
||||
Interval: configOpt.URLTestInterval,
|
||||
},
|
||||
}
|
||||
|
||||
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: "dns-out",
|
||||
Type: C.TypeDNS,
|
||||
},
|
||||
{
|
||||
Tag: "direct",
|
||||
Type: C.TypeDirect,
|
||||
},
|
||||
{
|
||||
Tag: "bypass",
|
||||
Type: C.TypeDirect,
|
||||
},
|
||||
{
|
||||
Tag: "block",
|
||||
Type: C.TypeBlock,
|
||||
},
|
||||
}...,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func applyOverrides(overrides ConfigOptions, options option.Options) option.Options {
|
||||
if overrides.EnableClashApi {
|
||||
options.Experimental.ClashAPI = &option.ClashAPIOptions{
|
||||
ExternalController: fmt.Sprintf("%s:%d", "127.0.0.1", overrides.ClashApiPort),
|
||||
StoreSelected: true,
|
||||
}
|
||||
}
|
||||
|
||||
options.Log = &option.LogOptions{
|
||||
Level: overrides.LogLevel,
|
||||
Output: "box.log",
|
||||
Disabled: false,
|
||||
}
|
||||
|
||||
var inbounds []option.Inbound
|
||||
for _, inb := range options.Inbounds {
|
||||
if inb.Type == C.TypeTun && !overrides.EnableTun {
|
||||
continue
|
||||
}
|
||||
inbounds = append(inbounds, inb)
|
||||
}
|
||||
options.Inbounds = inbounds
|
||||
|
||||
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
|
||||
}
|
||||
8
config/config.json.template
Normal file
8
config/config.json.template
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"log": {},
|
||||
"dns": {},
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
"route": {},
|
||||
"experimental": {}
|
||||
}
|
||||
31
config/debug.go
Normal file
31
config/debug.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
func DeferPanicToError(name string, err func(error)) {
|
||||
if r := recover(); r != nil {
|
||||
s := fmt.Errorf("%s panic: %s\n%s", name, r, string(debug.Stack()))
|
||||
err(s)
|
||||
}
|
||||
}
|
||||
88
config/option.go
Normal file
88
config/option.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
dns "github.com/sagernet/sing-dns"
|
||||
)
|
||||
|
||||
type ConfigOptions struct {
|
||||
ExecuteAsIs bool `json:"execute-config-as-is"`
|
||||
LogLevel string `json:"log-level"`
|
||||
ResolveDestination bool `json:"resolve-destination"`
|
||||
IPv6Mode option.DomainStrategy `json:"ipv6-mode"`
|
||||
RemoteDnsAddress string `json:"remote-dns-address"`
|
||||
RemoteDnsDomainStrategy option.DomainStrategy `json:"remote-dns-domain-strategy"`
|
||||
DirectDnsAddress string `json:"direct-dns-address"`
|
||||
DirectDnsDomainStrategy option.DomainStrategy `json:"direct-dns-domain-strategy"`
|
||||
MixedPort uint16 `json:"mixed-port"`
|
||||
LocalDnsPort uint16 `json:"local-dns-port"`
|
||||
MTU uint32 `json:"mtu"`
|
||||
StrictRoute bool `json:"strict-route"`
|
||||
TUNStack string `json:"tun-stack"`
|
||||
ConnectionTestUrl string `json:"connection-test-url"`
|
||||
URLTestInterval option.Duration `json:"url-test-interval"`
|
||||
EnableClashApi bool `json:"enable-clash-api"`
|
||||
ClashApiPort uint16 `json:"clash-api-port"`
|
||||
EnableTun bool `json:"enable-tun"`
|
||||
SetSystemProxy bool `json:"set-system-proxy"`
|
||||
BypassLAN bool `json:"bypass-lan"`
|
||||
AllowConnectionFromLAN bool `json:"allow-connection-from-lan"`
|
||||
EnableFakeDNS bool `json:"enable-fake-dns"`
|
||||
EnableDNSRouting bool `json:"enable-dns-routing"`
|
||||
IndependentDNSCache bool `json:"independent-dns-cache"`
|
||||
GeoIPPath string `json:"geoip-path"`
|
||||
GeoSitePath string `json:"geosite-path"`
|
||||
Rules []Rule `json:"rules"`
|
||||
TLSTricks
|
||||
}
|
||||
|
||||
type TLSTricks struct {
|
||||
EnableFragment bool `json:"enable-tls-fragment"`
|
||||
FragmentSize string `json:"tls-fragment-size"`
|
||||
FragmentSleep string `json:"tls-fragment-sleep"`
|
||||
EnableMixedSNICase bool `json:"enable-tls-mixed-sni-case"`
|
||||
EnablePadding bool `json:"enable-tls-padding"`
|
||||
PaddingSize string `json:"tls-padding-size"`
|
||||
}
|
||||
|
||||
func DefaultConfigOptions() *ConfigOptions {
|
||||
return &ConfigOptions{
|
||||
ExecuteAsIs: false,
|
||||
LogLevel: "info",
|
||||
ResolveDestination: false,
|
||||
IPv6Mode: option.DomainStrategy(dns.DomainStrategyAsIS),
|
||||
RemoteDnsAddress: "1.1.1.1",
|
||||
RemoteDnsDomainStrategy: option.DomainStrategy(dns.DomainStrategyAsIS),
|
||||
DirectDnsAddress: "1.1.1.1",
|
||||
DirectDnsDomainStrategy: option.DomainStrategy(dns.DomainStrategyAsIS),
|
||||
MixedPort: 2334,
|
||||
LocalDnsPort: 6450,
|
||||
MTU: 9000,
|
||||
StrictRoute: true,
|
||||
TUNStack: "mixed",
|
||||
ConnectionTestUrl: "https://cp.cloudflare.com/",
|
||||
URLTestInterval: option.Duration(10 * time.Minute),
|
||||
EnableClashApi: true,
|
||||
ClashApiPort: 6756,
|
||||
EnableTun: true,
|
||||
SetSystemProxy: true,
|
||||
BypassLAN: false,
|
||||
AllowConnectionFromLAN: false,
|
||||
EnableFakeDNS: false,
|
||||
EnableDNSRouting: false,
|
||||
IndependentDNSCache: false,
|
||||
GeoIPPath: "geoip.db",
|
||||
GeoSitePath: "geosite.db",
|
||||
Rules: []Rule{},
|
||||
TLSTricks: TLSTricks{
|
||||
EnableFragment: false,
|
||||
FragmentSize: "10-100",
|
||||
FragmentSleep: "50-200",
|
||||
EnableMixedSNICase: false,
|
||||
EnablePadding: false,
|
||||
PaddingSize: "100-200",
|
||||
},
|
||||
}
|
||||
}
|
||||
88
config/parser.go
Normal file
88
config/parser.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hiddify/ray2sing/ray2sing"
|
||||
"github.com/sagernet/sing-box/experimental/libbox"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/xmdhs/clash2singbox/convert"
|
||||
"github.com/xmdhs/clash2singbox/model/clash"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed config.json.template
|
||||
var configByte []byte
|
||||
|
||||
var configParsers = []func([]byte, bool) ([]byte, error){
|
||||
parseSingboxConfig,
|
||||
parseV2rayConfig,
|
||||
parseClashConfig,
|
||||
}
|
||||
|
||||
func ParseConfig(path string, tempPath string, debug bool) error {
|
||||
content, err := os.ReadFile(tempPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parseError error
|
||||
for index, parser := range configParsers {
|
||||
config, err := parser(content, debug)
|
||||
if err == nil {
|
||||
fmt.Printf("[ConfigParser] success with parser #%d, checking...\n", index)
|
||||
err = libbox.CheckConfig(string(config))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(path, config, 0777)
|
||||
return err
|
||||
}
|
||||
parseError = err
|
||||
}
|
||||
return parseError
|
||||
}
|
||||
|
||||
func parseV2rayConfig(content []byte, debug bool) ([]byte, error) {
|
||||
config, err := ray2sing.Ray2Singbox(string(content))
|
||||
if err != nil {
|
||||
fmt.Printf("[V2rayParser] error: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return []byte(config), nil
|
||||
}
|
||||
|
||||
func parseClashConfig(content []byte, debug bool) ([]byte, error) {
|
||||
clashConfig := clash.Clash{}
|
||||
err := yaml.Unmarshal(content, &clashConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("[ClashParser] unmarshal error: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sbConfig, err := convert.Clash2sing(clashConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("[ClashParser] convert error: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := configByte
|
||||
output, err = convert.Patch(output, sbConfig, "", "", nil)
|
||||
if err != nil {
|
||||
fmt.Printf("[ClashParser] patch error: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func parseSingboxConfig(content []byte, debug bool) ([]byte, error) {
|
||||
var options option.Options
|
||||
err := options.UnmarshalJSON(content)
|
||||
if err != nil {
|
||||
fmt.Printf("[SingboxParser] unmarshal error: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
95
config/rules.go
Normal file
95
config/rules.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package config
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user