diff --git a/.gitignore b/.gitignore index 16db01c..d9addfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .vscode/launch.json .vscode/settings.json dist -*test.go template.json +.idea \ No newline at end of file diff --git a/.goreleaser.pre.yaml b/.goreleaser.pre.yaml index 682c7ec..661278e 100644 --- a/.goreleaser.pre.yaml +++ b/.goreleaser.pre.yaml @@ -12,7 +12,7 @@ builds: - arm - "386" ldflags: - - -s -w -X sub2sing-box/main.Version={{ .Version }} + - -s -w -X sub2sing-box/constant.Version={{ .Version }} flags: - -trimpath no_unique_dist_dir: true diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2aea47c..9a6e416 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -12,7 +12,7 @@ builds: - arm - "386" ldflags: - - -s -w -X sub2sing-box/main.Version={{ .Version }} + - -s -w -X sub2sing-box/constant.Version={{ .Version }} flags: - -trimpath no_unique_dist_dir: true diff --git a/Dockerfile b/Dockerfile index 628043e..b03858d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download ARG version -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2clash/config.Version=${version}" -o sub2sing-box main.go +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2sing-box/constant.Version=${version}" -o sub2sing-box main.go WORKDIR /app diff --git a/api/handler/convert.go b/api/handler/convert.go index 15020c3..8e7d970 100644 --- a/api/handler/convert.go +++ b/api/handler/convert.go @@ -3,8 +3,8 @@ package handler import ( "encoding/json" "sub2sing-box/api/model" - iutil "sub2sing-box/internal/util" - putil "sub2sing-box/pkg/util" + "sub2sing-box/common" + "sub2sing-box/util" "github.com/gin-gonic/gin" ) @@ -17,7 +17,7 @@ func Convert(c *gin.Context) { }) return } - j, err := iutil.DecodeBase64(c.Query("data")) + j, err := util.DecodeBase64(c.Query("data")) if err != nil { c.JSON(400, gin.H{ "error": "Invalid data", @@ -38,7 +38,7 @@ func Convert(c *gin.Context) { }) return } - result, err := putil.Convert( + result, err := common.Convert( data.Subscriptions, data.Proxies, data.Template, diff --git a/cmd/convert.go b/cmd/convert.go index 6e9171b..9bf8bf1 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" "os" - . "sub2sing-box/pkg/util" + "sub2sing-box/common" "github.com/spf13/cobra" ) @@ -40,7 +40,7 @@ var convertCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { result := "" var err error - result, err = Convert( + result, err = common.Convert( subscriptions, proxies, template, diff --git a/cmd/root.go b/cmd/root.go index 6d8c1e7..ebef5cc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,3 @@ import ( ) var RootCmd = &cobra.Command{} - -func SetVersion(version string) { - RootCmd.Version = version -} diff --git a/cmd/version.go b/cmd/version.go index 2bb8dc2..608c9ce 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "sub2sing-box/constant" "github.com/spf13/cobra" ) @@ -11,7 +12,7 @@ var versionCmd = &cobra.Command{ Short: "Print version", Long: "Print version", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("version: " + RootCmd.Version) + fmt.Println("version: " + constant.Version) }, } diff --git a/pkg/util/convert.go b/common/convert.go similarity index 69% rename from pkg/util/convert.go rename to common/convert.go index 0f8db5e..432fee3 100644 --- a/pkg/util/convert.go +++ b/common/convert.go @@ -1,4 +1,4 @@ -package util +package common import ( "encoding/json" @@ -9,10 +9,10 @@ import ( "regexp" "sort" "strings" - - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" - "sub2sing-box/pkg/parser" + C "sub2sing-box/constant" + "sub2sing-box/model" + "sub2sing-box/parser" + "sub2sing-box/util" ) func Convert( @@ -29,7 +29,7 @@ func Convert( result := "" var err error - proxyList, err := ConvertSubscriptionsToSProxy(subscriptions) + outbounds, err := ConvertSubscriptionsToSProxy(subscriptions) if err != nil { return "", err } @@ -38,18 +38,18 @@ func Convert( if err != nil { return "", err } - proxyList = append(proxyList, p) + outbounds = append(outbounds, p) } if delete != "" { - proxyList, err = DeleteProxy(proxyList, delete) + outbounds, err = DeleteProxy(outbounds, delete) if err != nil { return "", err } } for k, v := range rename { - proxyList, err = RenameProxy(proxyList, k, v) + outbounds, err = RenameProxy(outbounds, k, v) if err != nil { return "", err } @@ -57,14 +57,14 @@ func Convert( keep := make(map[int]bool) set := make(map[string]struct { - Proxy model.Proxy + Proxy model.Outbound Count int }) - for i, p := range proxyList { + for i, p := range outbounds { if _, exists := set[p.Tag]; !exists { keep[i] = true set[p.Tag] = struct { - Proxy model.Proxy + Proxy model.Outbound Count int }{p, 0} } else { @@ -72,32 +72,16 @@ func Convert( p2, _ := json.Marshal(set[p.Tag]) if string(p1) != string(p2) { set[p.Tag] = struct { - Proxy model.Proxy + Proxy model.Outbound Count int }{p, set[p.Tag].Count + 1} keep[i] = true - proxyList[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count) + outbounds[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count) } else { keep[i] = false } } } - var newProxyList []model.Proxy - for i, p := range proxyList { - if keep[i] { - newProxyList = append(newProxyList, p) - } - } - proxyList = newProxyList - var outbounds []model.Outbound - ps, err := json.Marshal(&proxyList) - if err != nil { - return "", err - } - err = json.Unmarshal(ps, &outbounds) - if err != nil { - return "", err - } if group { outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType) } @@ -120,17 +104,30 @@ func Convert( func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound { newGroup := make(map[string]model.Outbound) for _, p := range proxies { - if p.Type != "selector" && p.Type != "urltest" { + if p.Type != C.TypeSelector && p.Type != C.TypeURLTest { country := model.GetContryName(p.Tag) if group, ok := newGroup[country]; ok { - group.Outbounds = append(group.Outbounds, p.Tag) + group.SetOutbounds(append(group.GetOutbounds(), p.Tag)) newGroup[country] = group } else { - newGroup[country] = model.Outbound{ - Tag: country, - Type: groupType, - Outbounds: []string{p.Tag}, - InterruptExistConnections: true, + if groupType == C.TypeSelector { + newGroup[country] = model.Outbound{ + Tag: country, + Type: groupType, + SelectorOptions: model.SelectorOutboundOptions{ + Outbounds: []string{p.Tag}, + InterruptExistConnections: true, + }, + } + } else if groupType == C.TypeURLTest { + newGroup[country] = model.Outbound{ + Tag: country, + Type: groupType, + URLTestOptions: model.URLTestOutboundOptions{ + Outbounds: []string{p.Tag}, + InterruptExistConnections: true, + }, + } } } } @@ -180,13 +177,13 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error) } } config, err = ReadTemplate(template) + if err != nil { + return "", err + } } proxyTags := make([]string, 0) groupTags := make([]string, 0) groups := make(map[string]model.Outbound) - if err != nil { - return "", err - } for _, p := range outbounds { if model.IsCountryGroup(p.Tag) { groupTags = append(groupTags, p.Tag) @@ -199,22 +196,24 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error) } reg := regexp.MustCompile("<[A-Za-z]{2}>") for i, outbound := range config.Outbounds { - var parsedOutbound []string = make([]string, 0) - for _, o := range outbound.Outbounds { - if o == "" { - parsedOutbound = append(parsedOutbound, proxyTags...) - } else if o == "" { - parsedOutbound = append(parsedOutbound, groupTags...) - } else if reg.MatchString(o) { - country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>")) - if group, ok := groups[country]; ok { - parsedOutbound = append(parsedOutbound, group.Outbounds...) + if outbound.Type == C.TypeSelector || outbound.Type == C.TypeURLTest { + var parsedOutbound []string = make([]string, 0) + for _, o := range outbound.GetOutbounds() { + if o == "" { + parsedOutbound = append(parsedOutbound, proxyTags...) + } else if o == "" { + parsedOutbound = append(parsedOutbound, groupTags...) + } else if reg.MatchString(o) { + country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>")) + if group, ok := groups[country]; ok { + parsedOutbound = append(parsedOutbound, group.GetOutbounds()...) + } + } else { + parsedOutbound = append(parsedOutbound, o) } - } else { - parsedOutbound = append(parsedOutbound, o) } + config.Outbounds[i].SetOutbounds(parsedOutbound) } - config.Outbounds[i].Outbounds = parsedOutbound } config.Outbounds = append(config.Outbounds, outbounds...) data, err := json.Marshal(config) @@ -224,17 +223,17 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error) return string(data), nil } -func ConvertCProxyToSProxy(proxy string) (model.Proxy, error) { +func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) { for prefix, parseFunc := range parser.ParserMap { if strings.HasPrefix(proxy, prefix) { proxy, err := parseFunc(proxy) if err != nil { - return model.Proxy{}, err + return model.Outbound{}, err } return proxy, nil } } - return model.Proxy{}, errors.New("Unknown proxy format") + return model.Outbound{}, errors.New("unknown proxy format") } func ConvertCProxyToJson(proxy string) (string, error) { @@ -249,8 +248,8 @@ func ConvertCProxyToJson(proxy string) (string, error) { return string(data), nil } -func ConvertSubscriptionsToSProxy(urls []string) ([]model.Proxy, error) { - proxyList := make([]model.Proxy, 0) +func ConvertSubscriptionsToSProxy(urls []string) ([]model.Outbound, error) { + proxyList := make([]model.Outbound, 0) for _, url := range urls { data, err := util.Fetch(url, 3) if err != nil { @@ -301,12 +300,12 @@ func ReadTemplate(path string) (model.Config, error) { return res, nil } -func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) { +func DeleteProxy(proxies []model.Outbound, regex string) ([]model.Outbound, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err } - var newProxies []model.Proxy + var newProxies []model.Outbound for _, p := range proxies { if !reg.MatchString(p.Tag) { newProxies = append(newProxies, p) @@ -315,7 +314,7 @@ func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) { return newProxies, nil } -func RenameProxy(proxies []model.Proxy, regex string, replaceText string) ([]model.Proxy, error) { +func RenameProxy(proxies []model.Outbound, regex string, replaceText string) ([]model.Outbound, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err diff --git a/constant/prefix.go b/constant/prefix.go new file mode 100644 index 0000000..9a27b85 --- /dev/null +++ b/constant/prefix.go @@ -0,0 +1,11 @@ +package constant + +const ( + HysteriaPrefix string = "hysteria://" + Hysteria2Prefix1 string = "hysteria2://" + Hysteria2Prefix2 string = "hy2://" + ShadowsocksPrefix string = "ss://" + TrojanPrefix string = "trojan://" + VLESSPrefix string = "vless://" + VMessPrefix string = "vmess://" +) diff --git a/constant/proxy.go b/constant/proxy.go new file mode 100644 index 0000000..ba4e3bf --- /dev/null +++ b/constant/proxy.go @@ -0,0 +1,31 @@ +package constant + +const ( + TypeTun = "tun" + TypeRedirect = "redirect" + TypeTProxy = "tproxy" + TypeDirect = "direct" + TypeBlock = "block" + TypeDNS = "dns" + TypeSOCKS = "socks" + TypeHTTP = "http" + TypeMixed = "mixed" + TypeShadowsocks = "shadowsocks" + TypeVMess = "vmess" + TypeTrojan = "trojan" + TypeNaive = "naive" + TypeWireGuard = "wireguard" + TypeHysteria = "hysteria" + TypeTor = "tor" + TypeSSH = "ssh" + TypeShadowTLS = "shadowtls" + TypeShadowsocksR = "shadowsocksr" + TypeVLESS = "vless" + TypeTUIC = "tuic" + TypeHysteria2 = "hysteria2" +) + +const ( + TypeSelector = "selector" + TypeURLTest = "urltest" +) diff --git a/constant/version.go b/constant/version.go new file mode 100644 index 0000000..c8b5168 --- /dev/null +++ b/constant/version.go @@ -0,0 +1,3 @@ +package constant + +var Version = "dev" diff --git a/go.mod b/go.mod index 89786ee..8e73ba2 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module sub2sing-box go 1.21.5 -require github.com/spf13/cobra v1.8.0 +require ( + github.com/spf13/cobra v1.8.0 + golang.org/x/text v0.9.0 +) require ( github.com/bytedance/sonic v1.9.1 // indirect @@ -26,7 +29,6 @@ require ( golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/model/config.go b/internal/model/config.go deleted file mode 100644 index fdf0e2c..0000000 --- a/internal/model/config.go +++ /dev/null @@ -1,423 +0,0 @@ -package model - -import "encoding/json" - -type Listable[T any] []T - -func (l *Listable[T]) UnmarshalJSON(data []byte) error { - var arr []T - if err := json.Unmarshal(data, &arr); err == nil { - *l = arr - return nil - } - var v T - if err := json.Unmarshal(data, &v); err == nil { - *l = []T{v} - return nil - } - return nil -} - -type Obfs struct { - Str string - Obfs *Hysteria2Obfs - IsStr bool -} - -func (o *Obfs) UnmarshalJSON(data []byte) error { - var str string - if err := json.Unmarshal(data, &str); err == nil { - o.Str = str - o.IsStr = true - return nil - } - var obfs Hysteria2Obfs - if err := json.Unmarshal(data, &obfs); err == nil { - o.IsStr = false - o.Obfs = &obfs - return nil - } - return nil -} - -func (o Obfs) MarshalJSON() ([]byte, error) { - if o.IsStr { - return json.Marshal(o.Str) - } - return json.Marshal(o.Obfs) -} - -type Config struct { - Log *LogOptions `json:"log,omitempty"` - DNS *DNSOptions `json:"dns,omitempty"` - NTP *NTPOptions `json:"ntp,omitempty"` - Inbounds Listable[Inbound] `json:"inbounds,omitempty"` - Outbounds Listable[Outbound] `json:"outbounds,omitempty"` - Route *RouteOptions `json:"route,omitempty"` - Experimental *ExperimentalOptions `json:"experimental,omitempty"` -} - -type LogOptions struct { - Disabled bool `json:"disabled,omitempty"` - Level string `json:"level,omitempty"` - Output string `json:"output,omitempty"` - Timestamp bool `json:"timestamp,omitempty"` -} - -type DNSOptions struct { - Servers Listable[DNSServerOptions] `json:"servers,omitempty"` - Rules Listable[DNSRule] `json:"rules,omitempty"` - Final string `json:"final,omitempty"` - ReverseMapping bool `json:"reverse_mapping,omitempty"` - FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"` - Strategy string `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - DisableExpire bool `json:"disable_expire,omitempty"` - IndependentCache bool `json:"independent_cache,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` -} - -type DNSServerOptions struct { - Tag string `json:"tag,omitempty"` - Address string `json:"address"` - AddressResolver string `json:"address_resolver,omitempty"` - AddressStrategy string `json:"address_strategy,omitempty"` - Strategy string `json:"strategy,omitempty"` - Detour string `json:"detour,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` -} - -type DNSRule struct { - Type string `json:"type,omitempty"` - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[string] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet string `json:"client_subnet,omitempty"` - Mode string `json:"mode,omitempty"` - Rules Listable[DNSRule] `json:"rules,omitempty"` -} - -type DNSFakeIPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inet4Range string `json:"inet4_range,omitempty"` - Inet6Range string `json:"inet6_range,omitempty"` -} - -type NTPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Server string `json:"server,omitempty"` - ServerPort uint16 `json:"server_port,omitempty"` - Interval string `json:"interval,omitempty"` - WriteToSystem bool `json:"write_to_system,omitempty"` - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress string `json:"inet4_bind_address,omitempty"` - Inet6BindAddress string `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout string `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment bool `json:"udp_fragment,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - FallbackDelay string `json:"fallback_delay,omitempty"` -} - -type Inbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Inet4Address Listable[string] `json:"inet4_address,omitempty"` - Inet6Address Listable[string] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[string] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[string] `json:"inet6_route_address,omitempty"` - Inet4RouteExcludeAddress Listable[string] `json:"inet4_route_exclude_address,omitempty"` - Inet6RouteExcludeAddress Listable[string] `json:"inet6_route_exclude_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout string `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - SniffEnabled bool `json:"sniff,omitempty"` - SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` - SniffTimeout string `json:"sniff_timeout,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` -} - -type TunPlatformOptions struct { - HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"` -} - -type HTTPProxyOptions struct { - Enabled bool `json:"enabled,omitempty"` - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - BypassDomain Listable[string] `json:"bypass_domain,omitempty"` - MatchDomain Listable[string] `json:"match_domain,omitempty"` -} - -type Outbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress string `json:"inet4_bind_address,omitempty"` - Inet6BindAddress string `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout string `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - FallbackDelay string `json:"fallback_delay,omitempty"` - OverrideAddress string `json:"override_address,omitempty"` - OverridePort uint16 `json:"override_port,omitempty"` - ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` - Server string `json:"server,omitempty"` - ServerPort uint16 `json:"server_port,omitempty"` - Version string `json:"version,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Path string `json:"path,omitempty"` - Headers map[string]Listable[string] `json:"headers,omitempty"` - Method string `json:"method,omitempty"` - Plugin string `json:"plugin,omitempty"` - PluginOptions string `json:"plugin_opts,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - UUID string `json:"uuid,omitempty"` - Security string `json:"security,omitempty"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - SystemInterface bool `json:"system_interface,omitempty"` - GSO bool `json:"gso,omitempty"` - InterfaceName string `json:"interface_name,omitempty"` - LocalAddress Listable[string] `json:"local_address,omitempty"` - PrivateKey string `json:"private_key,omitempty"` - Peers Listable[WireGuardPeer] `json:"peers,omitempty"` - PeerPublicKey string `json:"peer_public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - Reserved Listable[uint8] `json:"reserved,omitempty"` - Workers int `json:"workers,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Obfs `json:"obfs,omitempty"` - Auth Listable[byte] `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - ExecutablePath string `json:"executable_path,omitempty"` - ExtraArgs Listable[string] `json:"extra_args,omitempty"` - DataDirectory string `json:"data_directory,omitempty"` - Options map[string]string `json:"torrc,omitempty"` - User string `json:"user,omitempty"` - PrivateKeyPath string `json:"private_key_path,omitempty"` - PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` - HostKey Listable[string] `json:"host_key,omitempty"` - HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"` - ClientVersion string `json:"client_version,omitempty"` - ObfsParam string `json:"obfs_param,omitempty"` - Protocol string `json:"protocol,omitempty"` - ProtocolParam string `json:"protocol_param,omitempty"` - Flow string `json:"flow,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` - Default string `json:"default,omitempty"` - Outbounds Listable[string] `json:"outbounds,omitempty"` - URL string `json:"url,omitempty"` - Interval string `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - IdleTimeout string `json:"idle_timeout,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type WireGuardPeer struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - PublicKey string `json:"public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - AllowedIPs Listable[string] `json:"allowed_ips,omitempty"` - Reserved Listable[uint8] `json:"reserved,omitempty"` -} - -type RouteOptions struct { - GeoIP *GeoIPOptions `json:"geoip,omitempty"` - Geosite *GeositeOptions `json:"geosite,omitempty"` - Rules Listable[Rule] `json:"rules,omitempty"` - RuleSet Listable[RuleSet] `json:"rule_set,omitempty"` - Final string `json:"final,omitempty"` - FindProcess bool `json:"find_process,omitempty"` - AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` - OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` - DefaultInterface string `json:"default_interface,omitempty"` - DefaultMark int `json:"default_mark,omitempty"` -} - -type Rule struct { - Type string `json:"type,omitempty"` - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol string `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` - Mode string `json:"mode,omitempty"` - Rules Listable[Rule] `json:"rules,omitempty"` -} - -type GeoIPOptions struct { - Path string `json:"path,omitempty"` - DownloadURL string `json:"download_url,omitempty"` - DownloadDetour string `json:"download_detour,omitempty"` -} - -type GeositeOptions struct { - Path string `json:"path,omitempty"` - DownloadURL string `json:"download_url,omitempty"` - DownloadDetour string `json:"download_detour,omitempty"` -} - -type RuleSet struct { - Type string `json:"type"` - Tag string `json:"tag"` - Format string `json:"format"` - Path string `json:"path,omitempty"` - URL string `json:"url"` - DownloadDetour string `json:"download_detour,omitempty"` - UpdateInterval string `json:"update_interval,omitempty"` -} - -type ExperimentalOptions struct { - CacheFile *CacheFileOptions `json:"cache_file,omitempty"` - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` -} - -type CacheFileOptions struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` - CacheID string `json:"cache_id,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - StoreRDRC bool `json:"store_rdrc,omitempty"` - RDRCTimeout string `json:"rdrc_timeout,omitempty"` -} - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` - // Deprecated: migrated to global cache file - StoreMode bool `json:"store_mode,omitempty"` - // Deprecated: migrated to global cache file - StoreSelected bool `json:"store_selected,omitempty"` - // Deprecated: migrated to global cache file - StoreFakeIP bool `json:"store_fakeip,omitempty"` -} - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds Listable[string] `json:"inbounds,omitempty"` - Outbounds Listable[string] `json:"outbounds,omitempty"` - Users Listable[string] `json:"users,omitempty"` -} diff --git a/internal/model/hysteria.go b/internal/model/hysteria.go deleted file mode 100644 index bf299af..0000000 --- a/internal/model/hysteria.go +++ /dev/null @@ -1,18 +0,0 @@ -package model - -type Hysteria struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Auth []byte `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` -} diff --git a/internal/model/hysteria2.go b/internal/model/hysteria2.go deleted file mode 100644 index 066a341..0000000 --- a/internal/model/hysteria2.go +++ /dev/null @@ -1,18 +0,0 @@ -package model - -type Hysteria2Obfs struct { - Type string `json:"type,omitempty"` - Password string `json:"password,omitempty"` -} - -type Hysteria2 struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` -} diff --git a/internal/model/proxy.go b/internal/model/proxy.go deleted file mode 100644 index 2c76e55..0000000 --- a/internal/model/proxy.go +++ /dev/null @@ -1,94 +0,0 @@ -package model - -import ( - "encoding/json" -) - -type Proxy struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Shadowsocks `json:"-"` - VMess `json:"-"` - VLESS `json:"-"` - Trojan `json:"-"` - TUIC `json:"-"` - Hysteria `json:"-"` - Hysteria2 `json:"-"` -} - -func (p *Proxy) MarshalJSON() ([]byte, error) { - switch p.Type { - case "shadowsocks": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Shadowsocks - }{ - Type: p.Type, - Tag: p.Tag, - Shadowsocks: p.Shadowsocks, - }) - case "vmess": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - VMess - }{ - Type: p.Type, - Tag: p.Tag, - VMess: p.VMess, - }) - case "vless": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - VLESS - }{ - Type: p.Type, - Tag: p.Tag, - VLESS: p.VLESS, - }) - case "trojan": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Trojan - }{ - Type: p.Type, - Tag: p.Tag, - Trojan: p.Trojan, - }) - case "tuic": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - TUIC - }{ - Type: p.Type, - Tag: p.Tag, - TUIC: p.TUIC, - }) - case "hysteria": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Hysteria - }{ - Type: p.Type, - Tag: p.Tag, - Hysteria: p.Hysteria, - }) - case "hysteria2": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Hysteria2 - }{ - Type: p.Type, - Tag: p.Tag, - Hysteria2: p.Hysteria2, - }) - default: - return json.Marshal(p) - } -} diff --git a/internal/model/sort.go b/internal/model/sort.go deleted file mode 100644 index 737644d..0000000 --- a/internal/model/sort.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "golang.org/x/text/collate" - "golang.org/x/text/language" -) - -type SortByNumber []Outbound - -func (a SortByNumber) Len() int { return len(a) } -func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortByNumber) Less(i, j int) bool { return len(a[i].Outbounds) < len(a[j].Outbounds) } - -type SortByTag []Outbound - -func (a SortByTag) Len() int { return len(a) } -func (a SortByTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortByTag) Less(i, j int) bool { - tags := []language.Tag{ - language.English, - language.Chinese, - } - matcher := language.NewMatcher(tags) - bestMatch, _, _ := matcher.Match(language.Make("zh")) - c := collate.New(bestMatch) - return c.CompareString(a[i].Tag, a[j].Tag) < 0 -} diff --git a/internal/model/trojan.go b/internal/model/trojan.go deleted file mode 100644 index 5e827fa..0000000 --- a/internal/model/trojan.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type Trojan struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - Password string `json:"password"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} diff --git a/internal/model/tuic.go b/internal/model/tuic.go deleted file mode 100644 index 304c57d..0000000 --- a/internal/model/tuic.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -type TUIC struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` -} diff --git a/internal/model/vless.go b/internal/model/vless.go deleted file mode 100644 index 74e355e..0000000 --- a/internal/model/vless.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -type VLESS struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - PacketEncoding *string `json:"packet_encoding,omitempty"` -} diff --git a/internal/model/vmess.go b/internal/model/vmess.go deleted file mode 100644 index 472bbfb..0000000 --- a/internal/model/vmess.go +++ /dev/null @@ -1,34 +0,0 @@ -package model - -type VmessJson struct { - V string `json:"v"` - Ps string `json:"ps"` - Add string `json:"add"` - Port interface{} `json:"port"` - Id string `json:"id"` - Aid interface{} `json:"aid"` - Scy string `json:"scy"` - Net string `json:"net"` - Type string `json:"type"` - Host string `json:"host"` - Path string `json:"path"` - Tls string `json:"tls"` - Sni string `json:"sni"` - Alpn string `json:"alpn"` - Fp string `json:"fp"` -} - -type VMess struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} diff --git a/main.go b/main.go index 98cf883..408dbce 100644 --- a/main.go +++ b/main.go @@ -5,13 +5,6 @@ import ( "sub2sing-box/cmd" ) -var Version string - -func init() { - Version = "dev" - cmd.SetVersion(Version) -} - func main() { if err := cmd.RootCmd.Execute(); err != nil { fmt.Println(err) diff --git a/model/config.go b/model/config.go new file mode 100644 index 0000000..3cdaec4 --- /dev/null +++ b/model/config.go @@ -0,0 +1,102 @@ +package model + +import "encoding/json" + +type Listable[T any] []T + +func (l *Listable[T]) UnmarshalJSON(data []byte) error { + var arr []T + if err := json.Unmarshal(data, &arr); err == nil { + *l = arr + return nil + } + var v T + if err := json.Unmarshal(data, &v); err == nil { + *l = []T{v} + return nil + } + return nil +} + +type Config struct { + Log any `json:"log,omitempty"` + DNS any `json:"dns,omitempty"` + NTP any `json:"ntp,omitempty"` + Inbounds any `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` + Route *RouteOptions `json:"route,omitempty"` + Experimental any `json:"experimental,omitempty"` +} + +type RouteOptions struct { + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Geosite *GeositeOptions `json:"geosite,omitempty"` + Rules Listable[Rule] `json:"rules,omitempty"` + RuleSet Listable[RuleSet] `json:"rule_set,omitempty"` + Final string `json:"final,omitempty"` + FindProcess bool `json:"find_process,omitempty"` + AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` + OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` + DefaultInterface string `json:"default_interface,omitempty"` + DefaultMark int `json:"default_mark,omitempty"` +} + +type Rule struct { + Type string `json:"type,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol string `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode,omitempty"` + Rules Listable[Rule] `json:"rules,omitempty"` +} + +type GeoIPOptions struct { + Path string `json:"path,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + DownloadDetour string `json:"download_detour,omitempty"` +} + +type GeositeOptions struct { + Path string `json:"path,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + DownloadDetour string `json:"download_detour,omitempty"` +} + +type RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + Path string `json:"path,omitempty"` + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval string `json:"update_interval,omitempty"` +} diff --git a/internal/model/country_code_map.go b/model/country_code_map.go similarity index 100% rename from internal/model/country_code_map.go rename to model/country_code_map.go diff --git a/model/direct.go b/model/direct.go new file mode 100644 index 0000000..64464b6 --- /dev/null +++ b/model/direct.go @@ -0,0 +1,8 @@ +package model + +type DirectOutboundOptions struct { + DialerOptions + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` + ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` +} diff --git a/model/group.go b/model/group.go new file mode 100644 index 0000000..aa0511b --- /dev/null +++ b/model/group.go @@ -0,0 +1,16 @@ +package model + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval string `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout string `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/model/hysteria.go b/model/hysteria.go new file mode 100644 index 0000000..91d0e73 --- /dev/null +++ b/model/hysteria.go @@ -0,0 +1,18 @@ +package model + +type HysteriaOutboundOptions struct { + DialerOptions + ServerOptions + Up string `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down string `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindow uint64 `json:"recv_window,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/model/hysteria2.go b/model/hysteria2.go new file mode 100644 index 0000000..5a2f117 --- /dev/null +++ b/model/hysteria2.go @@ -0,0 +1,18 @@ +package model + +type Hysteria2Obfs struct { + Type string `json:"type,omitempty"` + Password string `json:"password,omitempty"` +} + +type Hysteria2OutboundOptions struct { + DialerOptions + ServerOptions + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + BrutalDebug bool `json:"brutal_debug,omitempty"` +} diff --git a/internal/model/multiplex.go b/model/multiplex.go similarity index 100% rename from internal/model/multiplex.go rename to model/multiplex.go diff --git a/model/outbound.go b/model/outbound.go new file mode 100644 index 0000000..62a2f9d --- /dev/null +++ b/model/outbound.go @@ -0,0 +1,167 @@ +package model + +import ( + "encoding/json" + "errors" + "reflect" + C "sub2sing-box/constant" + "sub2sing-box/util" +) + +type _Outbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + DirectOptions DirectOutboundOptions `json:"-"` + SocksOptions SocksOutboundOptions `json:"-"` + HTTPOptions HTTPOutboundOptions `json:"-"` + ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` + VMessOptions VMessOutboundOptions `json:"-"` + TrojanOptions TrojanOutboundOptions `json:"-"` + WireGuardOptions WireGuardOutboundOptions `json:"-"` + HysteriaOptions HysteriaOutboundOptions `json:"-"` + TorOptions TorOutboundOptions `json:"-"` + SSHOptions SSHOutboundOptions `json:"-"` + ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"` + ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"` + VLESSOptions VLESSOutboundOptions `json:"-"` + TUICOptions TUICOutboundOptions `json:"-"` + Hysteria2Options Hysteria2OutboundOptions `json:"-"` + SelectorOptions SelectorOutboundOptions `json:"-"` + URLTestOptions URLTestOutboundOptions `json:"-"` +} +type Outbound _Outbound + +func (h *Outbound) RawOptions() (any, error) { + var rawOptionsPtr any + switch h.Type { + case C.TypeDirect: + rawOptionsPtr = &h.DirectOptions + case C.TypeBlock, C.TypeDNS: + rawOptionsPtr = nil + case C.TypeSOCKS: + rawOptionsPtr = &h.SocksOptions + case C.TypeHTTP: + rawOptionsPtr = &h.HTTPOptions + case C.TypeShadowsocks: + rawOptionsPtr = &h.ShadowsocksOptions + case C.TypeVMess: + rawOptionsPtr = &h.VMessOptions + case C.TypeTrojan: + rawOptionsPtr = &h.TrojanOptions + case C.TypeWireGuard: + rawOptionsPtr = &h.WireGuardOptions + case C.TypeHysteria: + rawOptionsPtr = &h.HysteriaOptions + case C.TypeTor: + rawOptionsPtr = &h.TorOptions + case C.TypeSSH: + rawOptionsPtr = &h.SSHOptions + case C.TypeShadowTLS: + rawOptionsPtr = &h.ShadowTLSOptions + case C.TypeShadowsocksR: + rawOptionsPtr = &h.ShadowsocksROptions + case C.TypeVLESS: + rawOptionsPtr = &h.VLESSOptions + case C.TypeTUIC: + rawOptionsPtr = &h.TUICOptions + case C.TypeHysteria2: + rawOptionsPtr = &h.Hysteria2Options + case C.TypeSelector: + rawOptionsPtr = &h.SelectorOptions + case C.TypeURLTest: + rawOptionsPtr = &h.URLTestOptions + case "": + return nil, errors.New("missing outbound type") + default: + return nil, errors.New("unknown outbound type: " + h.Type) + } + return rawOptionsPtr, nil +} + +func (h *Outbound) MarshalJSON() ([]byte, error) { + rawOptions, err := h.RawOptions() + if err != nil { + return nil, err + } + result, err := util.MergeAndMarshal(struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + }{ + Type: h.Type, + Tag: h.Tag, + }, rawOptions) + if err != nil { + return nil, err + } + return []byte(result), nil +} + +func (h *Outbound) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_Outbound)(h)) + if err != nil { + return err + } + rawOptions, err := h.RawOptions() + if err != nil { + return err + } + if rawOptions == nil { + return nil + } + err = json.Unmarshal(bytes, rawOptions) + if err != nil { + return err + } + rawOptionsType := reflect.TypeOf(rawOptions).Elem() + hValue := reflect.ValueOf(h).Elem() + for i := 0; i < hValue.NumField(); i++ { + fieldType := hValue.Field(i).Type() + if fieldType == rawOptionsType { + hValue.Field(i).Set(reflect.ValueOf(rawOptions).Elem()) + return nil + } + } + return errors.New("unknown outbound type: " + h.Type) +} + +func (h *Outbound) GetOutbounds() []string { + if h.Type == C.TypeSelector { + return h.SelectorOptions.Outbounds + } + if h.Type == C.TypeURLTest { + return h.URLTestOptions.Outbounds + } + return nil +} + +func (h *Outbound) SetOutbounds(outbounds []string) { + if h.Type == C.TypeSelector { + h.SelectorOptions.Outbounds = outbounds + } + if h.Type == C.TypeURLTest { + h.URLTestOptions.Outbounds = outbounds + } +} + +type DialerOptions struct { + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress string `json:"inet4_bind_address,omitempty"` + Inet6BindAddress string `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark int `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout string `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy string `json:"domain_strategy,omitempty"` + FallbackDelay string `json:"fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` +} + +type ServerOptions struct { + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` +} diff --git a/internal/model/shadowsocks.go b/model/shadowsocks.go similarity index 76% rename from internal/model/shadowsocks.go rename to model/shadowsocks.go index 5cdc8b1..038de5e 100644 --- a/internal/model/shadowsocks.go +++ b/model/shadowsocks.go @@ -1,8 +1,8 @@ package model -type Shadowsocks struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` +type ShadowsocksOutboundOptions struct { + DialerOptions + ServerOptions Method string `json:"method"` Password string `json:"password"` Plugin string `json:"plugin,omitempty"` diff --git a/model/shadowsocksr.go b/model/shadowsocksr.go new file mode 100644 index 0000000..7cbc051 --- /dev/null +++ b/model/shadowsocksr.go @@ -0,0 +1,13 @@ +package model + +type ShadowsocksROutboundOptions struct { + DialerOptions + ServerOptions + Method string `json:"method"` + Password string `json:"password"` + Obfs string `json:"obfs,omitempty"` + ObfsParam string `json:"obfs_param,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolParam string `json:"protocol_param,omitempty"` + Network string `json:"network,omitempty"` +} diff --git a/model/shadowtls.go b/model/shadowtls.go new file mode 100644 index 0000000..30cd488 --- /dev/null +++ b/model/shadowtls.go @@ -0,0 +1,19 @@ +package model + +type ShadowTLSUser struct { + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` +} + +type ShadowTLSHandshakeOptions struct { + ServerOptions + DialerOptions +} + +type ShadowTLSOutboundOptions struct { + DialerOptions + ServerOptions + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/model/simple.go b/model/simple.go new file mode 100644 index 0000000..adf7439 --- /dev/null +++ b/model/simple.go @@ -0,0 +1,21 @@ +package model + +type SocksOutboundOptions struct { + DialerOptions + ServerOptions + Version string `json:"version,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network Listable[string] `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` +} + +type HTTPOutboundOptions struct { + DialerOptions + ServerOptions + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer + Path string `json:"path,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} diff --git a/model/sort.go b/model/sort.go new file mode 100644 index 0000000..e80e8eb --- /dev/null +++ b/model/sort.go @@ -0,0 +1,44 @@ +package model + +import ( + C "sub2sing-box/constant" + + "golang.org/x/text/collate" + "golang.org/x/text/language" +) + +type SortByNumber []Outbound + +func (a SortByNumber) Len() int { return len(a) } +func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortByNumber) Less(i, j int) bool { + var size1, size2 int + if a[i].Type == C.TypeSelector { + size1 = len(a[i].SelectorOptions.Outbounds) + } + if a[i].Type == C.TypeURLTest { + size1 = len(a[j].URLTestOptions.Outbounds) + } + if a[j].Type == C.TypeSelector { + size2 = len(a[j].SelectorOptions.Outbounds) + } + if a[j].Type == C.TypeURLTest { + size2 = len(a[j].URLTestOptions.Outbounds) + } + return size1 < size2 +} + +type SortByTag []Outbound + +func (a SortByTag) Len() int { return len(a) } +func (a SortByTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortByTag) Less(i, j int) bool { + tags := []language.Tag{ + language.English, + language.Chinese, + } + matcher := language.NewMatcher(tags) + bestMatch, _, _ := matcher.Match(language.Make("zh")) + c := collate.New(bestMatch) + return c.CompareString(a[i].Tag, a[j].Tag) < 0 +} diff --git a/model/ssh.go b/model/ssh.go new file mode 100644 index 0000000..3aa962f --- /dev/null +++ b/model/ssh.go @@ -0,0 +1,14 @@ +package model + +type SSHOutboundOptions struct { + DialerOptions + ServerOptions + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + PrivateKey Listable[string] `json:"private_key,omitempty"` + PrivateKeyPath string `json:"private_key_path,omitempty"` + PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` + HostKey Listable[string] `json:"host_key,omitempty"` + HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"` + ClientVersion string `json:"client_version,omitempty"` +} diff --git a/internal/model/tls.go b/model/tls.go similarity index 94% rename from internal/model/tls.go rename to model/tls.go index 6542944..64426de 100644 --- a/internal/model/tls.go +++ b/model/tls.go @@ -34,3 +34,7 @@ type OutboundRealityOptions struct { PublicKey string `json:"public_key,omitempty"` ShortID string `json:"short_id,omitempty"` } + +type OutboundTLSOptionsContainer struct { + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/model/tor.go b/model/tor.go new file mode 100644 index 0000000..4e04421 --- /dev/null +++ b/model/tor.go @@ -0,0 +1,9 @@ +package model + +type TorOutboundOptions struct { + DialerOptions + ExecutablePath string `json:"executable_path,omitempty"` + ExtraArgs []string `json:"extra_args,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + Options map[string]string `json:"torrc,omitempty"` +} diff --git a/model/trojan.go b/model/trojan.go new file mode 100644 index 0000000..a1176c0 --- /dev/null +++ b/model/trojan.go @@ -0,0 +1,11 @@ +package model + +type TrojanOutboundOptions struct { + DialerOptions + ServerOptions + Password string `json:"password"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/model/tuic.go b/model/tuic.go new file mode 100644 index 0000000..6cf3c91 --- /dev/null +++ b/model/tuic.go @@ -0,0 +1,15 @@ +package model + +type TUICOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat string `json:"heartbeat,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/internal/model/udp_over_tcp.go b/model/udp_over_tcp.go similarity index 100% rename from internal/model/udp_over_tcp.go rename to model/udp_over_tcp.go diff --git a/internal/model/v2ray_transport.go b/model/v2ray_transport.go similarity index 100% rename from internal/model/v2ray_transport.go rename to model/v2ray_transport.go diff --git a/model/vless.go b/model/vless.go new file mode 100644 index 0000000..d656087 --- /dev/null +++ b/model/vless.go @@ -0,0 +1,13 @@ +package model + +type VLESSOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + PacketEncoding *string `json:"packet_encoding,omitempty"` +} diff --git a/model/vmess.go b/model/vmess.go new file mode 100644 index 0000000..2063987 --- /dev/null +++ b/model/vmess.go @@ -0,0 +1,34 @@ +package model + +type VmessJson struct { + V string `json:"v"` + Ps string `json:"ps"` + Add string `json:"add"` + Port interface{} `json:"port"` + Id string `json:"id"` + Aid interface{} `json:"aid"` + Scy string `json:"scy"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + Path string `json:"path"` + Tls string `json:"tls"` + Sni string `json:"sni"` + Alpn string `json:"alpn"` + Fp string `json:"fp"` +} + +type VMessOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/model/wireguard.go b/model/wireguard.go new file mode 100644 index 0000000..5d1dfd2 --- /dev/null +++ b/model/wireguard.go @@ -0,0 +1,28 @@ +package model + +import "net/netip" + +type WireGuardOutboundOptions struct { + DialerOptions + SystemInterface bool `json:"system_interface,omitempty"` + GSO bool `json:"gso,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + LocalAddress Listable[netip.Prefix] `json:"local_address"` + PrivateKey string `json:"private_key"` + Peers []WireGuardPeer `json:"peers,omitempty"` + ServerOptions + PeerPublicKey string `json:"peer_public_key"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` + Workers int `json:"workers,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Network string `json:"network,omitempty"` +} + +type WireGuardPeer struct { + ServerOptions + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs Listable[string] `json:"allowed_ips,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` +} diff --git a/parser/error.go b/parser/error.go new file mode 100644 index 0000000..af95f0b --- /dev/null +++ b/parser/error.go @@ -0,0 +1,24 @@ +package parser + +type ParseError struct { + Type ParseErrorType + Message string + Raw string +} + +type ParseErrorType string + +const ( + ErrInvalidPrefix ParseErrorType = "invalid url prefix" + ErrInvalidStruct ParseErrorType = "invalid struct" + ErrInvalidPort ParseErrorType = "invalid port number" + ErrCannotParseParams ParseErrorType = "cannot parse query parameters" + ErrInvalidBase64 ParseErrorType = "invalid base64" +) + +func (e *ParseError) Error() string { + if e.Message != "" { + return string(e.Type) + ": " + e.Message + " \"" + e.Raw + "\"" + } + return string(e.Type) +} diff --git a/parser/hysteria.go b/parser/hysteria.go new file mode 100644 index 0000000..3fc487e --- /dev/null +++ b/parser/hysteria.go @@ -0,0 +1,89 @@ +package parser + +import ( + "net/url" + "strconv" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" +) + +func ParseHysteria(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.HysteriaPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix) + urlParts := strings.SplitN(proxy, "?", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + + serverInfo := strings.SplitN(urlParts[0], ":", 2) + if len(serverInfo) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + server, portStr := serverInfo[0], serverInfo[1] + + port, err := ParsePort(portStr) + if err != nil { + return model.Outbound{}, err + } + + params, err := url.ParseQuery(urlParts[1]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + + protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := params.Get("protocol"), params.Get("auth"), params.Get("insecure"), params.Get("upmbps"), params.Get("downmbps"), params.Get("obfs"), params.Get("alpn") + insecureBool, err := strconv.ParseBool(insecure) + if err != nil { + insecureBool = false + } + + var alpn []string + alpnStr = strings.TrimSpace(alpnStr) + if alpnStr != "" { + alpn = strings.Split(alpnStr, ",") + } + + remarks := server + ":" + portStr + if params.Get("remarks") != "" { + remarks = params.Get("remarks") + } + + return model.Outbound{ + Type: "hysteria", + Tag: remarks, + HysteriaOptions: model.HysteriaOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: port, + }, + Up: upmbps, + Down: downmbps, + Auth: []byte(auth), + Obfs: obfs, + Network: protocol, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + Insecure: insecureBool, + ALPN: alpn, + }, + }, + }, + }, nil +} diff --git a/parser/hysteria2.go b/parser/hysteria2.go new file mode 100644 index 0000000..3afcff5 --- /dev/null +++ b/parser/hysteria2.go @@ -0,0 +1,94 @@ +package parser + +import ( + "net/url" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" +) + +func ParseHysteria2(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) && + !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1) + proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + password := urlParts[0] + + serverInfo := strings.SplitN(urlParts[1], "/?", 2) + if len(serverInfo) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing params in url", + Raw: proxy, + } + } + paramStr := serverInfo[1] + + serverAndPort := strings.SplitN(serverInfo[0], ":", 2) + var server string + var portStr string + if len(serverAndPort) == 1 { + portStr = "443" + } else if len(serverAndPort) == 2 { + server, portStr = serverAndPort[0], serverAndPort[1] + } else { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + + port, err := ParsePort(portStr) + if err != nil { + return model.Outbound{}, err + } + + params, err := url.ParseQuery(paramStr) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + + remarks, network, obfs, obfsPassword, pinSHA256, insecure, sni := params.Get("name"), params.Get("network"), params.Get("obfs"), params.Get("obfs-password"), params.Get("pinSHA256"), params.Get("insecure"), params.Get("sni") + enableTLS := pinSHA256 != "" + insecureBool := insecure == "1" + + result := model.Outbound{ + Type: "hysteria2", + Tag: remarks, + Hysteria2Options: model.Hysteria2OutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Password: password, + Obfs: &model.Hysteria2Obfs{ + Type: obfs, + Password: obfsPassword, + }, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{Enabled: enableTLS, + Insecure: insecureBool, + ServerName: sni, + Certificate: []string{pinSHA256}}, + }, + Network: network, + }, + } + return result, nil +} diff --git a/parser/parsers_map.go b/parser/parsers_map.go new file mode 100644 index 0000000..dd8e9ac --- /dev/null +++ b/parser/parsers_map.go @@ -0,0 +1,16 @@ +package parser + +import ( + "sub2sing-box/constant" + "sub2sing-box/model" +) + +var ParserMap map[string]func(string) (model.Outbound, error) = map[string]func(string) (model.Outbound, error){ + constant.ShadowsocksPrefix: ParseShadowsocks, + constant.VMessPrefix: ParseVmess, + constant.TrojanPrefix: ParseTrojan, + constant.VLESSPrefix: ParseVless, + constant.HysteriaPrefix: ParseHysteria, + constant.Hysteria2Prefix1: ParseHysteria2, + constant.Hysteria2Prefix2: ParseHysteria2, +} diff --git a/parser/port.go b/parser/port.go new file mode 100644 index 0000000..88e0e1e --- /dev/null +++ b/parser/port.go @@ -0,0 +1,23 @@ +package parser + +import ( + "strconv" +) + +func ParsePort(portStr string) (uint16, error) { + port, err := strconv.Atoi(portStr) + + if err != nil { + return 0, &ParseError{ + Type: ErrInvalidPort, + Message: portStr, + } + } + if port < 1 || port > 65535 { + return 0, &ParseError{ + Type: ErrInvalidPort, + Message: portStr, + } + } + return uint16(port), nil +} diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go new file mode 100644 index 0000000..e2bb7aa --- /dev/null +++ b/parser/shadowsocks.go @@ -0,0 +1,91 @@ +package parser + +import ( + "net/url" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" + "sub2sing-box/util" +) + +func ParseShadowsocks(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + + var serverAndPort []string + if !strings.Contains(urlParts[0], ":") { + decoded, err := util.DecodeBase64(urlParts[0]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid base64 encoded", + Raw: proxy, + } + } + urlParts[0] = decoded + } + credentials := strings.SplitN(urlParts[0], ":", 2) + if len(credentials) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + method, password := credentials[0], credentials[1] + + serverInfo := strings.SplitN(urlParts[1], "#", 2) + serverAndPort = strings.SplitN(serverInfo[0], ":", 2) + server, portStr := serverAndPort[0], serverAndPort[1] + if len(serverInfo) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + port, err := ParsePort(portStr) + if err != nil { + return model.Outbound{}, err + } + + var remarks string + if len(serverInfo) == 2 { + unescape, err := url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "cannot unescape remarks", + Raw: proxy, + } + } + remarks = strings.TrimSpace(unescape) + } else { + remarks = strings.TrimSpace(server + ":" + portStr) + } + + result := model.Outbound{ + Type: "shadowsocks", + Tag: remarks, + ShadowsocksOptions: model.ShadowsocksOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: port, + }, + Method: method, + Password: password, + }, + } + return result, nil +} diff --git a/parser/trojan.go b/parser/trojan.go new file mode 100644 index 0000000..dc09e97 --- /dev/null +++ b/parser/trojan.go @@ -0,0 +1,157 @@ +package parser + +import ( + "net/url" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" +) + +func ParseTrojan(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.TrojanPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix) + urlParts := strings.SplitN(proxy, "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + password := strings.TrimSpace(urlParts[0]) + + serverInfo := strings.SplitN(urlParts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + if len(serverAndPortAndParams) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + server, portStr := serverAndPort[0], serverAndPort[1] + + params, err := url.ParseQuery(serverAndPortAndParams[1]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + + port, err := ParsePort(portStr) + if err != nil { + return model.Outbound{}, err + } + + remarks := "" + if len(serverInfo) == 2 { + remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) + } else { + remarks = serverAndPort[0] + } + + network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := params.Get("type"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("pbk"), params.Get("sid"), params.Get("fp"), params.Get("path"), params.Get("host"), params.Get("serviceName") + + var alpn []string + if strings.Contains(alpnStr, ",") { + alpn = strings.Split(alpnStr, ",") + } else { + alpn = nil + } + + enableUTLS := fp != "" + + result := model.Outbound{ + Type: "trojan", + Tag: remarks, + TrojanOptions: model.TrojanOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: port, + }, + Password: password, + Network: network, + }, + } + + if security == "xtls" || security == "tls" { + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + ServerName: sni, + }, + } + } + + if params.Get("security") == "reality" { + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ServerName: sni, + Reality: &model.OutboundRealityOptions{ + Enabled: true, + PublicKey: pbk, + ShortID: sid, + }, + UTLS: &model.OutboundUTLSOptions{ + Enabled: enableUTLS, + Fingerprint: fp, + }, + }, + } + } + + if params.Get("type") == "ws" { + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: path, + Headers: map[string]string{ + "Host": host, + }, + }, + } + } + + if params.Get("type") == "http" { + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ + Type: "http", + HTTPOptions: model.V2RayHTTPOptions{ + Host: []string{host}, + Path: params.Get("path"), + }, + } + } + + if params.Get("type") == "quic" { + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ + Type: "quic", + QUICOptions: model.V2RayQUICOptions{}, + } + } + + if params.Get("type") == "grpc" { + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ + Type: "grpc", + GRPCOptions: model.V2RayGRPCOptions{ + ServiceName: serviceName, + }, + } + } + return result, nil +} diff --git a/parser/vless.go b/parser/vless.go new file mode 100644 index 0000000..987de8e --- /dev/null +++ b/parser/vless.go @@ -0,0 +1,169 @@ +package parser + +import ( + "net/url" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" +) + +func ParseVless(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.VLESSPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2) + if len(urlParts) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '@' in url", + Raw: proxy, + } + } + + serverInfo := strings.SplitN(urlParts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + if len(serverAndPortAndParams) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing character '?' in url", + Raw: proxy, + } + } + + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } + } + server, portStr := serverAndPort[0], serverAndPort[1] + port, err := ParsePort(portStr) + if err != nil { + return model.Outbound{}, err + } + + params, err := url.ParseQuery(serverAndPortAndParams[1]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + + remarks := "" + if len(serverInfo) == 2 { + if strings.Contains(serverInfo[1], "|") { + remarks = strings.SplitN(serverInfo[1], "|", 2)[1] + } else { + remarks, err = url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + } + } else { + remarks, err = url.QueryUnescape(server) + if err != nil { + return model.Outbound{}, err + } + } + + uuid := strings.TrimSpace(urlParts[0]) + flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName := params.Get("flow"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("allowInsecure"), params.Get("fp"), params.Get("pbk"), params.Get("sid"), params.Get("path"), params.Get("host"), params.Get("serviceName") + + enableUTLS := fp != "" + insecureBool := insecure == "1" + var alpn []string + if strings.Contains(alpnStr, ",") { + alpn = strings.Split(alpnStr, ",") + } else { + alpn = nil + } + + result := model.Outbound{ + Type: "vless", + Tag: remarks, + VLESSOptions: model.VLESSOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: port, + }, + UUID: uuid, + Flow: flow, + }, + } + + if security == "tls" { + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + ServerName: sni, + Insecure: insecureBool, + }, + } + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: enableUTLS, + Fingerprint: fp, + } + } + + if security == "reality" { + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.Reality = &model.OutboundRealityOptions{ + Enabled: true, + PublicKey: pbk, + ShortID: sid, + } + } + + if params.Get("type") == "ws" { + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: path, + }, + } + result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = host + } + + if params.Get("type") == "quic" { + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ + Type: "quic", + QUICOptions: model.V2RayQUICOptions{}, + } + } + + if params.Get("type") == "grpc" { + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ + Type: "grpc", + GRPCOptions: model.V2RayGRPCOptions{ + ServiceName: serviceName, + }, + } + } + + if params.Get("type") == "http" { + hosts, err := url.QueryUnescape(host) + if err != nil { + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } + } + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ + Type: "http", + HTTPOptions: model.V2RayHTTPOptions{ + Host: strings.Split(hosts, ","), + }, + } + } + return result, nil +} diff --git a/parser/vmess.go b/parser/vmess.go new file mode 100644 index 0000000..a290fd6 --- /dev/null +++ b/parser/vmess.go @@ -0,0 +1,149 @@ +package parser + +import ( + "encoding/json" + "net/url" + "strconv" + "strings" + "sub2sing-box/constant" + "sub2sing-box/model" + "sub2sing-box/util" +) + +func ParseVmess(proxy string) (model.Outbound, error) { + if !strings.HasPrefix(proxy, constant.VMessPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} + } + + proxy = strings.TrimPrefix(proxy, constant.VMessPrefix) + base64, err := util.DecodeBase64(proxy) + if err != nil { + return model.Outbound{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()} + } + + var vmess model.VmessJson + err = json.Unmarshal([]byte(base64), &vmess) + if err != nil { + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} + } + + var port uint16 + switch vmess.Port.(type) { + case string: + port, err = ParsePort(vmess.Port.(string)) + if err != nil { + return model.Outbound{}, err + } + case float64: + port = uint16(vmess.Port.(float64)) + } + + aid := 0 + switch vmess.Aid.(type) { + case string: + aid, err = strconv.Atoi(vmess.Aid.(string)) + if err != nil { + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} + } + case float64: + aid = int(vmess.Aid.(float64)) + } + + if vmess.Scy == "" { + vmess.Scy = "auto" + } + + name, err := url.QueryUnescape(vmess.Ps) + if err != nil { + name = vmess.Ps + } + + result := model.Outbound{ + Type: "vmess", + Tag: name, + VMessOptions: model.VMessOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: vmess.Add, + ServerPort: port, + }, + UUID: vmess.Id, + AlterId: aid, + Security: vmess.Scy, + }, + } + + if vmess.Tls == "tls" { + var alpn []string + if strings.Contains(vmess.Alpn, ",") { + alpn = strings.Split(vmess.Alpn, ",") + } else { + alpn = nil + } + result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + UTLS: &model.OutboundUTLSOptions{ + Fingerprint: vmess.Fp, + }, + ALPN: alpn, + ServerName: vmess.Sni, + }, + } + if vmess.Fp != "" { + result.VMessOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: vmess.Fp, + } + } + } + + if vmess.Net == "ws" { + if vmess.Path == "" { + vmess.Path = "/" + } + if vmess.Host == "" { + vmess.Host = vmess.Add + } + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: vmess.Path, + Headers: map[string]string{ + "Host": vmess.Host, + }, + }, + } + } + + if vmess.Net == "quic" { + quic := model.V2RayQUICOptions{} + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "quic", + QUICOptions: quic, + } + } + + if vmess.Net == "grpc" { + grpc := model.V2RayGRPCOptions{ + ServiceName: vmess.Path, + PermitWithoutStream: true, + } + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "grpc", + GRPCOptions: grpc, + } + } + + if vmess.Net == "h2" { + httpOps := model.V2RayHTTPOptions{ + Host: strings.Split(vmess.Host, ","), + Path: vmess.Path, + } + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "http", + HTTPOptions: httpOps, + } + } + + return result, nil +} diff --git a/pkg/parser/hysteria.go b/pkg/parser/hysteria.go deleted file mode 100644 index bcfebfe..0000000 --- a/pkg/parser/hysteria.go +++ /dev/null @@ -1,88 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" -) - -//hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks -// -//- host: hostname or IP address of the server to connect to (required) -//- port: port of the server to connect to (required) -//- protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp") -//- auth: authentication payload (string) (optional) -//- peer: SNI for TLS (optional) -//- insecure: ignore certificate errors (optional) -//- upmbps: upstream bandwidth in Mbps (required) -//- downmbps: downstream bandwidth in Mbps (required) -//- alpn: QUIC ALPN (optional) -//- obfs: Obfuscation mode (optional, empty or "xplus") -//- obfsParam: Obfuscation password (optional) -//- remarks: remarks (optional) - -func ParseHysteria(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "hysteria://") { - return model.Proxy{}, errors.New("invalid hysteria Url") - } - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) - serverInfo := strings.SplitN(parts[0], ":", 2) - if len(serverInfo) != 2 { - return model.Proxy{}, errors.New("invalid hysteria Url") - } - params, err := url.ParseQuery(parts[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") - } - host := serverInfo[0] - port, err := strconv.Atoi(serverInfo[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") - } - protocol := params.Get("protocol") - auth := params.Get("auth") - // peer := params.Get("peer") - insecure := params.Get("insecure") - upmbps := params.Get("upmbps") - downmbps := params.Get("downmbps") - obfs := params.Get("obfs") - // obfsParam := params.Get("obfsParam") - var alpn []string - if params.Get("alpn") != "" { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - remarks := "" - if strings.Contains(parts[1], "#") { - r := strings.Split(parts[1], "#") - remarks = r[len(r)-1] - } else { - remarks = serverInfo[0] + ":" + serverInfo[1] - } - insecureBool, err := strconv.ParseBool(insecure) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") - } - result := model.Proxy{ - Type: "hysteria", - Tag: remarks, - Hysteria: model.Hysteria{ - Server: host, - ServerPort: uint16(port), - Up: upmbps, - Down: downmbps, - Auth: []byte(auth), - Obfs: obfs, - Network: protocol, - TLS: &model.OutboundTLSOptions{ - Enabled: true, - Insecure: insecureBool, - ALPN: alpn, - }, - }, - } - return result, nil -} diff --git a/pkg/parser/hysteria2.go b/pkg/parser/hysteria2.go deleted file mode 100644 index c1ce410..0000000 --- a/pkg/parser/hysteria2.go +++ /dev/null @@ -1,58 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" -) - -// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com - -func ParseHysteria2(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) - serverInfo := strings.SplitN(parts[1], "/?", 2) - serverAndPort := strings.SplitN(serverInfo[0], ":", 2) - if len(serverAndPort) == 1 { - serverAndPort = append(serverAndPort, "443") - } else if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - params, err := url.ParseQuery(serverInfo[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - port, err := strconv.Atoi(serverAndPort[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") - } - remarks := params.Get("name") - server := serverAndPort[0] - password := parts[0] - network := params.Get("network") - result := model.Proxy{ - Type: "hysteria2", - Tag: remarks, - Hysteria2: model.Hysteria2{ - Server: server, - ServerPort: uint16(port), - Password: password, - Obfs: &model.Hysteria2Obfs{ - Type: params.Get("obfs"), - Password: params.Get("obfs-password"), - }, - TLS: &model.OutboundTLSOptions{ - Enabled: params.Get("pinSHA256") != "", - Insecure: params.Get("insecure") == "1", - ServerName: params.Get("sni"), - Certificate: []string{params.Get("pinSHA256")}, - }, - Network: network, - }, - } - return result, nil -} diff --git a/pkg/parser/parsers_map.go b/pkg/parser/parsers_map.go deleted file mode 100644 index 06082c1..0000000 --- a/pkg/parser/parsers_map.go +++ /dev/null @@ -1,15 +0,0 @@ -package parser - -import ( - "sub2sing-box/internal/model" -) - -var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ - "ss://": ParseShadowsocks, - "vmess://": ParseVmess, - "trojan://": ParseTrojan, - "vless://": ParseVless, - "hysteria://": ParseHysteria, - "hy2://": ParseHysteria2, - "hysteria2://": ParseHysteria2, -} diff --git a/pkg/parser/shadowsocks.go b/pkg/parser/shadowsocks.go deleted file mode 100644 index 7b28579..0000000 --- a/pkg/parser/shadowsocks.go +++ /dev/null @@ -1,64 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" -) - -func ParseShadowsocks(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, errors.New("invalid ss Url") - } - parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) - if len(parts) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - if !strings.Contains(parts[0], ":") { - decoded, err := util.DecodeBase64(parts[0]) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - parts[0] = decoded - } - credentials := strings.SplitN(parts[0], ":", 2) - if len(credentials) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPort := strings.SplitN(serverInfo[0], ":", 2) - if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid ss Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - remarks := "" - if len(serverInfo) == 2 { - unescape, err := url.QueryUnescape(serverInfo[1]) - if err != nil { - return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) - } - remarks = strings.TrimSpace(unescape) - } else { - remarks = strings.TrimSpace(serverAndPort[0]) - } - method := credentials[0] - password := credentials[1] - server := strings.TrimSpace(serverAndPort[0]) - result := model.Proxy{ - Type: "shadowsocks", - Tag: remarks, - Shadowsocks: model.Shadowsocks{ - Method: method, - Password: password, - Server: server, - ServerPort: uint16(port), - }, - } - return result, nil -} diff --git a/pkg/parser/trojan.go b/pkg/parser/trojan.go deleted file mode 100644 index dd1ab3b..0000000 --- a/pkg/parser/trojan.go +++ /dev/null @@ -1,114 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" -) - -func ParseTrojan(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "trojan://") { - return model.Proxy{}, errors.New("invalid trojan Url") - } - parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) - if len(parts) != 2 { - return model.Proxy{}, errors.New("invalid trojan Url") - } - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) - serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) - params, err := url.ParseQuery(serverAndPortAndParams[1]) - if err != nil { - return model.Proxy{}, err - } - if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid trojan Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Proxy{}, err - } - remarks := "" - if len(serverInfo) == 2 { - remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) - } else { - remarks = serverAndPort[0] - } - server := strings.TrimSpace(serverAndPort[0]) - password := strings.TrimSpace(parts[0]) - result := model.Proxy{ - Type: "trojan", - Tag: remarks, - Trojan: model.Trojan{ - Server: server, - ServerPort: uint16(port), - Password: password, - Network: params.Get("type"), - }, - } - if params.Get("security") == "xtls" || params.Get("security") == "tls" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - result.Trojan.TLS = &model.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - ServerName: params.Get("sni"), - } - } - if params.Get("security") == "reality" { - result.Trojan.TLS = &model.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - Reality: &model.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - UTLS: &model.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), - }, - } - } - if params.Get("type") == "ws" { - result.Trojan.Transport = &model.V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), - Headers: map[string]string{ - "Host": params.Get("host"), - }, - }, - } - } - if params.Get("type") == "http" { - result.Trojan.Transport = &model.V2RayTransportOptions{ - Type: "http", - HTTPOptions: model.V2RayHTTPOptions{ - Host: []string{params.Get("host")}, - Path: params.Get("path"), - }, - } - } - if params.Get("type") == "quic" { - result.Trojan.Transport = &model.V2RayTransportOptions{ - Type: "quic", - QUICOptions: model.V2RayQUICOptions{}, - } - } - if params.Get("type") == "grpc" { - result.Trojan.Transport = &model.V2RayTransportOptions{ - Type: "grpc", - GRPCOptions: model.V2RayGRPCOptions{ - ServiceName: params.Get("serviceName"), - }, - } - } - return result, nil -} diff --git a/pkg/parser/vless.go b/pkg/parser/vless.go deleted file mode 100644 index a7767dc..0000000 --- a/pkg/parser/vless.go +++ /dev/null @@ -1,134 +0,0 @@ -package parser - -import ( - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" -) - -func ParseVless(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "vless://") { - return model.Proxy{}, errors.New("invalid vless Url") - } - parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) - if len(parts) != 2 { - return model.Proxy{}, errors.New("invalid vless Url") - } - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) - serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) - params, err := url.ParseQuery(serverAndPortAndParams[1]) - if err != nil { - return model.Proxy{}, err - } - if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid vless Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Proxy{}, err - } - remarks := "" - if len(serverInfo) == 2 { - if strings.Contains(serverInfo[1], "|") { - remarks = strings.SplitN(serverInfo[1], "|", 2)[1] - } else { - remarks, err = url.QueryUnescape(serverInfo[1]) - if err != nil { - return model.Proxy{}, err - } - } - } else { - remarks, err = url.QueryUnescape(serverAndPort[0]) - if err != nil { - return model.Proxy{}, err - } - } - server := strings.TrimSpace(serverAndPort[0]) - uuid := strings.TrimSpace(parts[0]) - result := model.Proxy{ - Type: "vless", - Tag: remarks, - VLESS: model.VLESS{ - Server: server, - ServerPort: uint16(port), - UUID: uuid, - Flow: params.Get("flow"), - }, - } - if params.Get("security") == "tls" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - result.VLESS.TLS = &model.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - Insecure: params.Get("allowInsecure") == "1", - } - } - if params.Get("security") == "reality" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - result.VLESS.TLS = &model.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - UTLS: &model.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), - }, - Reality: &model.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - ALPN: alpn, - } - } - if params.Get("type") == "ws" { - result.VLESS.Transport = &model.V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), - Headers: map[string]string{ - "Host": params.Get("host"), - }, - }, - } - } - if params.Get("type") == "quic" { - result.VLESS.Transport = &model.V2RayTransportOptions{ - Type: "quic", - QUICOptions: model.V2RayQUICOptions{}, - } - } - if params.Get("type") == "grpc" { - result.VLESS.Transport = &model.V2RayTransportOptions{ - Type: "grpc", - GRPCOptions: model.V2RayGRPCOptions{ - ServiceName: params.Get("serviceName"), - }, - } - } - if params.Get("type") == "http" { - host, err := url.QueryUnescape(params.Get("host")) - if err != nil { - return model.Proxy{}, err - } - result.VLESS.Transport = &model.V2RayTransportOptions{ - Type: "http", - HTTPOptions: model.V2RayHTTPOptions{ - Host: strings.Split(host, ","), - }, - } - } - return result, nil -} diff --git a/pkg/parser/vmess.go b/pkg/parser/vmess.go deleted file mode 100644 index a00ff00..0000000 --- a/pkg/parser/vmess.go +++ /dev/null @@ -1,138 +0,0 @@ -package parser - -import ( - "encoding/json" - "errors" - "net/url" - "strconv" - "strings" - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" -) - -func ParseVmess(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, errors.New("invalid vmess url") - } - base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) - if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) - } - var vmess model.VmessJson - err = json.Unmarshal([]byte(base64), &vmess) - if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) - } - port := 0 - switch vmess.Port.(type) { - case string: - port, err = strconv.Atoi(vmess.Port.(string)) - if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) - } - case float64: - port = int(vmess.Port.(float64)) - } - aid := 0 - switch vmess.Aid.(type) { - case string: - aid, err = strconv.Atoi(vmess.Aid.(string)) - if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) - } - case float64: - aid = int(vmess.Aid.(float64)) - } - if vmess.Scy == "" { - vmess.Scy = "auto" - } - - name, err := url.QueryUnescape(vmess.Ps) - if err != nil { - name = vmess.Ps - } - - result := model.Proxy{ - Type: "vmess", - Tag: name, - VMess: model.VMess{ - Server: vmess.Add, - ServerPort: uint16(port), - UUID: vmess.Id, - AlterId: aid, - Security: vmess.Scy, - }, - } - - if vmess.Tls == "tls" { - var alpn []string - if strings.Contains(vmess.Alpn, ",") { - alpn = strings.Split(vmess.Alpn, ",") - } else { - alpn = nil - } - tls := model.OutboundTLSOptions{ - Enabled: true, - UTLS: &model.OutboundUTLSOptions{ - Fingerprint: vmess.Fp, - }, - ALPN: alpn, - } - result.VMess.TLS = &tls - } - - if vmess.Net == "ws" { - if vmess.Path == "" { - vmess.Path = "/" - } - if vmess.Host == "" { - vmess.Host = vmess.Add - } - ws := model.V2RayWebsocketOptions{ - Path: vmess.Path, - Headers: map[string]string{ - "Host": vmess.Host, - }, - } - transport := model.V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: ws, - } - result.VMess.Transport = &transport - } - - if vmess.Net == "quic" { - quic := model.V2RayQUICOptions{} - transport := model.V2RayTransportOptions{ - Type: "quic", - QUICOptions: quic, - } - result.VMess.Transport = &transport - } - - if vmess.Net == "grpc" { - grpc := model.V2RayGRPCOptions{ - ServiceName: vmess.Path, - PermitWithoutStream: true, - } - transport := model.V2RayTransportOptions{ - Type: "grpc", - GRPCOptions: grpc, - } - result.VMess.Transport = &transport - } - - if vmess.Net == "h2" { - httpOps := model.V2RayHTTPOptions{ - Host: strings.Split(vmess.Host, ","), - Path: vmess.Path, - } - transport := model.V2RayTransportOptions{ - Type: "http", - HTTPOptions: httpOps, - } - result.VMess.Transport = &transport - } - - return result, nil -} diff --git a/templates/tun-without-dns-leaks-country-group.json b/templates/tun-without-dns-leaks-country-group.json new file mode 100644 index 0000000..93ca876 --- /dev/null +++ b/templates/tun-without-dns-leaks-country-group.json @@ -0,0 +1,252 @@ +{ + "log": { + "level": "info", + "timestamp": true + }, + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "https://223.5.5.5/dns-query", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn" + }, + { + "rule_set": "geoip-cn" + } + ], + "server": "google", + "client_subnet": "114.114.114.114" + } + ] + }, + "route": { + "rule_set": [ + { + "tag": "geosite-geolocation-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-geolocation-!cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs", + "download_detour": "节点选择" + }, + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-category-ads-all", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-microsoft", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-microsoft.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-bilibili", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-bilibili.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-bahamut", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-bahamut.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-category-games@cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-games@cn.srs", + "download_detour": "节点选择" + }, + { + "tag": "geosite-category-games", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-games.srs", + "download_detour": "节点选择" + } + ], + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns-out" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": ["geoip-cn", "geosite-geolocation-cn"], + "outbound": "direct" + }, + { + "rule_set": "geosite-category-ads-all", + "outbound": "Ads" + }, + { + "rule_set": "geosite-microsoft", + "outbound": "Microsoft" + }, + { + "rule_set": "geosite-bilibili", + "outbound": "Bilibili" + }, + { + "rule_set": "geosite-category-games@cn", + "outbound": "Games(中国)" + }, + { + "rule_set": "geosite-category-games", + "outbound": "Games(全球)" + }, + { + "rule_set": "geosite-bahamut", + "outbound": "Bahamut" + } + ], + "final": "节点选择", + "auto_detect_interface": true + }, + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "auto_route": true, + "strict_route": false, + "sniff": true, + "sniff_override_destination": false + } + ], + "outbounds": [ + { + "type": "selector", + "tag": "节点选择", + "outbounds": ["", "direct"], + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Ads", + "outbounds": ["direct", "block"], + "default": "block", + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Microsoft", + "outbounds": ["节点选择", "", "direct"], + "default": "节点选择", + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Bilibili", + "outbounds": ["节点选择", "", "direct"], + "default": "direct", + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Games(全球)", + "outbounds": ["节点选择", "", "direct"], + "default": "节点选择", + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Games(中国)", + "outbounds": ["节点选择", "", "direct"], + "default": "direct", + "interrupt_exist_connections": true + }, + { + "type": "selector", + "tag": "Bahamut", + "outbounds": ["节点选择", "", "direct"], + "default": "节点选择", + "interrupt_exist_connections": true + }, + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + }, + { + "type": "dns", + "tag": "dns-out" + } + ], + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced", + "external_controller": "127.0.0.1:9090", + "external_ui": "./ui", + "external_ui_download_detour": "节点选择" + } + } +} diff --git a/test/country_test.go b/test/country_test.go new file mode 100644 index 0000000..a6a5fed --- /dev/null +++ b/test/country_test.go @@ -0,0 +1,11 @@ +package model + +import ( + "log" + "sub2sing-box/model" + "testing" +) + +func TestCountry(t *testing.T) { + log.Println(model.GetContryName("US 节点")) +} diff --git a/internal/util/base64.go b/util/base64.go similarity index 100% rename from internal/util/base64.go rename to util/base64.go diff --git a/internal/util/fetch.go b/util/fetch.go similarity index 100% rename from internal/util/fetch.go rename to util/fetch.go diff --git a/util/marshal.go b/util/marshal.go new file mode 100644 index 0000000..389d8b3 --- /dev/null +++ b/util/marshal.go @@ -0,0 +1,25 @@ +package util + +import "encoding/json" + +func MergeAndMarshal(args ...interface{}) (string, error) { + merged := make(map[string]interface{}) + for _, arg := range args { + jsonBytes, err := json.Marshal(arg) + if err != nil { + return "", err + } + var m map[string]interface{} + if err := json.Unmarshal(jsonBytes, &m); err != nil { + return "", err + } + for k, v := range m { + merged[k] = v + } + } + result, err := json.Marshal(merged) + if err != nil { + return "", err + } + return string(result), nil +}