From 46a4d03b760f1c8c771b8de980919aa88e2c24b1 Mon Sep 17 00:00:00 2001 From: nite07 Date: Tue, 19 Mar 2024 23:35:13 +0800 Subject: [PATCH 1/7] fix: version cmd --- .goreleaser.pre.yaml | 2 +- .goreleaser.yaml | 2 +- Dockerfile | 2 +- cmd/root.go | 4 ---- cmd/version.go | 3 ++- constant/version.go | 3 +++ main.go | 7 ------- 7 files changed, 8 insertions(+), 15 deletions(-) create mode 100644 constant/version.go diff --git a/.goreleaser.pre.yaml b/.goreleaser.pre.yaml index 976e761..2ace5bf 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 e18e55d..7610cfa 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/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/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/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) From 9e0068f4e12a093aa79302e60e2da66e248500e6 Mon Sep 17 00:00:00 2001 From: nite07 Date: Wed, 20 Mar 2024 12:34:06 +0800 Subject: [PATCH 2/7] update: templates --- .../tun-without-dns-leaks-country-group.json | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 templates/tun-without-dns-leaks-country-group.json 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": "节点选择" + } + } +} From eee361dcbf49ae0d594e065e0b98a716f9bb426e Mon Sep 17 00:00:00 2001 From: nite07 Date: Wed, 20 Mar 2024 20:54:23 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/handler/convert.go | 8 +- cmd/convert.go | 5 +- {pkg/util => common}/convert.go | 73 +++++----- {internal/model => model}/config.go | 134 +++++++++--------- {internal/model => model}/country_code_map.go | 0 {internal/model => model}/hysteria.go | 0 {internal/model => model}/hysteria2.go | 0 {internal/model => model}/multiplex.go | 0 {internal/model => model}/proxy.go | 0 {internal/model => model}/shadowsocks.go | 0 {internal/model => model}/sort.go | 0 {internal/model => model}/tls.go | 0 {internal/model => model}/trojan.go | 0 {internal/model => model}/tuic.go | 0 {internal/model => model}/udp_over_tcp.go | 0 {internal/model => model}/v2ray_transport.go | 4 +- {internal/model => model}/vless.go | 0 {internal/model => model}/vmess.go | 0 {pkg/parser => parser}/hysteria.go | 20 +-- {pkg/parser => parser}/hysteria2.go | 20 +-- {pkg/parser => parser}/parsers_map.go | 2 +- {pkg/parser => parser}/shadowsocks.go | 24 ++-- {pkg/parser => parser}/trojan.go | 42 +++--- {pkg/parser => parser}/vless.go | 48 +++---- {pkg/parser => parser}/vmess.go | 42 +++--- {internal/util => util}/base64.go | 0 {internal/util => util}/fetch.go | 0 27 files changed, 210 insertions(+), 212 deletions(-) rename {pkg/util => common}/convert.go (79%) rename {internal/model => model}/config.go (77%) rename {internal/model => model}/country_code_map.go (100%) rename {internal/model => model}/hysteria.go (100%) rename {internal/model => model}/hysteria2.go (100%) rename {internal/model => model}/multiplex.go (100%) rename {internal/model => model}/proxy.go (100%) rename {internal/model => model}/shadowsocks.go (100%) rename {internal/model => model}/sort.go (100%) rename {internal/model => model}/tls.go (100%) rename {internal/model => model}/trojan.go (100%) rename {internal/model => model}/tuic.go (100%) rename {internal/model => model}/udp_over_tcp.go (100%) rename {internal/model => model}/v2ray_transport.go (95%) rename {internal/model => model}/vless.go (100%) rename {internal/model => model}/vmess.go (100%) rename {pkg/parser => parser}/hysteria.go (81%) rename {pkg/parser => parser}/hysteria2.go (73%) rename {pkg/parser => parser}/parsers_map.go (92%) rename {pkg/parser => parser}/shadowsocks.go (65%) rename {pkg/parser => parser}/trojan.go (68%) rename {pkg/parser => parser}/vless.go (69%) rename {pkg/parser => parser}/vmess.go (69%) rename {internal/util => util}/base64.go (100%) rename {internal/util => util}/fetch.go (100%) diff --git a/api/handler/convert.go b/api/handler/convert.go index 15020c3..3e6dac3 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" + putil "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 := putil.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..82d8942 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -2,10 +2,9 @@ package cmd import ( "fmt" - "os" - . "sub2sing-box/pkg/util" - "github.com/spf13/cobra" + "os" + . "sub2sing-box/common" ) var subscriptions []string diff --git a/pkg/util/convert.go b/common/convert.go similarity index 79% rename from pkg/util/convert.go rename to common/convert.go index 0f8db5e..f1e4574 100644 --- a/pkg/util/convert.go +++ b/common/convert.go @@ -1,4 +1,4 @@ -package util +package common import ( "encoding/json" @@ -9,10 +9,9 @@ import ( "regexp" "sort" "strings" - - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" - "sub2sing-box/pkg/parser" + model2 "sub2sing-box/model" + "sub2sing-box/parser" + "sub2sing-box/util" ) func Convert( @@ -57,14 +56,14 @@ func Convert( keep := make(map[int]bool) set := make(map[string]struct { - Proxy model.Proxy + Proxy model2.Proxy Count int }) for i, p := range proxyList { if _, exists := set[p.Tag]; !exists { keep[i] = true set[p.Tag] = struct { - Proxy model.Proxy + Proxy model2.Proxy Count int }{p, 0} } else { @@ -72,7 +71,7 @@ func Convert( p2, _ := json.Marshal(set[p.Tag]) if string(p1) != string(p2) { set[p.Tag] = struct { - Proxy model.Proxy + Proxy model2.Proxy Count int }{p, set[p.Tag].Count + 1} keep[i] = true @@ -82,14 +81,14 @@ func Convert( } } } - var newProxyList []model.Proxy + var newProxyList []model2.Proxy for i, p := range proxyList { if keep[i] { newProxyList = append(newProxyList, p) } } proxyList = newProxyList - var outbounds []model.Outbound + var outbounds []model2.Outbound ps, err := json.Marshal(&proxyList) if err != nil { return "", err @@ -117,16 +116,16 @@ func Convert( return string(result), nil } -func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound { - newGroup := make(map[string]model.Outbound) +func AddCountryGroup(proxies []model2.Outbound, groupType string, sortKey string, sortType string) []model2.Outbound { + newGroup := make(map[string]model2.Outbound) for _, p := range proxies { if p.Type != "selector" && p.Type != "urltest" { - country := model.GetContryName(p.Tag) + country := model2.GetContryName(p.Tag) if group, ok := newGroup[country]; ok { group.Outbounds = append(group.Outbounds, p.Tag) newGroup[country] = group } else { - newGroup[country] = model.Outbound{ + newGroup[country] = model2.Outbound{ Tag: country, Type: groupType, Outbounds: []string{p.Tag}, @@ -135,34 +134,34 @@ func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, } } } - var groups []model.Outbound + var groups []model2.Outbound for _, p := range newGroup { groups = append(groups, p) } if sortType == "asc" { switch sortKey { case "tag": - sort.Sort(model.SortByTag(groups)) + sort.Sort(model2.SortByTag(groups)) case "num": - sort.Sort(model.SortByNumber(groups)) + sort.Sort(model2.SortByNumber(groups)) default: - sort.Sort(model.SortByTag(groups)) + sort.Sort(model2.SortByTag(groups)) } } else { switch sortKey { case "tag": - sort.Sort(sort.Reverse(model.SortByTag(groups))) + sort.Sort(sort.Reverse(model2.SortByTag(groups))) case "num": - sort.Sort(sort.Reverse(model.SortByNumber(groups))) + sort.Sort(sort.Reverse(model2.SortByNumber(groups))) default: - sort.Sort(sort.Reverse(model.SortByTag(groups))) + sort.Sort(sort.Reverse(model2.SortByTag(groups))) } } return append(proxies, groups...) } -func MergeTemplate(outbounds []model.Outbound, template string) (string, error) { - var config model.Config +func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) { + var config model2.Config var err error if strings.HasPrefix(template, "http") { data, err := util.Fetch(template, 3) @@ -183,12 +182,12 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error) } proxyTags := make([]string, 0) groupTags := make([]string, 0) - groups := make(map[string]model.Outbound) + groups := make(map[string]model2.Outbound) if err != nil { return "", err } for _, p := range outbounds { - if model.IsCountryGroup(p.Tag) { + if model2.IsCountryGroup(p.Tag) { groupTags = append(groupTags, p.Tag) reg := regexp.MustCompile("[A-Za-z]{2}") country := reg.FindString(p.Tag) @@ -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) (model2.Proxy, error) { for prefix, parseFunc := range parser.ParserMap { if strings.HasPrefix(proxy, prefix) { proxy, err := parseFunc(proxy) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } return proxy, nil } } - return model.Proxy{}, errors.New("Unknown proxy format") + return model2.Proxy{}, 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) ([]model2.Proxy, error) { + proxyList := make([]model2.Proxy, 0) for _, url := range urls { data, err := util.Fetch(url, 3) if err != nil { @@ -288,25 +287,25 @@ func ConvertSubscriptionsToJson(urls []string) (string, error) { return string(result), nil } -func ReadTemplate(path string) (model.Config, error) { +func ReadTemplate(path string) (model2.Config, error) { data, err := os.ReadFile(path) if err != nil { - return model.Config{}, err + return model2.Config{}, err } - var res model.Config + var res model2.Config err = json.Unmarshal(data, &res) if err != nil { - return model.Config{}, err + return model2.Config{}, err } return res, nil } -func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) { +func DeleteProxy(proxies []model2.Proxy, regex string) ([]model2.Proxy, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err } - var newProxies []model.Proxy + var newProxies []model2.Proxy 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 []model2.Proxy, regex string, replaceText string) ([]model2.Proxy, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err diff --git a/internal/model/config.go b/model/config.go similarity index 77% rename from internal/model/config.go rename to model/config.go index fdf0e2c..b587140 100644 --- a/internal/model/config.go +++ b/model/config.go @@ -65,12 +65,12 @@ type LogOptions struct { } 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"` + 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"` @@ -162,29 +162,29 @@ type Inbound struct { 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"` + 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"` + 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"` @@ -227,53 +227,53 @@ type Outbound struct { 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"` + 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"` + 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"` + 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"` + 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"` + 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"` + 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"` + 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"` @@ -284,9 +284,9 @@ type Outbound struct { 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"` + 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"` @@ -303,11 +303,11 @@ type WireGuardPeer struct { } 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"` + 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"` 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/internal/model/hysteria.go b/model/hysteria.go similarity index 100% rename from internal/model/hysteria.go rename to model/hysteria.go diff --git a/internal/model/hysteria2.go b/model/hysteria2.go similarity index 100% rename from internal/model/hysteria2.go rename to model/hysteria2.go 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/internal/model/proxy.go b/model/proxy.go similarity index 100% rename from internal/model/proxy.go rename to model/proxy.go diff --git a/internal/model/shadowsocks.go b/model/shadowsocks.go similarity index 100% rename from internal/model/shadowsocks.go rename to model/shadowsocks.go diff --git a/internal/model/sort.go b/model/sort.go similarity index 100% rename from internal/model/sort.go rename to model/sort.go diff --git a/internal/model/tls.go b/model/tls.go similarity index 100% rename from internal/model/tls.go rename to model/tls.go diff --git a/internal/model/trojan.go b/model/trojan.go similarity index 100% rename from internal/model/trojan.go rename to model/trojan.go diff --git a/internal/model/tuic.go b/model/tuic.go similarity index 100% rename from internal/model/tuic.go rename to model/tuic.go 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 95% rename from internal/model/v2ray_transport.go rename to model/v2ray_transport.go index 46ac7a6..945014e 100644 --- a/internal/model/v2ray_transport.go +++ b/model/v2ray_transport.go @@ -53,8 +53,8 @@ func (o *V2RayTransportOptions) MarshalJSON() ([]byte, error) { } type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` + Host Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` Method string `json:"method,omitempty"` Headers map[string]string `json:"headers,omitempty"` IdleTimeout string `json:"idle_timeout,omitempty"` diff --git a/internal/model/vless.go b/model/vless.go similarity index 100% rename from internal/model/vless.go rename to model/vless.go diff --git a/internal/model/vmess.go b/model/vmess.go similarity index 100% rename from internal/model/vmess.go rename to model/vmess.go diff --git a/pkg/parser/hysteria.go b/parser/hysteria.go similarity index 81% rename from pkg/parser/hysteria.go rename to parser/hysteria.go index bcfebfe..ab1fbb9 100644 --- a/pkg/parser/hysteria.go +++ b/parser/hysteria.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" + model2 "sub2sing-box/model" ) //hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks @@ -23,23 +23,23 @@ import ( //- obfsParam: Obfuscation password (optional) //- remarks: remarks (optional) -func ParseHysteria(proxy string) (model.Proxy, error) { +func ParseHysteria(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "hysteria://") { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model2.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") + return model2.Proxy{}, errors.New("invalid hysteria Url") } params, err := url.ParseQuery(parts[1]) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model2.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") + return model2.Proxy{}, errors.New("invalid hysteria Url") } protocol := params.Get("protocol") auth := params.Get("auth") @@ -64,12 +64,12 @@ func ParseHysteria(proxy string) (model.Proxy, error) { } insecureBool, err := strconv.ParseBool(insecure) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria Url") + return model2.Proxy{}, errors.New("invalid hysteria Url") } - result := model.Proxy{ + result := model2.Proxy{ Type: "hysteria", Tag: remarks, - Hysteria: model.Hysteria{ + Hysteria: model2.Hysteria{ Server: host, ServerPort: uint16(port), Up: upmbps, @@ -77,7 +77,7 @@ func ParseHysteria(proxy string) (model.Proxy, error) { Auth: []byte(auth), Obfs: obfs, Network: protocol, - TLS: &model.OutboundTLSOptions{ + TLS: &model2.OutboundTLSOptions{ Enabled: true, Insecure: insecureBool, ALPN: alpn, diff --git a/pkg/parser/hysteria2.go b/parser/hysteria2.go similarity index 73% rename from pkg/parser/hysteria2.go rename to parser/hysteria2.go index c1ce410..7d1cc45 100644 --- a/pkg/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -5,14 +5,14 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" + model2 "sub2sing-box/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) { +func ParseHysteria2(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model.Proxy{}, errors.New("invalid hysteria2 Url") + return model2.Proxy{}, errors.New("invalid hysteria2 Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) serverInfo := strings.SplitN(parts[1], "/?", 2) @@ -20,32 +20,32 @@ func ParseHysteria2(proxy string) (model.Proxy, error) { if len(serverAndPort) == 1 { serverAndPort = append(serverAndPort, "443") } else if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid hysteria2 Url") + return model2.Proxy{}, errors.New("invalid hysteria2 Url") } params, err := url.ParseQuery(serverInfo[1]) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") + return model2.Proxy{}, errors.New("invalid hysteria2 Url") } port, err := strconv.Atoi(serverAndPort[1]) if err != nil { - return model.Proxy{}, errors.New("invalid hysteria2 Url") + return model2.Proxy{}, errors.New("invalid hysteria2 Url") } remarks := params.Get("name") server := serverAndPort[0] password := parts[0] network := params.Get("network") - result := model.Proxy{ + result := model2.Proxy{ Type: "hysteria2", Tag: remarks, - Hysteria2: model.Hysteria2{ + Hysteria2: model2.Hysteria2{ Server: server, ServerPort: uint16(port), Password: password, - Obfs: &model.Hysteria2Obfs{ + Obfs: &model2.Hysteria2Obfs{ Type: params.Get("obfs"), Password: params.Get("obfs-password"), }, - TLS: &model.OutboundTLSOptions{ + TLS: &model2.OutboundTLSOptions{ Enabled: params.Get("pinSHA256") != "", Insecure: params.Get("insecure") == "1", ServerName: params.Get("sni"), diff --git a/pkg/parser/parsers_map.go b/parser/parsers_map.go similarity index 92% rename from pkg/parser/parsers_map.go rename to parser/parsers_map.go index 06082c1..8fb615f 100644 --- a/pkg/parser/parsers_map.go +++ b/parser/parsers_map.go @@ -1,7 +1,7 @@ package parser import ( - "sub2sing-box/internal/model" + "sub2sing-box/model" ) var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ diff --git a/pkg/parser/shadowsocks.go b/parser/shadowsocks.go similarity index 65% rename from pkg/parser/shadowsocks.go rename to parser/shadowsocks.go index 7b28579..0c2f184 100644 --- a/pkg/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -5,43 +5,43 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" + model2 "sub2sing-box/model" + "sub2sing-box/util" ) -func ParseShadowsocks(proxy string) (model.Proxy, error) { +func ParseShadowsocks(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, errors.New("invalid ss Url") + return model2.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") + return model2.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()) + return model2.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") + return model2.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") + return model2.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()) + return model2.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()) + return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) } remarks = strings.TrimSpace(unescape) } else { @@ -50,10 +50,10 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { method := credentials[0] password := credentials[1] server := strings.TrimSpace(serverAndPort[0]) - result := model.Proxy{ + result := model2.Proxy{ Type: "shadowsocks", Tag: remarks, - Shadowsocks: model.Shadowsocks{ + Shadowsocks: model2.Shadowsocks{ Method: method, Password: password, Server: server, diff --git a/pkg/parser/trojan.go b/parser/trojan.go similarity index 68% rename from pkg/parser/trojan.go rename to parser/trojan.go index dd1ab3b..4d8b896 100644 --- a/pkg/parser/trojan.go +++ b/parser/trojan.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" + model2 "sub2sing-box/model" ) -func ParseTrojan(proxy string) (model.Proxy, error) { +func ParseTrojan(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "trojan://") { - return model.Proxy{}, errors.New("invalid trojan Url") + return model2.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") + return model2.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 + return model2.Proxy{}, err } if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid trojan Url") + return model2.Proxy{}, errors.New("invalid trojan Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } remarks := "" if len(serverInfo) == 2 { @@ -38,10 +38,10 @@ func ParseTrojan(proxy string) (model.Proxy, error) { } server := strings.TrimSpace(serverAndPort[0]) password := strings.TrimSpace(parts[0]) - result := model.Proxy{ + result := model2.Proxy{ Type: "trojan", Tag: remarks, - Trojan: model.Trojan{ + Trojan: model2.Trojan{ Server: server, ServerPort: uint16(port), Password: password, @@ -55,31 +55,31 @@ func ParseTrojan(proxy string) (model.Proxy, error) { } else { alpn = nil } - result.Trojan.TLS = &model.OutboundTLSOptions{ + result.Trojan.TLS = &model2.OutboundTLSOptions{ Enabled: true, ALPN: alpn, ServerName: params.Get("sni"), } } if params.Get("security") == "reality" { - result.Trojan.TLS = &model.OutboundTLSOptions{ + result.Trojan.TLS = &model2.OutboundTLSOptions{ Enabled: true, ServerName: params.Get("sni"), - Reality: &model.OutboundRealityOptions{ + Reality: &model2.OutboundRealityOptions{ Enabled: true, PublicKey: params.Get("pbk"), ShortID: params.Get("sid"), }, - UTLS: &model.OutboundUTLSOptions{ + UTLS: &model2.OutboundUTLSOptions{ Enabled: params.Get("fp") != "", Fingerprint: params.Get("fp"), }, } } if params.Get("type") == "ws" { - result.Trojan.Transport = &model.V2RayTransportOptions{ + result.Trojan.Transport = &model2.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model.V2RayWebsocketOptions{ + WebsocketOptions: model2.V2RayWebsocketOptions{ Path: params.Get("path"), Headers: map[string]string{ "Host": params.Get("host"), @@ -88,24 +88,24 @@ func ParseTrojan(proxy string) (model.Proxy, error) { } } if params.Get("type") == "http" { - result.Trojan.Transport = &model.V2RayTransportOptions{ + result.Trojan.Transport = &model2.V2RayTransportOptions{ Type: "http", - HTTPOptions: model.V2RayHTTPOptions{ + HTTPOptions: model2.V2RayHTTPOptions{ Host: []string{params.Get("host")}, Path: params.Get("path"), }, } } if params.Get("type") == "quic" { - result.Trojan.Transport = &model.V2RayTransportOptions{ + result.Trojan.Transport = &model2.V2RayTransportOptions{ Type: "quic", - QUICOptions: model.V2RayQUICOptions{}, + QUICOptions: model2.V2RayQUICOptions{}, } } if params.Get("type") == "grpc" { - result.Trojan.Transport = &model.V2RayTransportOptions{ + result.Trojan.Transport = &model2.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model.V2RayGRPCOptions{ + GRPCOptions: model2.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } diff --git a/pkg/parser/vless.go b/parser/vless.go similarity index 69% rename from pkg/parser/vless.go rename to parser/vless.go index a7767dc..4ac80b5 100644 --- a/pkg/parser/vless.go +++ b/parser/vless.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" + model2 "sub2sing-box/model" ) -func ParseVless(proxy string) (model.Proxy, error) { +func ParseVless(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "vless://") { - return model.Proxy{}, errors.New("invalid vless Url") + return model2.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") + return model2.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 + return model2.Proxy{}, err } if len(serverAndPort) != 2 { - return model.Proxy{}, errors.New("invalid vless Url") + return model2.Proxy{}, errors.New("invalid vless Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } remarks := "" if len(serverInfo) == 2 { @@ -37,21 +37,21 @@ func ParseVless(proxy string) (model.Proxy, error) { } else { remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } } } else { remarks, err = url.QueryUnescape(serverAndPort[0]) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } } server := strings.TrimSpace(serverAndPort[0]) uuid := strings.TrimSpace(parts[0]) - result := model.Proxy{ + result := model2.Proxy{ Type: "vless", Tag: remarks, - VLESS: model.VLESS{ + VLESS: model2.VLESS{ Server: server, ServerPort: uint16(port), UUID: uuid, @@ -65,7 +65,7 @@ func ParseVless(proxy string) (model.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model.OutboundTLSOptions{ + result.VLESS.TLS = &model2.OutboundTLSOptions{ Enabled: true, ALPN: alpn, Insecure: params.Get("allowInsecure") == "1", @@ -78,14 +78,14 @@ func ParseVless(proxy string) (model.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model.OutboundTLSOptions{ + result.VLESS.TLS = &model2.OutboundTLSOptions{ Enabled: true, ServerName: params.Get("sni"), - UTLS: &model.OutboundUTLSOptions{ + UTLS: &model2.OutboundUTLSOptions{ Enabled: params.Get("fp") != "", Fingerprint: params.Get("fp"), }, - Reality: &model.OutboundRealityOptions{ + Reality: &model2.OutboundRealityOptions{ Enabled: true, PublicKey: params.Get("pbk"), ShortID: params.Get("sid"), @@ -94,9 +94,9 @@ func ParseVless(proxy string) (model.Proxy, error) { } } if params.Get("type") == "ws" { - result.VLESS.Transport = &model.V2RayTransportOptions{ + result.VLESS.Transport = &model2.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model.V2RayWebsocketOptions{ + WebsocketOptions: model2.V2RayWebsocketOptions{ Path: params.Get("path"), Headers: map[string]string{ "Host": params.Get("host"), @@ -105,15 +105,15 @@ func ParseVless(proxy string) (model.Proxy, error) { } } if params.Get("type") == "quic" { - result.VLESS.Transport = &model.V2RayTransportOptions{ + result.VLESS.Transport = &model2.V2RayTransportOptions{ Type: "quic", - QUICOptions: model.V2RayQUICOptions{}, + QUICOptions: model2.V2RayQUICOptions{}, } } if params.Get("type") == "grpc" { - result.VLESS.Transport = &model.V2RayTransportOptions{ + result.VLESS.Transport = &model2.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model.V2RayGRPCOptions{ + GRPCOptions: model2.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } @@ -121,11 +121,11 @@ func ParseVless(proxy string) (model.Proxy, error) { if params.Get("type") == "http" { host, err := url.QueryUnescape(params.Get("host")) if err != nil { - return model.Proxy{}, err + return model2.Proxy{}, err } - result.VLESS.Transport = &model.V2RayTransportOptions{ + result.VLESS.Transport = &model2.V2RayTransportOptions{ Type: "http", - HTTPOptions: model.V2RayHTTPOptions{ + HTTPOptions: model2.V2RayHTTPOptions{ Host: strings.Split(host, ","), }, } diff --git a/pkg/parser/vmess.go b/parser/vmess.go similarity index 69% rename from pkg/parser/vmess.go rename to parser/vmess.go index a00ff00..051db02 100644 --- a/pkg/parser/vmess.go +++ b/parser/vmess.go @@ -6,29 +6,29 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/internal/model" - "sub2sing-box/internal/util" + model2 "sub2sing-box/model" + "sub2sing-box/util" ) -func ParseVmess(proxy string) (model.Proxy, error) { +func ParseVmess(proxy string) (model2.Proxy, error) { if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, errors.New("invalid vmess url") + return model2.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()) + return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) } - var vmess model.VmessJson + var vmess model2.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model2.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()) + return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) } case float64: port = int(vmess.Port.(float64)) @@ -38,7 +38,7 @@ func ParseVmess(proxy string) (model.Proxy, error) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) } case float64: aid = int(vmess.Aid.(float64)) @@ -52,10 +52,10 @@ func ParseVmess(proxy string) (model.Proxy, error) { name = vmess.Ps } - result := model.Proxy{ + result := model2.Proxy{ Type: "vmess", Tag: name, - VMess: model.VMess{ + VMess: model2.VMess{ Server: vmess.Add, ServerPort: uint16(port), UUID: vmess.Id, @@ -71,9 +71,9 @@ func ParseVmess(proxy string) (model.Proxy, error) { } else { alpn = nil } - tls := model.OutboundTLSOptions{ + tls := model2.OutboundTLSOptions{ Enabled: true, - UTLS: &model.OutboundUTLSOptions{ + UTLS: &model2.OutboundUTLSOptions{ Fingerprint: vmess.Fp, }, ALPN: alpn, @@ -88,13 +88,13 @@ func ParseVmess(proxy string) (model.Proxy, error) { if vmess.Host == "" { vmess.Host = vmess.Add } - ws := model.V2RayWebsocketOptions{ + ws := model2.V2RayWebsocketOptions{ Path: vmess.Path, Headers: map[string]string{ "Host": vmess.Host, }, } - transport := model.V2RayTransportOptions{ + transport := model2.V2RayTransportOptions{ Type: "ws", WebsocketOptions: ws, } @@ -102,8 +102,8 @@ func ParseVmess(proxy string) (model.Proxy, error) { } if vmess.Net == "quic" { - quic := model.V2RayQUICOptions{} - transport := model.V2RayTransportOptions{ + quic := model2.V2RayQUICOptions{} + transport := model2.V2RayTransportOptions{ Type: "quic", QUICOptions: quic, } @@ -111,11 +111,11 @@ func ParseVmess(proxy string) (model.Proxy, error) { } if vmess.Net == "grpc" { - grpc := model.V2RayGRPCOptions{ + grpc := model2.V2RayGRPCOptions{ ServiceName: vmess.Path, PermitWithoutStream: true, } - transport := model.V2RayTransportOptions{ + transport := model2.V2RayTransportOptions{ Type: "grpc", GRPCOptions: grpc, } @@ -123,11 +123,11 @@ func ParseVmess(proxy string) (model.Proxy, error) { } if vmess.Net == "h2" { - httpOps := model.V2RayHTTPOptions{ + httpOps := model2.V2RayHTTPOptions{ Host: strings.Split(vmess.Host, ","), Path: vmess.Path, } - transport := model.V2RayTransportOptions{ + transport := model2.V2RayTransportOptions{ Type: "http", HTTPOptions: httpOps, } 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 From 91571affdd1ed6e1404a06eb57225d730f4d4243 Mon Sep 17 00:00:00 2001 From: nite07 Date: Wed, 20 Mar 2024 20:58:38 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20vmess/vless=20quic=20=E7=BC=BA?= =?UTF-8?q?=E5=B0=91=20tls=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parser/vless.go | 3 +++ parser/vmess.go | 18 ++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/parser/vless.go b/parser/vless.go index 4ac80b5..9005cf5 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -109,6 +109,9 @@ func ParseVless(proxy string) (model2.Proxy, error) { Type: "quic", QUICOptions: model2.V2RayQUICOptions{}, } + result.VLESS.TLS = &model2.OutboundTLSOptions{ + Enabled: true, + } } if params.Get("type") == "grpc" { result.VLESS.Transport = &model2.V2RayTransportOptions{ diff --git a/parser/vmess.go b/parser/vmess.go index 051db02..3288118 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -71,14 +71,13 @@ func ParseVmess(proxy string) (model2.Proxy, error) { } else { alpn = nil } - tls := model2.OutboundTLSOptions{ + result.VMess.TLS = &model2.OutboundTLSOptions{ Enabled: true, UTLS: &model2.OutboundUTLSOptions{ Fingerprint: vmess.Fp, }, ALPN: alpn, } - result.VMess.TLS = &tls } if vmess.Net == "ws" { @@ -94,20 +93,21 @@ func ParseVmess(proxy string) (model2.Proxy, error) { "Host": vmess.Host, }, } - transport := model2.V2RayTransportOptions{ + result.VMess.Transport = &model2.V2RayTransportOptions{ Type: "ws", WebsocketOptions: ws, } - result.VMess.Transport = &transport } if vmess.Net == "quic" { quic := model2.V2RayQUICOptions{} - transport := model2.V2RayTransportOptions{ + result.VMess.Transport = &model2.V2RayTransportOptions{ Type: "quic", QUICOptions: quic, } - result.VMess.Transport = &transport + result.VMess.TLS = &model2.OutboundTLSOptions{ + Enabled: true, + } } if vmess.Net == "grpc" { @@ -115,11 +115,10 @@ func ParseVmess(proxy string) (model2.Proxy, error) { ServiceName: vmess.Path, PermitWithoutStream: true, } - transport := model2.V2RayTransportOptions{ + result.VMess.Transport = &model2.V2RayTransportOptions{ Type: "grpc", GRPCOptions: grpc, } - result.VMess.Transport = &transport } if vmess.Net == "h2" { @@ -127,11 +126,10 @@ func ParseVmess(proxy string) (model2.Proxy, error) { Host: strings.Split(vmess.Host, ","), Path: vmess.Path, } - transport := model2.V2RayTransportOptions{ + result.VMess.Transport = &model2.V2RayTransportOptions{ Type: "http", HTTPOptions: httpOps, } - result.VMess.Transport = &transport } return result, nil From 688f8287afa53abfeecabfafb6ce2f991378a909 Mon Sep 17 00:00:00 2001 From: nite07 Date: Thu, 21 Mar 2024 00:02:38 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=20fix:=20vless=20ws=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=BC=BA=E5=B0=91=20path=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + common/convert.go | 150 +++++++++++++++++----------------- constant/proxy.go | 31 +++++++ go.mod | 6 +- model/config.go | 169 ++++++++------------------------------- model/direct.go | 8 ++ model/group.go | 16 ++++ model/hysteria.go | 30 +++---- model/hysteria2.go | 20 ++--- model/outbound.go | 167 ++++++++++++++++++++++++++++++++++++++ model/proxy.go | 94 ---------------------- model/shadowsocks.go | 6 +- model/shadowsocksr.go | 13 +++ model/shadowtls.go | 19 +++++ model/simple.go | 21 +++++ model/sort.go | 23 +++++- model/ssh.go | 14 ++++ model/tls.go | 4 + model/tor.go | 9 +++ model/trojan.go | 16 ++-- model/tuic.go | 24 +++--- model/v2ray_transport.go | 4 +- model/vless.go | 14 ++-- model/vmess.go | 26 +++--- model/wireguard.go | 28 +++++++ parser/hysteria.go | 44 +++++----- parser/hysteria2.go | 36 +++++---- parser/parsers_map.go | 2 +- parser/shadowsocks.go | 32 ++++---- parser/trojan.go | 78 +++++++++--------- parser/vless.go | 98 ++++++++++++----------- parser/vmess.go | 77 +++++++++--------- util/marshal.go | 25 ++++++ 33 files changed, 757 insertions(+), 548 deletions(-) create mode 100644 constant/proxy.go create mode 100644 model/direct.go create mode 100644 model/group.go create mode 100644 model/outbound.go delete mode 100644 model/proxy.go create mode 100644 model/shadowsocksr.go create mode 100644 model/shadowtls.go create mode 100644 model/simple.go create mode 100644 model/ssh.go create mode 100644 model/tor.go create mode 100644 model/wireguard.go create mode 100644 util/marshal.go diff --git a/.gitignore b/.gitignore index 16db01c..8e47708 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist *test.go template.json +.idea \ No newline at end of file diff --git a/common/convert.go b/common/convert.go index f1e4574..e975560 100644 --- a/common/convert.go +++ b/common/convert.go @@ -9,7 +9,8 @@ import ( "regexp" "sort" "strings" - model2 "sub2sing-box/model" + C "sub2sing-box/constant" + "sub2sing-box/model" "sub2sing-box/parser" "sub2sing-box/util" ) @@ -28,7 +29,7 @@ func Convert( result := "" var err error - proxyList, err := ConvertSubscriptionsToSProxy(subscriptions) + outbounds, err := ConvertSubscriptionsToSProxy(subscriptions) if err != nil { return "", err } @@ -37,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 } @@ -56,14 +57,14 @@ func Convert( keep := make(map[int]bool) set := make(map[string]struct { - Proxy model2.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 model2.Proxy + Proxy model.Outbound Count int }{p, 0} } else { @@ -71,32 +72,16 @@ func Convert( p2, _ := json.Marshal(set[p.Tag]) if string(p1) != string(p2) { set[p.Tag] = struct { - Proxy model2.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 []model2.Proxy - for i, p := range proxyList { - if keep[i] { - newProxyList = append(newProxyList, p) - } - } - proxyList = newProxyList - var outbounds []model2.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) } @@ -116,52 +101,65 @@ func Convert( return string(result), nil } -func AddCountryGroup(proxies []model2.Outbound, groupType string, sortKey string, sortType string) []model2.Outbound { - newGroup := make(map[string]model2.Outbound) +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" { - country := model2.GetContryName(p.Tag) + 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] = model2.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, + }, + } } } } } - var groups []model2.Outbound + var groups []model.Outbound for _, p := range newGroup { groups = append(groups, p) } if sortType == "asc" { switch sortKey { case "tag": - sort.Sort(model2.SortByTag(groups)) + sort.Sort(model.SortByTag(groups)) case "num": - sort.Sort(model2.SortByNumber(groups)) + sort.Sort(model.SortByNumber(groups)) default: - sort.Sort(model2.SortByTag(groups)) + sort.Sort(model.SortByTag(groups)) } } else { switch sortKey { case "tag": - sort.Sort(sort.Reverse(model2.SortByTag(groups))) + sort.Sort(sort.Reverse(model.SortByTag(groups))) case "num": - sort.Sort(sort.Reverse(model2.SortByNumber(groups))) + sort.Sort(sort.Reverse(model.SortByNumber(groups))) default: - sort.Sort(sort.Reverse(model2.SortByTag(groups))) + sort.Sort(sort.Reverse(model.SortByTag(groups))) } } return append(proxies, groups...) } -func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) { - var config model2.Config +func MergeTemplate(outbounds []model.Outbound, template string) (string, error) { + var config model.Config var err error if strings.HasPrefix(template, "http") { data, err := util.Fetch(template, 3) @@ -179,15 +177,15 @@ func MergeTemplate(outbounds []model2.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]model2.Outbound) - if err != nil { - return "", err - } + groups := make(map[string]model.Outbound) for _, p := range outbounds { - if model2.IsCountryGroup(p.Tag) { + if model.IsCountryGroup(p.Tag) { groupTags = append(groupTags, p.Tag) reg := regexp.MustCompile("[A-Za-z]{2}") country := reg.FindString(p.Tag) @@ -198,22 +196,24 @@ func MergeTemplate(outbounds []model2.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) @@ -223,17 +223,17 @@ func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) return string(data), nil } -func ConvertCProxyToSProxy(proxy string) (model2.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 model2.Proxy{}, err + return model.Outbound{}, err } return proxy, nil } } - return model2.Proxy{}, errors.New("Unknown proxy format") + return model.Outbound{}, errors.New("Unknown proxy format") } func ConvertCProxyToJson(proxy string) (string, error) { @@ -248,8 +248,8 @@ func ConvertCProxyToJson(proxy string) (string, error) { return string(data), nil } -func ConvertSubscriptionsToSProxy(urls []string) ([]model2.Proxy, error) { - proxyList := make([]model2.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 { @@ -287,25 +287,25 @@ func ConvertSubscriptionsToJson(urls []string) (string, error) { return string(result), nil } -func ReadTemplate(path string) (model2.Config, error) { +func ReadTemplate(path string) (model.Config, error) { data, err := os.ReadFile(path) if err != nil { - return model2.Config{}, err + return model.Config{}, err } - var res model2.Config + var res model.Config err = json.Unmarshal(data, &res) if err != nil { - return model2.Config{}, err + return model.Config{}, err } return res, nil } -func DeleteProxy(proxies []model2.Proxy, regex string) ([]model2.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 []model2.Proxy + var newProxies []model.Outbound for _, p := range proxies { if !reg.MatchString(p.Tag) { newProxies = append(newProxies, p) @@ -314,7 +314,7 @@ func DeleteProxy(proxies []model2.Proxy, regex string) ([]model2.Proxy, error) { return newProxies, nil } -func RenameProxy(proxies []model2.Proxy, regex string, replaceText string) ([]model2.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/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/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/model/config.go b/model/config.go index b587140..052c607 100644 --- a/model/config.go +++ b/model/config.go @@ -51,8 +51,8 @@ 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"` + Inbounds []Inbound `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` Route *RouteOptions `json:"route,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"` } @@ -65,12 +65,12 @@ type LogOptions struct { } 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"` + 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"` @@ -162,29 +162,29 @@ type Inbound struct { 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"` + 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"` + 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"` @@ -203,111 +203,12 @@ type HTTPProxyOptions struct { 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"` + 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"` 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 index bf299af..91d0e73 100644 --- a/model/hysteria.go +++ b/model/hysteria.go @@ -1,18 +1,18 @@ 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"` +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 index 066a341..5a2f117 100644 --- a/model/hysteria2.go +++ b/model/hysteria2.go @@ -5,14 +5,14 @@ type Hysteria2Obfs struct { 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"` +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/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/model/proxy.go b/model/proxy.go deleted file mode 100644 index 2c76e55..0000000 --- a/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/model/shadowsocks.go b/model/shadowsocks.go index 5cdc8b1..038de5e 100644 --- a/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 index 737644d..e80e8eb 100644 --- a/model/sort.go +++ b/model/sort.go @@ -1,15 +1,32 @@ 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 { return len(a[i].Outbounds) < len(a[j].Outbounds) } +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 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/model/tls.go b/model/tls.go index 6542944..64426de 100644 --- a/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 index 5e827fa..a1176c0 100644 --- a/model/trojan.go +++ b/model/trojan.go @@ -1,11 +1,11 @@ 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"` +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 index 304c57d..6cf3c91 100644 --- a/model/tuic.go +++ b/model/tuic.go @@ -1,15 +1,15 @@ 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"` +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/model/v2ray_transport.go b/model/v2ray_transport.go index 945014e..46ac7a6 100644 --- a/model/v2ray_transport.go +++ b/model/v2ray_transport.go @@ -53,8 +53,8 @@ func (o *V2RayTransportOptions) MarshalJSON() ([]byte, error) { } type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` + Host Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` Method string `json:"method,omitempty"` Headers map[string]string `json:"headers,omitempty"` IdleTimeout string `json:"idle_timeout,omitempty"` diff --git a/model/vless.go b/model/vless.go index 74e355e..d656087 100644 --- a/model/vless.go +++ b/model/vless.go @@ -1,12 +1,12 @@ 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"` +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 index 472bbfb..2063987 100644 --- a/model/vmess.go +++ b/model/vmess.go @@ -18,17 +18,17 @@ type VmessJson struct { 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"` +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/hysteria.go b/parser/hysteria.go index ab1fbb9..854e910 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) //hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks @@ -23,23 +23,23 @@ import ( //- obfsParam: Obfuscation password (optional) //- remarks: remarks (optional) -func ParseHysteria(proxy string) (model2.Proxy, error) { +func ParseHysteria(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "hysteria://") { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) serverInfo := strings.SplitN(parts[0], ":", 2) if len(serverInfo) != 2 { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } params, err := url.ParseQuery(parts[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } host := serverInfo[0] port, err := strconv.Atoi(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } protocol := params.Get("protocol") auth := params.Get("auth") @@ -64,23 +64,27 @@ func ParseHysteria(proxy string) (model2.Proxy, error) { } insecureBool, err := strconv.ParseBool(insecure) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } - result := model2.Proxy{ + result := model.Outbound{ Type: "hysteria", Tag: remarks, - Hysteria: model2.Hysteria{ - Server: host, - ServerPort: uint16(port), - Up: upmbps, - Down: downmbps, - Auth: []byte(auth), - Obfs: obfs, - Network: protocol, - TLS: &model2.OutboundTLSOptions{ - Enabled: true, - Insecure: insecureBool, - ALPN: alpn, + HysteriaOptions: model.HysteriaOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: host, + ServerPort: uint16(port), + }, + Up: upmbps, + Down: downmbps, + Auth: []byte(auth), + Obfs: obfs, + Network: protocol, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + Insecure: insecureBool, + ALPN: alpn, + }, }, }, } diff --git a/parser/hysteria2.go b/parser/hysteria2.go index 7d1cc45..88b97c0 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -5,14 +5,14 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) // hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com -func ParseHysteria2(proxy string) (model2.Proxy, error) { +func ParseHysteria2(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) serverInfo := strings.SplitN(parts[1], "/?", 2) @@ -20,36 +20,38 @@ func ParseHysteria2(proxy string) (model2.Proxy, error) { if len(serverAndPort) == 1 { serverAndPort = append(serverAndPort, "443") } else if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } params, err := url.ParseQuery(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } port, err := strconv.Atoi(serverAndPort[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } remarks := params.Get("name") server := serverAndPort[0] password := parts[0] network := params.Get("network") - result := model2.Proxy{ + result := model.Outbound{ Type: "hysteria2", Tag: remarks, - Hysteria2: model2.Hysteria2{ - Server: server, - ServerPort: uint16(port), - Password: password, - Obfs: &model2.Hysteria2Obfs{ + Hysteria2Options: model.Hysteria2OutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Password: password, + Obfs: &model.Hysteria2Obfs{ Type: params.Get("obfs"), Password: params.Get("obfs-password"), }, - TLS: &model2.OutboundTLSOptions{ - Enabled: params.Get("pinSHA256") != "", - Insecure: params.Get("insecure") == "1", - ServerName: params.Get("sni"), - Certificate: []string{params.Get("pinSHA256")}, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{Enabled: params.Get("pinSHA256") != "", + Insecure: params.Get("insecure") == "1", + ServerName: params.Get("sni"), + Certificate: []string{params.Get("pinSHA256")}}, }, Network: network, }, diff --git a/parser/parsers_map.go b/parser/parsers_map.go index 8fb615f..6837179 100644 --- a/parser/parsers_map.go +++ b/parser/parsers_map.go @@ -4,7 +4,7 @@ import ( "sub2sing-box/model" ) -var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ +var ParserMap map[string]func(string) (model.Outbound, error) = map[string]func(string) (model.Outbound, error){ "ss://": ParseShadowsocks, "vmess://": ParseVmess, "trojan://": ParseTrojan, diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index 0c2f184..2ce68d9 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -5,43 +5,43 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" "sub2sing-box/util" ) -func ParseShadowsocks(proxy string) (model2.Proxy, error) { +func ParseShadowsocks(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "ss://") { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } if !strings.Contains(parts[0], ":") { decoded, err := util.DecodeBase64(parts[0]) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } parts[0] = decoded } credentials := strings.SplitN(parts[0], ":", 2) if len(credentials) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPort := strings.SplitN(serverInfo[0], ":", 2) if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } remarks := "" if len(serverInfo) == 2 { unescape, err := url.QueryUnescape(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } remarks = strings.TrimSpace(unescape) } else { @@ -50,14 +50,16 @@ func ParseShadowsocks(proxy string) (model2.Proxy, error) { method := credentials[0] password := credentials[1] server := strings.TrimSpace(serverAndPort[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "shadowsocks", Tag: remarks, - Shadowsocks: model2.Shadowsocks{ - Method: method, - Password: password, - Server: server, - ServerPort: uint16(port), + ShadowsocksOptions: model.ShadowsocksOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Method: method, + Password: password, }, } return result, nil diff --git a/parser/trojan.go b/parser/trojan.go index 4d8b896..d838dd2 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) -func ParseTrojan(proxy string) (model2.Proxy, error) { +func ParseTrojan(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "trojan://") { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, errors.New("invalid trojan Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, 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 model2.Proxy{}, err + return model.Outbound{}, err } if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, errors.New("invalid trojan Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } remarks := "" if len(serverInfo) == 2 { @@ -38,14 +38,16 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } server := strings.TrimSpace(serverAndPort[0]) password := strings.TrimSpace(parts[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "trojan", Tag: remarks, - Trojan: model2.Trojan{ - Server: server, - ServerPort: uint16(port), - Password: password, - Network: params.Get("type"), + TrojanOptions: model.TrojanOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Password: password, + Network: params.Get("type"), }, } if params.Get("security") == "xtls" || params.Get("security") == "tls" { @@ -55,31 +57,35 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.Trojan.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - ServerName: params.Get("sni"), + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + ServerName: params.Get("sni"), + }, } } if params.Get("security") == "reality" { - result.Trojan.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - Reality: &model2.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - UTLS: &model2.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + 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 = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model2.V2RayWebsocketOptions{ + WebsocketOptions: model.V2RayWebsocketOptions{ Path: params.Get("path"), Headers: map[string]string{ "Host": params.Get("host"), @@ -88,24 +94,24 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } } if params.Get("type") == "http" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "http", - HTTPOptions: model2.V2RayHTTPOptions{ + HTTPOptions: model.V2RayHTTPOptions{ Host: []string{params.Get("host")}, Path: params.Get("path"), }, } } if params.Get("type") == "quic" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", - QUICOptions: model2.V2RayQUICOptions{}, + QUICOptions: model.V2RayQUICOptions{}, } } if params.Get("type") == "grpc" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model2.V2RayGRPCOptions{ + GRPCOptions: model.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } diff --git a/parser/vless.go b/parser/vless.go index 9005cf5..38986f2 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) -func ParseVless(proxy string) (model2.Proxy, error) { +func ParseVless(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "vless://") { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, errors.New("invalid vless Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, 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 model2.Proxy{}, err + return model.Outbound{}, err } if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, errors.New("invalid vless Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } remarks := "" if len(serverInfo) == 2 { @@ -37,25 +37,27 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } } } else { remarks, err = url.QueryUnescape(serverAndPort[0]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } } server := strings.TrimSpace(serverAndPort[0]) uuid := strings.TrimSpace(parts[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "vless", Tag: remarks, - VLESS: model2.VLESS{ - Server: server, - ServerPort: uint16(port), - UUID: uuid, - Flow: params.Get("flow"), + VLESSOptions: model.VLESSOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + UUID: uuid, + Flow: params.Get("flow"), }, } if params.Get("security") == "tls" { @@ -65,10 +67,12 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - Insecure: params.Get("allowInsecure") == "1", + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + Insecure: params.Get("allowInsecure") == "1", + }, } } if params.Get("security") == "reality" { @@ -78,45 +82,49 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - UTLS: &model2.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + 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, }, - Reality: &model2.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - ALPN: alpn, } } if params.Get("type") == "ws" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model2.V2RayWebsocketOptions{ + WebsocketOptions: model.V2RayWebsocketOptions{ Path: params.Get("path"), - Headers: map[string]string{ - "Host": params.Get("host"), - }, }, } + if params.Get("host") != "" { + result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = params.Get("host") + } } if params.Get("type") == "quic" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", - QUICOptions: model2.V2RayQUICOptions{}, + QUICOptions: model.V2RayQUICOptions{}, } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + }, } } if params.Get("type") == "grpc" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model2.V2RayGRPCOptions{ + GRPCOptions: model.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } @@ -124,11 +132,11 @@ func ParseVless(proxy string) (model2.Proxy, error) { if params.Get("type") == "http" { host, err := url.QueryUnescape(params.Get("host")) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "http", - HTTPOptions: model2.V2RayHTTPOptions{ + HTTPOptions: model.V2RayHTTPOptions{ Host: strings.Split(host, ","), }, } diff --git a/parser/vmess.go b/parser/vmess.go index 3288118..8bf7b1c 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -6,29 +6,29 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" "sub2sing-box/util" ) -func ParseVmess(proxy string) (model2.Proxy, error) { +func ParseVmess(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "vmess://") { - return model2.Proxy{}, errors.New("invalid vmess url") + return model.Outbound{}, errors.New("invalid vmess url") } base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } - var vmess model2.VmessJson + var vmess model.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, 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 model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } case float64: port = int(vmess.Port.(float64)) @@ -38,7 +38,7 @@ func ParseVmess(proxy string) (model2.Proxy, error) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } case float64: aid = int(vmess.Aid.(float64)) @@ -52,15 +52,17 @@ func ParseVmess(proxy string) (model2.Proxy, error) { name = vmess.Ps } - result := model2.Proxy{ + result := model.Outbound{ Type: "vmess", Tag: name, - VMess: model2.VMess{ - Server: vmess.Add, - ServerPort: uint16(port), - UUID: vmess.Id, - AlterId: aid, - Security: vmess.Scy, + VMessOptions: model.VMessOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: vmess.Add, + ServerPort: uint16(port), + }, + UUID: vmess.Id, + AlterId: aid, + Security: vmess.Scy, }, } @@ -71,12 +73,14 @@ func ParseVmess(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VMess.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - UTLS: &model2.OutboundUTLSOptions{ - Fingerprint: vmess.Fp, + result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + UTLS: &model.OutboundUTLSOptions{ + Fingerprint: vmess.Fp, + }, + ALPN: alpn, }, - ALPN: alpn, } } @@ -87,46 +91,47 @@ func ParseVmess(proxy string) (model2.Proxy, error) { if vmess.Host == "" { vmess.Host = vmess.Add } - ws := model2.V2RayWebsocketOptions{ - Path: vmess.Path, - Headers: map[string]string{ - "Host": vmess.Host, + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: vmess.Path, + Headers: map[string]string{ + "Host": vmess.Host, + }, }, } - result.VMess.Transport = &model2.V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: ws, - } } if vmess.Net == "quic" { - quic := model2.V2RayQUICOptions{} - result.VMess.Transport = &model2.V2RayTransportOptions{ + quic := model.V2RayQUICOptions{} + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", QUICOptions: quic, } - result.VMess.TLS = &model2.OutboundTLSOptions{ - Enabled: true, + result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + }, } } if vmess.Net == "grpc" { - grpc := model2.V2RayGRPCOptions{ + grpc := model.V2RayGRPCOptions{ ServiceName: vmess.Path, PermitWithoutStream: true, } - result.VMess.Transport = &model2.V2RayTransportOptions{ + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", GRPCOptions: grpc, } } if vmess.Net == "h2" { - httpOps := model2.V2RayHTTPOptions{ + httpOps := model.V2RayHTTPOptions{ Host: strings.Split(vmess.Host, ","), Path: vmess.Path, } - result.VMess.Transport = &model2.V2RayTransportOptions{ + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: httpOps, } 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 +} From cb65b2f4536a6e574e523520f522125dba88d22e Mon Sep 17 00:00:00 2001 From: nite07 Date: Thu, 21 Mar 2024 00:59:47 +0800 Subject: [PATCH 6/7] update: vless,vmess parser --- parser/vless.go | 43 ++++++++++++++----------------------------- parser/vmess.go | 14 ++++++++------ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/parser/vless.go b/parser/vless.go index 38986f2..1fc12c6 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -69,34 +69,24 @@ func ParseVless(proxy string) (model.Outbound, error) { } result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - Insecure: params.Get("allowInsecure") == "1", + Enabled: true, + ALPN: alpn, + ServerName: params.Get("sni"), + Insecure: params.Get("allowInsecure") == "1", }, } + if params.Get("fp") != "" { + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: params.Get("fp"), + } + } } if params.Get("security") == "reality" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } - result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ - 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, - }, + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.Reality = &model.OutboundRealityOptions{ + Enabled: true, + PublicKey: params.Get("pbk"), + ShortID: params.Get("sid"), } } if params.Get("type") == "ws" { @@ -115,11 +105,6 @@ func ParseVless(proxy string) (model.Outbound, error) { Type: "quic", QUICOptions: model.V2RayQUICOptions{}, } - result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ - TLS: &model.OutboundTLSOptions{ - Enabled: true, - }, - } } if params.Get("type") == "grpc" { result.VLESSOptions.Transport = &model.V2RayTransportOptions{ diff --git a/parser/vmess.go b/parser/vmess.go index 8bf7b1c..262d871 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -79,9 +79,16 @@ func ParseVmess(proxy string) (model.Outbound, error) { UTLS: &model.OutboundUTLSOptions{ Fingerprint: vmess.Fp, }, - ALPN: alpn, + ALPN: alpn, + ServerName: vmess.Sni, }, } + if vmess.Fp != "" { + result.VMessOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: vmess.Fp, + } + } } if vmess.Net == "ws" { @@ -108,11 +115,6 @@ func ParseVmess(proxy string) (model.Outbound, error) { Type: "quic", QUICOptions: quic, } - result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ - TLS: &model.OutboundTLSOptions{ - Enabled: true, - }, - } } if vmess.Net == "grpc" { From 07c9d937674638d618f595e2a3cbb4a690521d5e Mon Sep 17 00:00:00 2001 From: nite07 Date: Fri, 22 Mar 2024 16:10:15 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - api/handler/convert.go | 4 +- cmd/convert.go | 7 +- common/convert.go | 2 +- constant/prefix.go | 11 ++ model/config.go | 236 ++--------------------------------------- parser/error.go | 24 +++++ parser/hysteria.go | 103 +++++++++--------- parser/hysteria2.go | 84 ++++++++++----- parser/parsers_map.go | 15 +-- parser/port.go | 23 ++++ parser/shadowsocks.go | 81 +++++++++----- parser/trojan.go | 105 ++++++++++++------ parser/vless.go | 133 +++++++++++++++-------- parser/vmess.go | 30 +++--- test/country_test.go | 11 ++ 16 files changed, 428 insertions(+), 442 deletions(-) create mode 100644 constant/prefix.go create mode 100644 parser/error.go create mode 100644 parser/port.go create mode 100644 test/country_test.go diff --git a/.gitignore b/.gitignore index 8e47708..d9addfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode/launch.json .vscode/settings.json dist -*test.go template.json .idea \ No newline at end of file diff --git a/api/handler/convert.go b/api/handler/convert.go index 3e6dac3..8e7d970 100644 --- a/api/handler/convert.go +++ b/api/handler/convert.go @@ -4,7 +4,7 @@ import ( "encoding/json" "sub2sing-box/api/model" "sub2sing-box/common" - putil "sub2sing-box/util" + "sub2sing-box/util" "github.com/gin-gonic/gin" ) @@ -17,7 +17,7 @@ func Convert(c *gin.Context) { }) return } - j, err := putil.DecodeBase64(c.Query("data")) + j, err := util.DecodeBase64(c.Query("data")) if err != nil { c.JSON(400, gin.H{ "error": "Invalid data", diff --git a/cmd/convert.go b/cmd/convert.go index 82d8942..9bf8bf1 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -2,9 +2,10 @@ package cmd import ( "fmt" - "github.com/spf13/cobra" "os" - . "sub2sing-box/common" + "sub2sing-box/common" + + "github.com/spf13/cobra" ) var subscriptions []string @@ -39,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/common/convert.go b/common/convert.go index e975560..432fee3 100644 --- a/common/convert.go +++ b/common/convert.go @@ -233,7 +233,7 @@ func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) { return proxy, nil } } - return model.Outbound{}, errors.New("Unknown proxy format") + return model.Outbound{}, errors.New("unknown proxy format") } func ConvertCProxyToJson(proxy string) (string, error) { 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/model/config.go b/model/config.go index 052c607..3cdaec4 100644 --- a/model/config.go +++ b/model/config.go @@ -18,189 +18,14 @@ func (l *Listable[T]) UnmarshalJSON(data []byte) error { 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 []Inbound `json:"inbounds,omitempty"` - Outbounds []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"` + 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 { @@ -275,50 +100,3 @@ type RuleSet struct { 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/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 index 854e910..3fc487e 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -1,78 +1,76 @@ package parser import ( - "errors" "net/url" "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/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.Outbound, error) { - if !strings.HasPrefix(proxy, "hysteria://") { - return model.Outbound{}, errors.New("invalid hysteria Url") + if !strings.HasPrefix(proxy, constant.HysteriaPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) - serverInfo := strings.SplitN(parts[0], ":", 2) + + 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{}, errors.New("invalid hysteria Url") + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - params, err := url.ParseQuery(parts[1]) + server, portStr := serverInfo[0], serverInfo[1] + + port, err := ParsePort(portStr) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria Url") + return model.Outbound{}, err } - host := serverInfo[0] - port, err := strconv.Atoi(serverInfo[1]) + + params, err := url.ParseQuery(urlParts[1]) if err != nil { - return model.Outbound{}, 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] + 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 { - return model.Outbound{}, errors.New("invalid hysteria Url") + insecureBool = false } - result := model.Outbound{ + + 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: host, - ServerPort: uint16(port), + Server: server, + ServerPort: port, }, Up: upmbps, Down: downmbps, @@ -87,6 +85,5 @@ func ParseHysteria(proxy string) (model.Outbound, error) { }, }, }, - } - return result, nil + }, nil } diff --git a/parser/hysteria2.go b/parser/hysteria2.go index 88b97c0..3afcff5 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -1,39 +1,73 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) -// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com - func ParseHysteria2(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) && + !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) - serverInfo := strings.SplitN(parts[1], "/?", 2) + + 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 { - serverAndPort = append(serverAndPort, "443") - } else if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + 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, + } } - params, err := url.ParseQuery(serverInfo[1]) + + port, err := ParsePort(portStr) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, err } - port, err := strconv.Atoi(serverAndPort[1]) + + params, err := url.ParseQuery(paramStr) if err != nil { - return model.Outbound{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } - remarks := params.Get("name") - server := serverAndPort[0] - password := parts[0] - network := params.Get("network") + + 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, @@ -44,14 +78,14 @@ func ParseHysteria2(proxy string) (model.Outbound, error) { }, Password: password, Obfs: &model.Hysteria2Obfs{ - Type: params.Get("obfs"), - Password: params.Get("obfs-password"), + Type: obfs, + Password: obfsPassword, }, OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ - TLS: &model.OutboundTLSOptions{Enabled: params.Get("pinSHA256") != "", - Insecure: params.Get("insecure") == "1", - ServerName: params.Get("sni"), - Certificate: []string{params.Get("pinSHA256")}}, + TLS: &model.OutboundTLSOptions{Enabled: enableTLS, + Insecure: insecureBool, + ServerName: sni, + Certificate: []string{pinSHA256}}, }, Network: network, }, diff --git a/parser/parsers_map.go b/parser/parsers_map.go index 6837179..dd8e9ac 100644 --- a/parser/parsers_map.go +++ b/parser/parsers_map.go @@ -1,15 +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){ - "ss://": ParseShadowsocks, - "vmess://": ParseVmess, - "trojan://": ParseTrojan, - "vless://": ParseVless, - "hysteria://": ParseHysteria, - "hy2://": ParseHysteria2, - "hysteria2://": ParseHysteria2, + 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 index 2ce68d9..e2bb7aa 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -1,62 +1,87 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" "sub2sing-box/util" ) func ParseShadowsocks(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "ss://") { - return model.Outbound{}, errors.New("invalid ss Url") + if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid ss Url") - } - if !strings.Contains(parts[0], ":") { - decoded, err := util.DecodeBase64(parts[0]) - if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + + 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, } - parts[0] = decoded } - credentials := strings.SplitN(parts[0], ":", 2) + + 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{}, errors.New("invalid ss Url") + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "missing server host or port", + Raw: proxy, + } } - serverInfo := strings.SplitN(parts[1], "#", 2) - serverAndPort := strings.SplitN(serverInfo[0], ":", 2) - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid ss Url") + 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 := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + port, err := ParsePort(portStr) if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, err } - remarks := "" + + var remarks string if len(serverInfo) == 2 { unescape, err := url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "cannot unescape remarks", + Raw: proxy, + } } remarks = strings.TrimSpace(unescape) } else { - remarks = strings.TrimSpace(serverAndPort[0]) + remarks = strings.TrimSpace(server + ":" + portStr) } - method := credentials[0] - password := credentials[1] - server := strings.TrimSpace(serverAndPort[0]) + result := model.Outbound{ Type: "shadowsocks", Tag: remarks, ShadowsocksOptions: model.ShadowsocksOutboundOptions{ ServerOptions: model.ServerOptions{ Server: server, - ServerPort: uint16(port), + ServerPort: port, }, Method: method, Password: password, diff --git a/parser/trojan.go b/parser/trojan.go index d838dd2..dc09e97 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -1,118 +1,155 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) func ParseTrojan(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "trojan://") { - return model.Outbound{}, errors.New("invalid trojan Url") + if !strings.HasPrefix(proxy, constant.TrojanPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid trojan Url") + + 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, + } } - serverInfo := strings.SplitN(parts[1], "#", 2) + 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{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid trojan Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + + 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] } - server := strings.TrimSpace(serverAndPort[0]) - password := strings.TrimSpace(parts[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: uint16(port), + ServerPort: port, }, Password: password, - Network: params.Get("type"), + Network: network, }, } - 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 - } + + if security == "xtls" || security == "tls" { result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, ALPN: alpn, - ServerName: params.Get("sni"), + ServerName: sni, }, } } + if params.Get("security") == "reality" { result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, - ServerName: params.Get("sni"), + ServerName: sni, Reality: &model.OutboundRealityOptions{ Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), + PublicKey: pbk, + ShortID: sid, }, UTLS: &model.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + Enabled: enableUTLS, + Fingerprint: fp, }, }, } } + if params.Get("type") == "ws" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), + Path: path, Headers: map[string]string{ - "Host": params.Get("host"), + "Host": host, }, }, } } + if params.Get("type") == "http" { result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: model.V2RayHTTPOptions{ - Host: []string{params.Get("host")}, + 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: params.Get("serviceName"), + ServiceName: serviceName, }, } } diff --git a/parser/vless.go b/parser/vless.go index 1fc12c6..987de8e 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -1,35 +1,59 @@ package parser import ( - "errors" "net/url" - "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" ) func ParseVless(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "vless://") { - return model.Outbound{}, errors.New("invalid vless Url") + if !strings.HasPrefix(proxy, constant.VLESSPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) - if len(parts) != 2 { - return model.Outbound{}, errors.New("invalid vless Url") + + 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(parts[1], "#", 2) + + 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{}, err - } - if len(serverAndPort) != 2 { - return model.Outbound{}, errors.New("invalid vless Url") - } - port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) - if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } + remarks := "" if len(serverInfo) == 2 { if strings.Contains(serverInfo[1], "|") { @@ -37,92 +61,107 @@ func ParseVless(proxy string) (model.Outbound, error) { } else { remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } } } else { - remarks, err = url.QueryUnescape(serverAndPort[0]) + remarks, err = url.QueryUnescape(server) if err != nil { return model.Outbound{}, err } } - server := strings.TrimSpace(serverAndPort[0]) - uuid := strings.TrimSpace(parts[0]) + + 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: uint16(port), + ServerPort: port, }, UUID: uuid, - Flow: params.Get("flow"), + Flow: flow, }, } - if params.Get("security") == "tls" { - var alpn []string - if strings.Contains(params.Get("alpn"), ",") { - alpn = strings.Split(params.Get("alpn"), ",") - } else { - alpn = nil - } + + if security == "tls" { result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ TLS: &model.OutboundTLSOptions{ Enabled: true, ALPN: alpn, - ServerName: params.Get("sni"), - Insecure: params.Get("allowInsecure") == "1", + ServerName: sni, + Insecure: insecureBool, }, } - if params.Get("fp") != "" { - result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ - Enabled: true, - Fingerprint: params.Get("fp"), - } + result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{ + Enabled: enableUTLS, + Fingerprint: fp, } } - if params.Get("security") == "reality" { + + if security == "reality" { result.VLESSOptions.OutboundTLSOptionsContainer.TLS.Reality = &model.OutboundRealityOptions{ Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), + PublicKey: pbk, + ShortID: sid, } } + if params.Get("type") == "ws" { result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", WebsocketOptions: model.V2RayWebsocketOptions{ - Path: params.Get("path"), + Path: path, }, } - if params.Get("host") != "" { - result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = params.Get("host") - } + 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: params.Get("serviceName"), + ServiceName: serviceName, }, } } + if params.Get("type") == "http" { - host, err := url.QueryUnescape(params.Get("host")) + hosts, err := url.QueryUnescape(host) if err != nil { - return model.Outbound{}, err + return model.Outbound{}, &ParseError{ + Type: ErrCannotParseParams, + Raw: proxy, + Message: err.Error(), + } } result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: model.V2RayHTTPOptions{ - Host: strings.Split(host, ","), + Host: strings.Split(hosts, ","), }, } } diff --git a/parser/vmess.go b/parser/vmess.go index 262d871..a290fd6 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -2,47 +2,53 @@ package parser import ( "encoding/json" - "errors" "net/url" "strconv" "strings" + "sub2sing-box/constant" "sub2sing-box/model" "sub2sing-box/util" ) func ParseVmess(proxy string) (model.Outbound, error) { - if !strings.HasPrefix(proxy, "vmess://") { - return model.Outbound{}, errors.New("invalid vmess url") + if !strings.HasPrefix(proxy, constant.VMessPrefix) { + return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} } - base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + + proxy = strings.TrimPrefix(proxy, constant.VMessPrefix) + base64, err := util.DecodeBase64(proxy) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + 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{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } - port := 0 + + var port uint16 switch vmess.Port.(type) { case string: - port, err = strconv.Atoi(vmess.Port.(string)) + port, err = ParsePort(vmess.Port.(string)) if err != nil { - return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, err } case float64: - port = int(vmess.Port.(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{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} } case float64: aid = int(vmess.Aid.(float64)) } + if vmess.Scy == "" { vmess.Scy = "auto" } @@ -58,7 +64,7 @@ func ParseVmess(proxy string) (model.Outbound, error) { VMessOptions: model.VMessOutboundOptions{ ServerOptions: model.ServerOptions{ Server: vmess.Add, - ServerPort: uint16(port), + ServerPort: port, }, UUID: vmess.Id, AlterId: aid, 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 节点")) +}