mirror of
https://github.com/nitezs/sub2sing-box.git
synced 2024-12-23 15:04:41 -05:00
Merge branch 'dev'
This commit is contained in:
commit
598ef72717
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
dist
|
||||
*test.go
|
||||
template.json
|
||||
.idea
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -3,8 +3,8 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"sub2sing-box/api/model"
|
||||
iutil "sub2sing-box/internal/util"
|
||||
putil "sub2sing-box/pkg/util"
|
||||
"sub2sing-box/common"
|
||||
"sub2sing-box/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -17,7 +17,7 @@ func Convert(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
j, err := iutil.DecodeBase64(c.Query("data"))
|
||||
j, err := util.DecodeBase64(c.Query("data"))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid data",
|
||||
@ -38,7 +38,7 @@ func Convert(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
result, err := putil.Convert(
|
||||
result, err := common.Convert(
|
||||
data.Subscriptions,
|
||||
data.Proxies,
|
||||
data.Template,
|
||||
|
@ -3,7 +3,7 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
. "sub2sing-box/pkg/util"
|
||||
"sub2sing-box/common"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -40,7 +40,7 @@ var convertCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
result := ""
|
||||
var err error
|
||||
result, err = Convert(
|
||||
result, err = common.Convert(
|
||||
subscriptions,
|
||||
proxies,
|
||||
template,
|
||||
|
@ -5,7 +5,3 @@ import (
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{}
|
||||
|
||||
func SetVersion(version string) {
|
||||
RootCmd.Version = version
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package util
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -9,10 +9,10 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sub2sing-box/internal/model"
|
||||
"sub2sing-box/internal/util"
|
||||
"sub2sing-box/pkg/parser"
|
||||
C "sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
"sub2sing-box/parser"
|
||||
"sub2sing-box/util"
|
||||
)
|
||||
|
||||
func Convert(
|
||||
@ -29,7 +29,7 @@ func Convert(
|
||||
result := ""
|
||||
var err error
|
||||
|
||||
proxyList, err := ConvertSubscriptionsToSProxy(subscriptions)
|
||||
outbounds, err := ConvertSubscriptionsToSProxy(subscriptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -38,18 +38,18 @@ func Convert(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
proxyList = append(proxyList, p)
|
||||
outbounds = append(outbounds, p)
|
||||
}
|
||||
|
||||
if delete != "" {
|
||||
proxyList, err = DeleteProxy(proxyList, delete)
|
||||
outbounds, err = DeleteProxy(outbounds, delete)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range rename {
|
||||
proxyList, err = RenameProxy(proxyList, k, v)
|
||||
outbounds, err = RenameProxy(outbounds, k, v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -57,14 +57,14 @@ func Convert(
|
||||
|
||||
keep := make(map[int]bool)
|
||||
set := make(map[string]struct {
|
||||
Proxy model.Proxy
|
||||
Proxy model.Outbound
|
||||
Count int
|
||||
})
|
||||
for i, p := range proxyList {
|
||||
for i, p := range outbounds {
|
||||
if _, exists := set[p.Tag]; !exists {
|
||||
keep[i] = true
|
||||
set[p.Tag] = struct {
|
||||
Proxy model.Proxy
|
||||
Proxy model.Outbound
|
||||
Count int
|
||||
}{p, 0}
|
||||
} else {
|
||||
@ -72,32 +72,16 @@ func Convert(
|
||||
p2, _ := json.Marshal(set[p.Tag])
|
||||
if string(p1) != string(p2) {
|
||||
set[p.Tag] = struct {
|
||||
Proxy model.Proxy
|
||||
Proxy model.Outbound
|
||||
Count int
|
||||
}{p, set[p.Tag].Count + 1}
|
||||
keep[i] = true
|
||||
proxyList[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count)
|
||||
outbounds[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count)
|
||||
} else {
|
||||
keep[i] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
var newProxyList []model.Proxy
|
||||
for i, p := range proxyList {
|
||||
if keep[i] {
|
||||
newProxyList = append(newProxyList, p)
|
||||
}
|
||||
}
|
||||
proxyList = newProxyList
|
||||
var outbounds []model.Outbound
|
||||
ps, err := json.Marshal(&proxyList)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = json.Unmarshal(ps, &outbounds)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if group {
|
||||
outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType)
|
||||
}
|
||||
@ -120,17 +104,30 @@ func Convert(
|
||||
func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound {
|
||||
newGroup := make(map[string]model.Outbound)
|
||||
for _, p := range proxies {
|
||||
if p.Type != "selector" && p.Type != "urltest" {
|
||||
if p.Type != C.TypeSelector && p.Type != C.TypeURLTest {
|
||||
country := model.GetContryName(p.Tag)
|
||||
if group, ok := newGroup[country]; ok {
|
||||
group.Outbounds = append(group.Outbounds, p.Tag)
|
||||
group.SetOutbounds(append(group.GetOutbounds(), p.Tag))
|
||||
newGroup[country] = group
|
||||
} else {
|
||||
newGroup[country] = model.Outbound{
|
||||
Tag: country,
|
||||
Type: groupType,
|
||||
Outbounds: []string{p.Tag},
|
||||
InterruptExistConnections: true,
|
||||
if groupType == C.TypeSelector {
|
||||
newGroup[country] = model.Outbound{
|
||||
Tag: country,
|
||||
Type: groupType,
|
||||
SelectorOptions: model.SelectorOutboundOptions{
|
||||
Outbounds: []string{p.Tag},
|
||||
InterruptExistConnections: true,
|
||||
},
|
||||
}
|
||||
} else if groupType == C.TypeURLTest {
|
||||
newGroup[country] = model.Outbound{
|
||||
Tag: country,
|
||||
Type: groupType,
|
||||
URLTestOptions: model.URLTestOutboundOptions{
|
||||
Outbounds: []string{p.Tag},
|
||||
InterruptExistConnections: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,13 +177,13 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error)
|
||||
}
|
||||
}
|
||||
config, err = ReadTemplate(template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
proxyTags := make([]string, 0)
|
||||
groupTags := make([]string, 0)
|
||||
groups := make(map[string]model.Outbound)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, p := range outbounds {
|
||||
if model.IsCountryGroup(p.Tag) {
|
||||
groupTags = append(groupTags, p.Tag)
|
||||
@ -199,22 +196,24 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error)
|
||||
}
|
||||
reg := regexp.MustCompile("<[A-Za-z]{2}>")
|
||||
for i, outbound := range config.Outbounds {
|
||||
var parsedOutbound []string = make([]string, 0)
|
||||
for _, o := range outbound.Outbounds {
|
||||
if o == "<all-proxy-tags>" {
|
||||
parsedOutbound = append(parsedOutbound, proxyTags...)
|
||||
} else if o == "<all-country-tags>" {
|
||||
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 == "<all-proxy-tags>" {
|
||||
parsedOutbound = append(parsedOutbound, proxyTags...)
|
||||
} else if o == "<all-country-tags>" {
|
||||
parsedOutbound = append(parsedOutbound, groupTags...)
|
||||
} else if reg.MatchString(o) {
|
||||
country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>"))
|
||||
if group, ok := groups[country]; ok {
|
||||
parsedOutbound = append(parsedOutbound, group.GetOutbounds()...)
|
||||
}
|
||||
} else {
|
||||
parsedOutbound = append(parsedOutbound, o)
|
||||
}
|
||||
} else {
|
||||
parsedOutbound = append(parsedOutbound, o)
|
||||
}
|
||||
config.Outbounds[i].SetOutbounds(parsedOutbound)
|
||||
}
|
||||
config.Outbounds[i].Outbounds = parsedOutbound
|
||||
}
|
||||
config.Outbounds = append(config.Outbounds, outbounds...)
|
||||
data, err := json.Marshal(config)
|
||||
@ -224,17 +223,17 @@ func MergeTemplate(outbounds []model.Outbound, template string) (string, error)
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func ConvertCProxyToSProxy(proxy string) (model.Proxy, error) {
|
||||
func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) {
|
||||
for prefix, parseFunc := range parser.ParserMap {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
proxy, err := parseFunc(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
return proxy, nil
|
||||
}
|
||||
}
|
||||
return model.Proxy{}, errors.New("Unknown proxy format")
|
||||
return model.Outbound{}, errors.New("unknown proxy format")
|
||||
}
|
||||
|
||||
func ConvertCProxyToJson(proxy string) (string, error) {
|
||||
@ -249,8 +248,8 @@ func ConvertCProxyToJson(proxy string) (string, error) {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func ConvertSubscriptionsToSProxy(urls []string) ([]model.Proxy, error) {
|
||||
proxyList := make([]model.Proxy, 0)
|
||||
func ConvertSubscriptionsToSProxy(urls []string) ([]model.Outbound, error) {
|
||||
proxyList := make([]model.Outbound, 0)
|
||||
for _, url := range urls {
|
||||
data, err := util.Fetch(url, 3)
|
||||
if err != nil {
|
||||
@ -301,12 +300,12 @@ func ReadTemplate(path string) (model.Config, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) {
|
||||
func DeleteProxy(proxies []model.Outbound, regex string) ([]model.Outbound, error) {
|
||||
reg, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var newProxies []model.Proxy
|
||||
var newProxies []model.Outbound
|
||||
for _, p := range proxies {
|
||||
if !reg.MatchString(p.Tag) {
|
||||
newProxies = append(newProxies, p)
|
||||
@ -315,7 +314,7 @@ func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) {
|
||||
return newProxies, nil
|
||||
}
|
||||
|
||||
func RenameProxy(proxies []model.Proxy, regex string, replaceText string) ([]model.Proxy, error) {
|
||||
func RenameProxy(proxies []model.Outbound, regex string, replaceText string) ([]model.Outbound, error) {
|
||||
reg, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return nil, err
|
11
constant/prefix.go
Normal file
11
constant/prefix.go
Normal file
@ -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://"
|
||||
)
|
31
constant/proxy.go
Normal file
31
constant/proxy.go
Normal file
@ -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"
|
||||
)
|
3
constant/version.go
Normal file
3
constant/version.go
Normal file
@ -0,0 +1,3 @@
|
||||
package constant
|
||||
|
||||
var Version = "dev"
|
6
go.mod
6
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
|
||||
)
|
||||
|
@ -1,423 +0,0 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Listable[T any] []T
|
||||
|
||||
func (l *Listable[T]) UnmarshalJSON(data []byte) error {
|
||||
var arr []T
|
||||
if err := json.Unmarshal(data, &arr); err == nil {
|
||||
*l = arr
|
||||
return nil
|
||||
}
|
||||
var v T
|
||||
if err := json.Unmarshal(data, &v); err == nil {
|
||||
*l = []T{v}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Obfs struct {
|
||||
Str string
|
||||
Obfs *Hysteria2Obfs
|
||||
IsStr bool
|
||||
}
|
||||
|
||||
func (o *Obfs) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
o.Str = str
|
||||
o.IsStr = true
|
||||
return nil
|
||||
}
|
||||
var obfs Hysteria2Obfs
|
||||
if err := json.Unmarshal(data, &obfs); err == nil {
|
||||
o.IsStr = false
|
||||
o.Obfs = &obfs
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Obfs) MarshalJSON() ([]byte, error) {
|
||||
if o.IsStr {
|
||||
return json.Marshal(o.Str)
|
||||
}
|
||||
return json.Marshal(o.Obfs)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Log *LogOptions `json:"log,omitempty"`
|
||||
DNS *DNSOptions `json:"dns,omitempty"`
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Inbounds Listable[Inbound] `json:"inbounds,omitempty"`
|
||||
Outbounds Listable[Outbound] `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
}
|
||||
|
||||
type LogOptions struct {
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Level string `json:"level,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Timestamp bool `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
type DNSOptions struct {
|
||||
Servers Listable[DNSServerOptions] `json:"servers,omitempty"`
|
||||
Rules Listable[DNSRule] `json:"rules,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
ReverseMapping bool `json:"reverse_mapping,omitempty"`
|
||||
FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
DisableExpire bool `json:"disable_expire,omitempty"`
|
||||
IndependentCache bool `json:"independent_cache,omitempty"`
|
||||
ClientSubnet string `json:"client_subnet,omitempty"`
|
||||
}
|
||||
|
||||
type DNSServerOptions struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Address string `json:"address"`
|
||||
AddressResolver string `json:"address_resolver,omitempty"`
|
||||
AddressStrategy string `json:"address_strategy,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
ClientSubnet string `json:"client_subnet,omitempty"`
|
||||
}
|
||||
|
||||
type DNSRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Inbound Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
QueryType Listable[string] `json:"query_type,omitempty"`
|
||||
Network Listable[string] `json:"network,omitempty"`
|
||||
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol Listable[string] `json:"protocol,omitempty"`
|
||||
Domain Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP Listable[string] `json:"geoip,omitempty"`
|
||||
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
SourcePort Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath Listable[string] `json:"process_path,omitempty"`
|
||||
PackageName Listable[string] `json:"package_name,omitempty"`
|
||||
User Listable[string] `json:"user,omitempty"`
|
||||
UserID Listable[int32] `json:"user_id,omitempty"`
|
||||
Outbound Listable[string] `json:"outbound,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
RewriteTTL uint32 `json:"rewrite_ttl,omitempty"`
|
||||
ClientSubnet string `json:"client_subnet,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Rules Listable[DNSRule] `json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
type DNSFakeIPOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Inet4Range string `json:"inet4_range,omitempty"`
|
||||
Inet6Range string `json:"inet6_range,omitempty"`
|
||||
}
|
||||
|
||||
type NTPOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
ServerPort uint16 `json:"server_port,omitempty"`
|
||||
Interval string `json:"interval,omitempty"`
|
||||
WriteToSystem bool `json:"write_to_system,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
BindInterface string `json:"bind_interface,omitempty"`
|
||||
Inet4BindAddress string `json:"inet4_bind_address,omitempty"`
|
||||
Inet6BindAddress string `json:"inet6_bind_address,omitempty"`
|
||||
ProtectPath string `json:"protect_path,omitempty"`
|
||||
RoutingMark int `json:"routing_mark,omitempty"`
|
||||
ReuseAddr bool `json:"reuse_addr,omitempty"`
|
||||
ConnectTimeout string `json:"connect_timeout,omitempty"`
|
||||
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
||||
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
||||
UDPFragment bool `json:"udp_fragment,omitempty"`
|
||||
DomainStrategy string `json:"domain_strategy,omitempty"`
|
||||
FallbackDelay string `json:"fallback_delay,omitempty"`
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
InterfaceName string `json:"interface_name,omitempty"`
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
GSO bool `json:"gso,omitempty"`
|
||||
Inet4Address Listable[string] `json:"inet4_address,omitempty"`
|
||||
Inet6Address Listable[string] `json:"inet6_address,omitempty"`
|
||||
AutoRoute bool `json:"auto_route,omitempty"`
|
||||
StrictRoute bool `json:"strict_route,omitempty"`
|
||||
Inet4RouteAddress Listable[string] `json:"inet4_route_address,omitempty"`
|
||||
Inet6RouteAddress Listable[string] `json:"inet6_route_address,omitempty"`
|
||||
Inet4RouteExcludeAddress Listable[string] `json:"inet4_route_exclude_address,omitempty"`
|
||||
Inet6RouteExcludeAddress Listable[string] `json:"inet6_route_exclude_address,omitempty"`
|
||||
IncludeInterface Listable[string] `json:"include_interface,omitempty"`
|
||||
ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"`
|
||||
IncludeUID Listable[uint32] `json:"include_uid,omitempty"`
|
||||
IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"`
|
||||
ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"`
|
||||
ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"`
|
||||
IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"`
|
||||
IncludePackage Listable[string] `json:"include_package,omitempty"`
|
||||
ExcludePackage Listable[string] `json:"exclude_package,omitempty"`
|
||||
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"`
|
||||
UDPTimeout string `json:"udp_timeout,omitempty"`
|
||||
Stack string `json:"stack,omitempty"`
|
||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||
SniffEnabled bool `json:"sniff,omitempty"`
|
||||
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
|
||||
SniffTimeout string `json:"sniff_timeout,omitempty"`
|
||||
DomainStrategy string `json:"domain_strategy,omitempty"`
|
||||
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
|
||||
}
|
||||
|
||||
type TunPlatformOptions struct {
|
||||
HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPProxyOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
BypassDomain Listable[string] `json:"bypass_domain,omitempty"`
|
||||
MatchDomain Listable[string] `json:"match_domain,omitempty"`
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
BindInterface string `json:"bind_interface,omitempty"`
|
||||
Inet4BindAddress string `json:"inet4_bind_address,omitempty"`
|
||||
Inet6BindAddress string `json:"inet6_bind_address,omitempty"`
|
||||
ProtectPath string `json:"protect_path,omitempty"`
|
||||
RoutingMark int `json:"routing_mark,omitempty"`
|
||||
ReuseAddr bool `json:"reuse_addr,omitempty"`
|
||||
ConnectTimeout string `json:"connect_timeout,omitempty"`
|
||||
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
||||
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
||||
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
||||
DomainStrategy string `json:"domain_strategy,omitempty"`
|
||||
FallbackDelay string `json:"fallback_delay,omitempty"`
|
||||
OverrideAddress string `json:"override_address,omitempty"`
|
||||
OverridePort uint16 `json:"override_port,omitempty"`
|
||||
ProxyProtocol uint8 `json:"proxy_protocol,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
ServerPort uint16 `json:"server_port,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Headers map[string]Listable[string] `json:"headers,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
PluginOptions string `json:"plugin_opts,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
AlterId int `json:"alter_id,omitempty"`
|
||||
GlobalPadding bool `json:"global_padding,omitempty"`
|
||||
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
|
||||
PacketEncoding string `json:"packet_encoding,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
SystemInterface bool `json:"system_interface,omitempty"`
|
||||
GSO bool `json:"gso,omitempty"`
|
||||
InterfaceName string `json:"interface_name,omitempty"`
|
||||
LocalAddress Listable[string] `json:"local_address,omitempty"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
Peers Listable[WireGuardPeer] `json:"peers,omitempty"`
|
||||
PeerPublicKey string `json:"peer_public_key,omitempty"`
|
||||
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
||||
Reserved Listable[uint8] `json:"reserved,omitempty"`
|
||||
Workers int `json:"workers,omitempty"`
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
Up string `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down string `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs *Obfs `json:"obfs,omitempty"`
|
||||
Auth Listable[byte] `json:"auth,omitempty"`
|
||||
AuthString string `json:"auth_str,omitempty"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
ExecutablePath string `json:"executable_path,omitempty"`
|
||||
ExtraArgs Listable[string] `json:"extra_args,omitempty"`
|
||||
DataDirectory string `json:"data_directory,omitempty"`
|
||||
Options map[string]string `json:"torrc,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
PrivateKeyPath string `json:"private_key_path,omitempty"`
|
||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||
HostKey Listable[string] `json:"host_key,omitempty"`
|
||||
HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"`
|
||||
ClientVersion string `json:"client_version,omitempty"`
|
||||
ObfsParam string `json:"obfs_param,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
ProtocolParam string `json:"protocol_param,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
CongestionControl string `json:"congestion_control,omitempty"`
|
||||
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
|
||||
UDPOverStream bool `json:"udp_over_stream,omitempty"`
|
||||
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
|
||||
Heartbeat string `json:"heartbeat,omitempty"`
|
||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Outbounds Listable[string] `json:"outbounds,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Interval string `json:"interval,omitempty"`
|
||||
Tolerance uint16 `json:"tolerance,omitempty"`
|
||||
IdleTimeout string `json:"idle_timeout,omitempty"`
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
}
|
||||
|
||||
type WireGuardPeer struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
||||
AllowedIPs Listable[string] `json:"allowed_ips,omitempty"`
|
||||
Reserved Listable[uint8] `json:"reserved,omitempty"`
|
||||
}
|
||||
|
||||
type RouteOptions struct {
|
||||
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
|
||||
Geosite *GeositeOptions `json:"geosite,omitempty"`
|
||||
Rules Listable[Rule] `json:"rules,omitempty"`
|
||||
RuleSet Listable[RuleSet] `json:"rule_set,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
FindProcess bool `json:"find_process,omitempty"`
|
||||
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
|
||||
DefaultInterface string `json:"default_interface,omitempty"`
|
||||
DefaultMark int `json:"default_mark,omitempty"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Inbound Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
Network Listable[string] `json:"network,omitempty"`
|
||||
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Domain Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP Listable[string] `json:"geoip,omitempty"`
|
||||
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourcePort Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath Listable[string] `json:"process_path,omitempty"`
|
||||
PackageName Listable[string] `json:"package_name,omitempty"`
|
||||
User Listable[string] `json:"user,omitempty"`
|
||||
UserID Listable[int32] `json:"user_id,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Outbound string `json:"outbound,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Rules Listable[Rule] `json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
type GeoIPOptions struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
}
|
||||
|
||||
type GeositeOptions struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
}
|
||||
|
||||
type RuleSet struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Format string `json:"format"`
|
||||
Path string `json:"path,omitempty"`
|
||||
URL string `json:"url"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval string `json:"update_interval,omitempty"`
|
||||
}
|
||||
|
||||
type ExperimentalOptions struct {
|
||||
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
|
||||
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
||||
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
|
||||
}
|
||||
|
||||
type CacheFileOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||
StoreRDRC bool `json:"store_rdrc,omitempty"`
|
||||
RDRCTimeout string `json:"rdrc_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type ClashAPIOptions struct {
|
||||
ExternalController string `json:"external_controller,omitempty"`
|
||||
ExternalUI string `json:"external_ui,omitempty"`
|
||||
ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
|
||||
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheFile string `json:"cache_file,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
CacheID string `json:"cache_id,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreMode bool `json:"store_mode,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreSelected bool `json:"store_selected,omitempty"`
|
||||
// Deprecated: migrated to global cache file
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayAPIOptions struct {
|
||||
Listen string `json:"listen,omitempty"`
|
||||
Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayStatsServiceOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Inbounds Listable[string] `json:"inbounds,omitempty"`
|
||||
Outbounds Listable[string] `json:"outbounds,omitempty"`
|
||||
Users Listable[string] `json:"users,omitempty"`
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type Hysteria struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
Up string `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down string `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Auth []byte `json:"auth,omitempty"`
|
||||
AuthString string `json:"auth_str,omitempty"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type Hysteria2Obfs struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type Hysteria2 struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type SortByNumber []Outbound
|
||||
|
||||
func (a SortByNumber) Len() int { return len(a) }
|
||||
func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a SortByNumber) Less(i, j int) bool { return len(a[i].Outbounds) < len(a[j].Outbounds) }
|
||||
|
||||
type SortByTag []Outbound
|
||||
|
||||
func (a SortByTag) Len() int { return len(a) }
|
||||
func (a SortByTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a SortByTag) Less(i, j int) bool {
|
||||
tags := []language.Tag{
|
||||
language.English,
|
||||
language.Chinese,
|
||||
}
|
||||
matcher := language.NewMatcher(tags)
|
||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||
c := collate.New(bestMatch)
|
||||
return c.CompareString(a[i].Tag, a[j].Tag) < 0
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package model
|
||||
|
||||
type Trojan struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
Password string `json:"password"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package model
|
||||
|
||||
type TUIC struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
CongestionControl string `json:"congestion_control,omitempty"`
|
||||
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
|
||||
UDPOverStream bool `json:"udp_over_stream,omitempty"`
|
||||
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
|
||||
Heartbeat string `json:"heartbeat,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package model
|
||||
|
||||
type VLESS struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
UUID string `json:"uuid"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
PacketEncoding *string `json:"packet_encoding,omitempty"`
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package model
|
||||
|
||||
type VmessJson struct {
|
||||
V string `json:"v"`
|
||||
Ps string `json:"ps"`
|
||||
Add string `json:"add"`
|
||||
Port interface{} `json:"port"`
|
||||
Id string `json:"id"`
|
||||
Aid interface{} `json:"aid"`
|
||||
Scy string `json:"scy"`
|
||||
Net string `json:"net"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Tls string `json:"tls"`
|
||||
Sni string `json:"sni"`
|
||||
Alpn string `json:"alpn"`
|
||||
Fp string `json:"fp"`
|
||||
}
|
||||
|
||||
type VMess struct {
|
||||
Server string `json:"server"`
|
||||
ServerPort uint16 `json:"server_port"`
|
||||
UUID string `json:"uuid"`
|
||||
Security string `json:"security"`
|
||||
AlterId int `json:"alter_id,omitempty"`
|
||||
GlobalPadding bool `json:"global_padding,omitempty"`
|
||||
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
PacketEncoding string `json:"packet_encoding,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
7
main.go
7
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)
|
||||
|
102
model/config.go
Normal file
102
model/config.go
Normal file
@ -0,0 +1,102 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Listable[T any] []T
|
||||
|
||||
func (l *Listable[T]) UnmarshalJSON(data []byte) error {
|
||||
var arr []T
|
||||
if err := json.Unmarshal(data, &arr); err == nil {
|
||||
*l = arr
|
||||
return nil
|
||||
}
|
||||
var v T
|
||||
if err := json.Unmarshal(data, &v); err == nil {
|
||||
*l = []T{v}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Log any `json:"log,omitempty"`
|
||||
DNS any `json:"dns,omitempty"`
|
||||
NTP any `json:"ntp,omitempty"`
|
||||
Inbounds any `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Experimental any `json:"experimental,omitempty"`
|
||||
}
|
||||
|
||||
type RouteOptions struct {
|
||||
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
|
||||
Geosite *GeositeOptions `json:"geosite,omitempty"`
|
||||
Rules Listable[Rule] `json:"rules,omitempty"`
|
||||
RuleSet Listable[RuleSet] `json:"rule_set,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
FindProcess bool `json:"find_process,omitempty"`
|
||||
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
|
||||
DefaultInterface string `json:"default_interface,omitempty"`
|
||||
DefaultMark int `json:"default_mark,omitempty"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Inbound Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
Network Listable[string] `json:"network,omitempty"`
|
||||
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Domain Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP Listable[string] `json:"geoip,omitempty"`
|
||||
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourcePort Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath Listable[string] `json:"process_path,omitempty"`
|
||||
PackageName Listable[string] `json:"package_name,omitempty"`
|
||||
User Listable[string] `json:"user,omitempty"`
|
||||
UserID Listable[int32] `json:"user_id,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Outbound string `json:"outbound,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Rules Listable[Rule] `json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
type GeoIPOptions struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
}
|
||||
|
||||
type GeositeOptions struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
}
|
||||
|
||||
type RuleSet struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
Format string `json:"format"`
|
||||
Path string `json:"path,omitempty"`
|
||||
URL string `json:"url"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval string `json:"update_interval,omitempty"`
|
||||
}
|
8
model/direct.go
Normal file
8
model/direct.go
Normal file
@ -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"`
|
||||
}
|
16
model/group.go
Normal file
16
model/group.go
Normal file
@ -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"`
|
||||
}
|
18
model/hysteria.go
Normal file
18
model/hysteria.go
Normal file
@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
type HysteriaOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Up string `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down string `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Auth []byte `json:"auth,omitempty"`
|
||||
AuthString string `json:"auth_str,omitempty"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
18
model/hysteria2.go
Normal file
18
model/hysteria2.go
Normal file
@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
type Hysteria2Obfs struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type Hysteria2OutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||
}
|
167
model/outbound.go
Normal file
167
model/outbound.go
Normal file
@ -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"`
|
||||
}
|
@ -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"`
|
13
model/shadowsocksr.go
Normal file
13
model/shadowsocksr.go
Normal file
@ -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"`
|
||||
}
|
19
model/shadowtls.go
Normal file
19
model/shadowtls.go
Normal file
@ -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
|
||||
}
|
21
model/simple.go
Normal file
21
model/simple.go
Normal file
@ -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"`
|
||||
}
|
44
model/sort.go
Normal file
44
model/sort.go
Normal file
@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
C "sub2sing-box/constant"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type SortByNumber []Outbound
|
||||
|
||||
func (a SortByNumber) Len() int { return len(a) }
|
||||
func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a SortByNumber) Less(i, j int) bool {
|
||||
var size1, size2 int
|
||||
if a[i].Type == C.TypeSelector {
|
||||
size1 = len(a[i].SelectorOptions.Outbounds)
|
||||
}
|
||||
if a[i].Type == C.TypeURLTest {
|
||||
size1 = len(a[j].URLTestOptions.Outbounds)
|
||||
}
|
||||
if a[j].Type == C.TypeSelector {
|
||||
size2 = len(a[j].SelectorOptions.Outbounds)
|
||||
}
|
||||
if a[j].Type == C.TypeURLTest {
|
||||
size2 = len(a[j].URLTestOptions.Outbounds)
|
||||
}
|
||||
return size1 < size2
|
||||
}
|
||||
|
||||
type SortByTag []Outbound
|
||||
|
||||
func (a SortByTag) Len() int { return len(a) }
|
||||
func (a SortByTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a SortByTag) Less(i, j int) bool {
|
||||
tags := []language.Tag{
|
||||
language.English,
|
||||
language.Chinese,
|
||||
}
|
||||
matcher := language.NewMatcher(tags)
|
||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||
c := collate.New(bestMatch)
|
||||
return c.CompareString(a[i].Tag, a[j].Tag) < 0
|
||||
}
|
14
model/ssh.go
Normal file
14
model/ssh.go
Normal file
@ -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"`
|
||||
}
|
@ -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"`
|
||||
}
|
9
model/tor.go
Normal file
9
model/tor.go
Normal file
@ -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"`
|
||||
}
|
11
model/trojan.go
Normal file
11
model/trojan.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type TrojanOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Password string `json:"password"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
15
model/tuic.go
Normal file
15
model/tuic.go
Normal file
@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type TUICOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
CongestionControl string `json:"congestion_control,omitempty"`
|
||||
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
|
||||
UDPOverStream bool `json:"udp_over_stream,omitempty"`
|
||||
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
|
||||
Heartbeat string `json:"heartbeat,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
13
model/vless.go
Normal file
13
model/vless.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
type VLESSOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
UUID string `json:"uuid"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
PacketEncoding *string `json:"packet_encoding,omitempty"`
|
||||
}
|
34
model/vmess.go
Normal file
34
model/vmess.go
Normal file
@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
type VmessJson struct {
|
||||
V string `json:"v"`
|
||||
Ps string `json:"ps"`
|
||||
Add string `json:"add"`
|
||||
Port interface{} `json:"port"`
|
||||
Id string `json:"id"`
|
||||
Aid interface{} `json:"aid"`
|
||||
Scy string `json:"scy"`
|
||||
Net string `json:"net"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Tls string `json:"tls"`
|
||||
Sni string `json:"sni"`
|
||||
Alpn string `json:"alpn"`
|
||||
Fp string `json:"fp"`
|
||||
}
|
||||
|
||||
type VMessOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
UUID string `json:"uuid"`
|
||||
Security string `json:"security"`
|
||||
AlterId int `json:"alter_id,omitempty"`
|
||||
GlobalPadding bool `json:"global_padding,omitempty"`
|
||||
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
PacketEncoding string `json:"packet_encoding,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
28
model/wireguard.go
Normal file
28
model/wireguard.go
Normal file
@ -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"`
|
||||
}
|
24
parser/error.go
Normal file
24
parser/error.go
Normal file
@ -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)
|
||||
}
|
89
parser/hysteria.go
Normal file
89
parser/hysteria.go
Normal file
@ -0,0 +1,89 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
)
|
||||
|
||||
func ParseHysteria(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.HysteriaPrefix) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix)
|
||||
urlParts := strings.SplitN(proxy, "?", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '?' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[0], ":", 2)
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server, portStr := serverInfo[0], serverInfo[1]
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(urlParts[1])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := params.Get("protocol"), params.Get("auth"), params.Get("insecure"), params.Get("upmbps"), params.Get("downmbps"), params.Get("obfs"), params.Get("alpn")
|
||||
insecureBool, err := strconv.ParseBool(insecure)
|
||||
if err != nil {
|
||||
insecureBool = false
|
||||
}
|
||||
|
||||
var alpn []string
|
||||
alpnStr = strings.TrimSpace(alpnStr)
|
||||
if alpnStr != "" {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
}
|
||||
|
||||
remarks := server + ":" + portStr
|
||||
if params.Get("remarks") != "" {
|
||||
remarks = params.Get("remarks")
|
||||
}
|
||||
|
||||
return model.Outbound{
|
||||
Type: "hysteria",
|
||||
Tag: remarks,
|
||||
HysteriaOptions: model.HysteriaOutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: server,
|
||||
ServerPort: port,
|
||||
},
|
||||
Up: upmbps,
|
||||
Down: downmbps,
|
||||
Auth: []byte(auth),
|
||||
Obfs: obfs,
|
||||
Network: protocol,
|
||||
OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Insecure: insecureBool,
|
||||
ALPN: alpn,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
94
parser/hysteria2.go
Normal file
94
parser/hysteria2.go
Normal file
@ -0,0 +1,94 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
)
|
||||
|
||||
func ParseHysteria2(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) &&
|
||||
!strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1)
|
||||
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2)
|
||||
urlParts := strings.SplitN(proxy, "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
password := urlParts[0]
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "/?", 2)
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing params in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
paramStr := serverInfo[1]
|
||||
|
||||
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
|
||||
var server string
|
||||
var portStr string
|
||||
if len(serverAndPort) == 1 {
|
||||
portStr = "443"
|
||||
} else if len(serverAndPort) == 2 {
|
||||
server, portStr = serverAndPort[0], serverAndPort[1]
|
||||
} else {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(paramStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
remarks, network, obfs, obfsPassword, pinSHA256, insecure, sni := params.Get("name"), params.Get("network"), params.Get("obfs"), params.Get("obfs-password"), params.Get("pinSHA256"), params.Get("insecure"), params.Get("sni")
|
||||
enableTLS := pinSHA256 != ""
|
||||
insecureBool := insecure == "1"
|
||||
|
||||
result := model.Outbound{
|
||||
Type: "hysteria2",
|
||||
Tag: remarks,
|
||||
Hysteria2Options: model.Hysteria2OutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: server,
|
||||
ServerPort: uint16(port),
|
||||
},
|
||||
Password: password,
|
||||
Obfs: &model.Hysteria2Obfs{
|
||||
Type: obfs,
|
||||
Password: obfsPassword,
|
||||
},
|
||||
OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{Enabled: enableTLS,
|
||||
Insecure: insecureBool,
|
||||
ServerName: sni,
|
||||
Certificate: []string{pinSHA256}},
|
||||
},
|
||||
Network: network,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
16
parser/parsers_map.go
Normal file
16
parser/parsers_map.go
Normal file
@ -0,0 +1,16 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
)
|
||||
|
||||
var ParserMap map[string]func(string) (model.Outbound, error) = map[string]func(string) (model.Outbound, error){
|
||||
constant.ShadowsocksPrefix: ParseShadowsocks,
|
||||
constant.VMessPrefix: ParseVmess,
|
||||
constant.TrojanPrefix: ParseTrojan,
|
||||
constant.VLESSPrefix: ParseVless,
|
||||
constant.HysteriaPrefix: ParseHysteria,
|
||||
constant.Hysteria2Prefix1: ParseHysteria2,
|
||||
constant.Hysteria2Prefix2: ParseHysteria2,
|
||||
}
|
23
parser/port.go
Normal file
23
parser/port.go
Normal file
@ -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
|
||||
}
|
91
parser/shadowsocks.go
Normal file
91
parser/shadowsocks.go
Normal file
@ -0,0 +1,91 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
"sub2sing-box/util"
|
||||
)
|
||||
|
||||
func ParseShadowsocks(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix)
|
||||
urlParts := strings.SplitN(proxy, "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
var serverAndPort []string
|
||||
if !strings.Contains(urlParts[0], ":") {
|
||||
decoded, err := util.DecodeBase64(urlParts[0])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "invalid base64 encoded",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
urlParts[0] = decoded
|
||||
}
|
||||
credentials := strings.SplitN(urlParts[0], ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
method, password := credentials[0], credentials[1]
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "#", 2)
|
||||
serverAndPort = strings.SplitN(serverInfo[0], ":", 2)
|
||||
server, portStr := serverAndPort[0], serverAndPort[1]
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
|
||||
var remarks string
|
||||
if len(serverInfo) == 2 {
|
||||
unescape, err := url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "cannot unescape remarks",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
remarks = strings.TrimSpace(unescape)
|
||||
} else {
|
||||
remarks = strings.TrimSpace(server + ":" + portStr)
|
||||
}
|
||||
|
||||
result := model.Outbound{
|
||||
Type: "shadowsocks",
|
||||
Tag: remarks,
|
||||
ShadowsocksOptions: model.ShadowsocksOutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: server,
|
||||
ServerPort: port,
|
||||
},
|
||||
Method: method,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
157
parser/trojan.go
Normal file
157
parser/trojan.go
Normal file
@ -0,0 +1,157 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
)
|
||||
|
||||
func ParseTrojan(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix)
|
||||
urlParts := strings.SplitN(proxy, "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
password := strings.TrimSpace(urlParts[0])
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
if len(serverAndPortAndParams) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '?' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server, portStr := serverAndPort[0], serverAndPort[1]
|
||||
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
|
||||
} else {
|
||||
remarks = serverAndPort[0]
|
||||
}
|
||||
|
||||
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := params.Get("type"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("pbk"), params.Get("sid"), params.Get("fp"), params.Get("path"), params.Get("host"), params.Get("serviceName")
|
||||
|
||||
var alpn []string
|
||||
if strings.Contains(alpnStr, ",") {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
|
||||
enableUTLS := fp != ""
|
||||
|
||||
result := model.Outbound{
|
||||
Type: "trojan",
|
||||
Tag: remarks,
|
||||
TrojanOptions: model.TrojanOutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: server,
|
||||
ServerPort: port,
|
||||
},
|
||||
Password: password,
|
||||
Network: network,
|
||||
},
|
||||
}
|
||||
|
||||
if security == "xtls" || security == "tls" {
|
||||
result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ALPN: alpn,
|
||||
ServerName: sni,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("security") == "reality" {
|
||||
result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: sni,
|
||||
Reality: &model.OutboundRealityOptions{
|
||||
Enabled: true,
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
},
|
||||
UTLS: &model.OutboundUTLSOptions{
|
||||
Enabled: enableUTLS,
|
||||
Fingerprint: fp,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "ws" {
|
||||
result.TrojanOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: model.V2RayWebsocketOptions{
|
||||
Path: path,
|
||||
Headers: map[string]string{
|
||||
"Host": host,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "http" {
|
||||
result.TrojanOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: model.V2RayHTTPOptions{
|
||||
Host: []string{host},
|
||||
Path: params.Get("path"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "quic" {
|
||||
result.TrojanOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: model.V2RayQUICOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "grpc" {
|
||||
result.TrojanOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: model.V2RayGRPCOptions{
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
169
parser/vless.go
Normal file
169
parser/vless.go
Normal file
@ -0,0 +1,169 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
)
|
||||
|
||||
func ParseVless(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.VLESSPrefix) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
if len(serverAndPortAndParams) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '?' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server, portStr := serverAndPort[0], serverAndPort[1]
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
if strings.Contains(serverInfo[1], "|") {
|
||||
remarks = strings.SplitN(serverInfo[1], "|", 2)[1]
|
||||
} else {
|
||||
remarks, err = url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remarks, err = url.QueryUnescape(server)
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
}
|
||||
|
||||
uuid := strings.TrimSpace(urlParts[0])
|
||||
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName := params.Get("flow"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("allowInsecure"), params.Get("fp"), params.Get("pbk"), params.Get("sid"), params.Get("path"), params.Get("host"), params.Get("serviceName")
|
||||
|
||||
enableUTLS := fp != ""
|
||||
insecureBool := insecure == "1"
|
||||
var alpn []string
|
||||
if strings.Contains(alpnStr, ",") {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
|
||||
result := model.Outbound{
|
||||
Type: "vless",
|
||||
Tag: remarks,
|
||||
VLESSOptions: model.VLESSOutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: server,
|
||||
ServerPort: port,
|
||||
},
|
||||
UUID: uuid,
|
||||
Flow: flow,
|
||||
},
|
||||
}
|
||||
|
||||
if security == "tls" {
|
||||
result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ALPN: alpn,
|
||||
ServerName: sni,
|
||||
Insecure: insecureBool,
|
||||
},
|
||||
}
|
||||
result.VLESSOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{
|
||||
Enabled: enableUTLS,
|
||||
Fingerprint: fp,
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
result.VLESSOptions.OutboundTLSOptionsContainer.TLS.Reality = &model.OutboundRealityOptions{
|
||||
Enabled: true,
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "ws" {
|
||||
result.VLESSOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: model.V2RayWebsocketOptions{
|
||||
Path: path,
|
||||
},
|
||||
}
|
||||
result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = host
|
||||
}
|
||||
|
||||
if params.Get("type") == "quic" {
|
||||
result.VLESSOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: model.V2RayQUICOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "grpc" {
|
||||
result.VLESSOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: model.V2RayGRPCOptions{
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if params.Get("type") == "http" {
|
||||
hosts, err := url.QueryUnescape(host)
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
result.VLESSOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: model.V2RayHTTPOptions{
|
||||
Host: strings.Split(hosts, ","),
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
149
parser/vmess.go
Normal file
149
parser/vmess.go
Normal file
@ -0,0 +1,149 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/constant"
|
||||
"sub2sing-box/model"
|
||||
"sub2sing-box/util"
|
||||
)
|
||||
|
||||
func ParseVmess(proxy string) (model.Outbound, error) {
|
||||
if !strings.HasPrefix(proxy, constant.VMessPrefix) {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.VMessPrefix)
|
||||
base64, err := util.DecodeBase64(proxy)
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
|
||||
var vmess model.VmessJson
|
||||
err = json.Unmarshal([]byte(base64), &vmess)
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
|
||||
var port uint16
|
||||
switch vmess.Port.(type) {
|
||||
case string:
|
||||
port, err = ParsePort(vmess.Port.(string))
|
||||
if err != nil {
|
||||
return model.Outbound{}, err
|
||||
}
|
||||
case float64:
|
||||
port = uint16(vmess.Port.(float64))
|
||||
}
|
||||
|
||||
aid := 0
|
||||
switch vmess.Aid.(type) {
|
||||
case string:
|
||||
aid, err = strconv.Atoi(vmess.Aid.(string))
|
||||
if err != nil {
|
||||
return model.Outbound{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
case float64:
|
||||
aid = int(vmess.Aid.(float64))
|
||||
}
|
||||
|
||||
if vmess.Scy == "" {
|
||||
vmess.Scy = "auto"
|
||||
}
|
||||
|
||||
name, err := url.QueryUnescape(vmess.Ps)
|
||||
if err != nil {
|
||||
name = vmess.Ps
|
||||
}
|
||||
|
||||
result := model.Outbound{
|
||||
Type: "vmess",
|
||||
Tag: name,
|
||||
VMessOptions: model.VMessOutboundOptions{
|
||||
ServerOptions: model.ServerOptions{
|
||||
Server: vmess.Add,
|
||||
ServerPort: port,
|
||||
},
|
||||
UUID: vmess.Id,
|
||||
AlterId: aid,
|
||||
Security: vmess.Scy,
|
||||
},
|
||||
}
|
||||
|
||||
if vmess.Tls == "tls" {
|
||||
var alpn []string
|
||||
if strings.Contains(vmess.Alpn, ",") {
|
||||
alpn = strings.Split(vmess.Alpn, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
UTLS: &model.OutboundUTLSOptions{
|
||||
Fingerprint: vmess.Fp,
|
||||
},
|
||||
ALPN: alpn,
|
||||
ServerName: vmess.Sni,
|
||||
},
|
||||
}
|
||||
if vmess.Fp != "" {
|
||||
result.VMessOptions.OutboundTLSOptionsContainer.TLS.UTLS = &model.OutboundUTLSOptions{
|
||||
Enabled: true,
|
||||
Fingerprint: vmess.Fp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if vmess.Net == "ws" {
|
||||
if vmess.Path == "" {
|
||||
vmess.Path = "/"
|
||||
}
|
||||
if vmess.Host == "" {
|
||||
vmess.Host = vmess.Add
|
||||
}
|
||||
result.VMessOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: model.V2RayWebsocketOptions{
|
||||
Path: vmess.Path,
|
||||
Headers: map[string]string{
|
||||
"Host": vmess.Host,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if vmess.Net == "quic" {
|
||||
quic := model.V2RayQUICOptions{}
|
||||
result.VMessOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: quic,
|
||||
}
|
||||
}
|
||||
|
||||
if vmess.Net == "grpc" {
|
||||
grpc := model.V2RayGRPCOptions{
|
||||
ServiceName: vmess.Path,
|
||||
PermitWithoutStream: true,
|
||||
}
|
||||
result.VMessOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: grpc,
|
||||
}
|
||||
}
|
||||
|
||||
if vmess.Net == "h2" {
|
||||
httpOps := model.V2RayHTTPOptions{
|
||||
Host: strings.Split(vmess.Host, ","),
|
||||
Path: vmess.Path,
|
||||
}
|
||||
result.VMessOptions.Transport = &model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: httpOps,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
)
|
||||
|
||||
//hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
|
||||
//
|
||||
//- host: hostname or IP address of the server to connect to (required)
|
||||
//- port: port of the server to connect to (required)
|
||||
//- protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
|
||||
//- auth: authentication payload (string) (optional)
|
||||
//- peer: SNI for TLS (optional)
|
||||
//- insecure: ignore certificate errors (optional)
|
||||
//- upmbps: upstream bandwidth in Mbps (required)
|
||||
//- downmbps: downstream bandwidth in Mbps (required)
|
||||
//- alpn: QUIC ALPN (optional)
|
||||
//- obfs: Obfuscation mode (optional, empty or "xplus")
|
||||
//- obfsParam: Obfuscation password (optional)
|
||||
//- remarks: remarks (optional)
|
||||
|
||||
func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "hysteria://") {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2)
|
||||
serverInfo := strings.SplitN(parts[0], ":", 2)
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
params, err := url.ParseQuery(parts[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
host := serverInfo[0]
|
||||
port, err := strconv.Atoi(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
protocol := params.Get("protocol")
|
||||
auth := params.Get("auth")
|
||||
// peer := params.Get("peer")
|
||||
insecure := params.Get("insecure")
|
||||
upmbps := params.Get("upmbps")
|
||||
downmbps := params.Get("downmbps")
|
||||
obfs := params.Get("obfs")
|
||||
// obfsParam := params.Get("obfsParam")
|
||||
var alpn []string
|
||||
if params.Get("alpn") != "" {
|
||||
alpn = strings.Split(params.Get("alpn"), ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
remarks := ""
|
||||
if strings.Contains(parts[1], "#") {
|
||||
r := strings.Split(parts[1], "#")
|
||||
remarks = r[len(r)-1]
|
||||
} else {
|
||||
remarks = serverInfo[0] + ":" + serverInfo[1]
|
||||
}
|
||||
insecureBool, err := strconv.ParseBool(insecure)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
result := model.Proxy{
|
||||
Type: "hysteria",
|
||||
Tag: remarks,
|
||||
Hysteria: model.Hysteria{
|
||||
Server: host,
|
||||
ServerPort: uint16(port),
|
||||
Up: upmbps,
|
||||
Down: downmbps,
|
||||
Auth: []byte(auth),
|
||||
Obfs: obfs,
|
||||
Network: protocol,
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Insecure: insecureBool,
|
||||
ALPN: alpn,
|
||||
},
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
)
|
||||
|
||||
// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com
|
||||
|
||||
func ParseHysteria2(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2)
|
||||
serverInfo := strings.SplitN(parts[1], "/?", 2)
|
||||
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
|
||||
if len(serverAndPort) == 1 {
|
||||
serverAndPort = append(serverAndPort, "443")
|
||||
} else if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
}
|
||||
params, err := url.ParseQuery(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
}
|
||||
port, err := strconv.Atoi(serverAndPort[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
}
|
||||
remarks := params.Get("name")
|
||||
server := serverAndPort[0]
|
||||
password := parts[0]
|
||||
network := params.Get("network")
|
||||
result := model.Proxy{
|
||||
Type: "hysteria2",
|
||||
Tag: remarks,
|
||||
Hysteria2: model.Hysteria2{
|
||||
Server: server,
|
||||
ServerPort: uint16(port),
|
||||
Password: password,
|
||||
Obfs: &model.Hysteria2Obfs{
|
||||
Type: params.Get("obfs"),
|
||||
Password: params.Get("obfs-password"),
|
||||
},
|
||||
TLS: &model.OutboundTLSOptions{
|
||||
Enabled: params.Get("pinSHA256") != "",
|
||||
Insecure: params.Get("insecure") == "1",
|
||||
ServerName: params.Get("sni"),
|
||||
Certificate: []string{params.Get("pinSHA256")},
|
||||
},
|
||||
Network: network,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"sub2sing-box/internal/model"
|
||||
)
|
||||
|
||||
var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){
|
||||
"ss://": ParseShadowsocks,
|
||||
"vmess://": ParseVmess,
|
||||
"trojan://": ParseTrojan,
|
||||
"vless://": ParseVless,
|
||||
"hysteria://": ParseHysteria,
|
||||
"hy2://": ParseHysteria2,
|
||||
"hysteria2://": ParseHysteria2,
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
"sub2sing-box/internal/util"
|
||||
)
|
||||
|
||||
func ParseShadowsocks(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "ss://") {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
if !strings.Contains(parts[0], ":") {
|
||||
decoded, err := util.DecodeBase64(parts[0])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
parts[0] = decoded
|
||||
}
|
||||
credentials := strings.SplitN(parts[0], ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
unescape, err := url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
remarks = strings.TrimSpace(unescape)
|
||||
} else {
|
||||
remarks = strings.TrimSpace(serverAndPort[0])
|
||||
}
|
||||
method := credentials[0]
|
||||
password := credentials[1]
|
||||
server := strings.TrimSpace(serverAndPort[0])
|
||||
result := model.Proxy{
|
||||
Type: "shadowsocks",
|
||||
Tag: remarks,
|
||||
Shadowsocks: model.Shadowsocks{
|
||||
Method: method,
|
||||
Password: password,
|
||||
Server: server,
|
||||
ServerPort: uint16(port),
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
)
|
||||
|
||||
func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "trojan://") {
|
||||
return model.Proxy{}, errors.New("invalid trojan Url")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid trojan Url")
|
||||
}
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid trojan Url")
|
||||
}
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
|
||||
} else {
|
||||
remarks = serverAndPort[0]
|
||||
}
|
||||
server := strings.TrimSpace(serverAndPort[0])
|
||||
password := strings.TrimSpace(parts[0])
|
||||
result := model.Proxy{
|
||||
Type: "trojan",
|
||||
Tag: remarks,
|
||||
Trojan: model.Trojan{
|
||||
Server: server,
|
||||
ServerPort: uint16(port),
|
||||
Password: password,
|
||||
Network: params.Get("type"),
|
||||
},
|
||||
}
|
||||
if params.Get("security") == "xtls" || params.Get("security") == "tls" {
|
||||
var alpn []string
|
||||
if strings.Contains(params.Get("alpn"), ",") {
|
||||
alpn = strings.Split(params.Get("alpn"), ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
result.Trojan.TLS = &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ALPN: alpn,
|
||||
ServerName: params.Get("sni"),
|
||||
}
|
||||
}
|
||||
if params.Get("security") == "reality" {
|
||||
result.Trojan.TLS = &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: params.Get("sni"),
|
||||
Reality: &model.OutboundRealityOptions{
|
||||
Enabled: true,
|
||||
PublicKey: params.Get("pbk"),
|
||||
ShortID: params.Get("sid"),
|
||||
},
|
||||
UTLS: &model.OutboundUTLSOptions{
|
||||
Enabled: params.Get("fp") != "",
|
||||
Fingerprint: params.Get("fp"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "ws" {
|
||||
result.Trojan.Transport = &model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: model.V2RayWebsocketOptions{
|
||||
Path: params.Get("path"),
|
||||
Headers: map[string]string{
|
||||
"Host": params.Get("host"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "http" {
|
||||
result.Trojan.Transport = &model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: model.V2RayHTTPOptions{
|
||||
Host: []string{params.Get("host")},
|
||||
Path: params.Get("path"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "quic" {
|
||||
result.Trojan.Transport = &model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: model.V2RayQUICOptions{},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "grpc" {
|
||||
result.Trojan.Transport = &model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: model.V2RayGRPCOptions{
|
||||
ServiceName: params.Get("serviceName"),
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
)
|
||||
|
||||
func ParseVless(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "vless://") {
|
||||
return model.Proxy{}, errors.New("invalid vless Url")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid vless Url")
|
||||
}
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid vless Url")
|
||||
}
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
if strings.Contains(serverInfo[1], "|") {
|
||||
remarks = strings.SplitN(serverInfo[1], "|", 2)[1]
|
||||
} else {
|
||||
remarks, err = url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remarks, err = url.QueryUnescape(serverAndPort[0])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
}
|
||||
server := strings.TrimSpace(serverAndPort[0])
|
||||
uuid := strings.TrimSpace(parts[0])
|
||||
result := model.Proxy{
|
||||
Type: "vless",
|
||||
Tag: remarks,
|
||||
VLESS: model.VLESS{
|
||||
Server: server,
|
||||
ServerPort: uint16(port),
|
||||
UUID: uuid,
|
||||
Flow: params.Get("flow"),
|
||||
},
|
||||
}
|
||||
if params.Get("security") == "tls" {
|
||||
var alpn []string
|
||||
if strings.Contains(params.Get("alpn"), ",") {
|
||||
alpn = strings.Split(params.Get("alpn"), ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
result.VLESS.TLS = &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ALPN: alpn,
|
||||
Insecure: params.Get("allowInsecure") == "1",
|
||||
}
|
||||
}
|
||||
if params.Get("security") == "reality" {
|
||||
var alpn []string
|
||||
if strings.Contains(params.Get("alpn"), ",") {
|
||||
alpn = strings.Split(params.Get("alpn"), ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
result.VLESS.TLS = &model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: params.Get("sni"),
|
||||
UTLS: &model.OutboundUTLSOptions{
|
||||
Enabled: params.Get("fp") != "",
|
||||
Fingerprint: params.Get("fp"),
|
||||
},
|
||||
Reality: &model.OutboundRealityOptions{
|
||||
Enabled: true,
|
||||
PublicKey: params.Get("pbk"),
|
||||
ShortID: params.Get("sid"),
|
||||
},
|
||||
ALPN: alpn,
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "ws" {
|
||||
result.VLESS.Transport = &model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: model.V2RayWebsocketOptions{
|
||||
Path: params.Get("path"),
|
||||
Headers: map[string]string{
|
||||
"Host": params.Get("host"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "quic" {
|
||||
result.VLESS.Transport = &model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: model.V2RayQUICOptions{},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "grpc" {
|
||||
result.VLESS.Transport = &model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: model.V2RayGRPCOptions{
|
||||
ServiceName: params.Get("serviceName"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "http" {
|
||||
host, err := url.QueryUnescape(params.Get("host"))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
result.VLESS.Transport = &model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: model.V2RayHTTPOptions{
|
||||
Host: strings.Split(host, ","),
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2sing-box/internal/model"
|
||||
"sub2sing-box/internal/util"
|
||||
)
|
||||
|
||||
func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, "vmess://") {
|
||||
return model.Proxy{}, errors.New("invalid vmess url")
|
||||
}
|
||||
base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://"))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
}
|
||||
var vmess model.VmessJson
|
||||
err = json.Unmarshal([]byte(base64), &vmess)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
}
|
||||
port := 0
|
||||
switch vmess.Port.(type) {
|
||||
case string:
|
||||
port, err = strconv.Atoi(vmess.Port.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
}
|
||||
case float64:
|
||||
port = int(vmess.Port.(float64))
|
||||
}
|
||||
aid := 0
|
||||
switch vmess.Aid.(type) {
|
||||
case string:
|
||||
aid, err = strconv.Atoi(vmess.Aid.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
}
|
||||
case float64:
|
||||
aid = int(vmess.Aid.(float64))
|
||||
}
|
||||
if vmess.Scy == "" {
|
||||
vmess.Scy = "auto"
|
||||
}
|
||||
|
||||
name, err := url.QueryUnescape(vmess.Ps)
|
||||
if err != nil {
|
||||
name = vmess.Ps
|
||||
}
|
||||
|
||||
result := model.Proxy{
|
||||
Type: "vmess",
|
||||
Tag: name,
|
||||
VMess: model.VMess{
|
||||
Server: vmess.Add,
|
||||
ServerPort: uint16(port),
|
||||
UUID: vmess.Id,
|
||||
AlterId: aid,
|
||||
Security: vmess.Scy,
|
||||
},
|
||||
}
|
||||
|
||||
if vmess.Tls == "tls" {
|
||||
var alpn []string
|
||||
if strings.Contains(vmess.Alpn, ",") {
|
||||
alpn = strings.Split(vmess.Alpn, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
tls := model.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
UTLS: &model.OutboundUTLSOptions{
|
||||
Fingerprint: vmess.Fp,
|
||||
},
|
||||
ALPN: alpn,
|
||||
}
|
||||
result.VMess.TLS = &tls
|
||||
}
|
||||
|
||||
if vmess.Net == "ws" {
|
||||
if vmess.Path == "" {
|
||||
vmess.Path = "/"
|
||||
}
|
||||
if vmess.Host == "" {
|
||||
vmess.Host = vmess.Add
|
||||
}
|
||||
ws := model.V2RayWebsocketOptions{
|
||||
Path: vmess.Path,
|
||||
Headers: map[string]string{
|
||||
"Host": vmess.Host,
|
||||
},
|
||||
}
|
||||
transport := model.V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: ws,
|
||||
}
|
||||
result.VMess.Transport = &transport
|
||||
}
|
||||
|
||||
if vmess.Net == "quic" {
|
||||
quic := model.V2RayQUICOptions{}
|
||||
transport := model.V2RayTransportOptions{
|
||||
Type: "quic",
|
||||
QUICOptions: quic,
|
||||
}
|
||||
result.VMess.Transport = &transport
|
||||
}
|
||||
|
||||
if vmess.Net == "grpc" {
|
||||
grpc := model.V2RayGRPCOptions{
|
||||
ServiceName: vmess.Path,
|
||||
PermitWithoutStream: true,
|
||||
}
|
||||
transport := model.V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: grpc,
|
||||
}
|
||||
result.VMess.Transport = &transport
|
||||
}
|
||||
|
||||
if vmess.Net == "h2" {
|
||||
httpOps := model.V2RayHTTPOptions{
|
||||
Host: strings.Split(vmess.Host, ","),
|
||||
Path: vmess.Path,
|
||||
}
|
||||
transport := model.V2RayTransportOptions{
|
||||
Type: "http",
|
||||
HTTPOptions: httpOps,
|
||||
}
|
||||
result.VMess.Transport = &transport
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
252
templates/tun-without-dns-leaks-country-group.json
Normal file
252
templates/tun-without-dns-leaks-country-group.json
Normal file
@ -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": ["<all-country-tags>", "direct"],
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Ads",
|
||||
"outbounds": ["direct", "block"],
|
||||
"default": "block",
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Microsoft",
|
||||
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||
"default": "节点选择",
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Bilibili",
|
||||
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||
"default": "direct",
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Games(全球)",
|
||||
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||
"default": "节点选择",
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Games(中国)",
|
||||
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||
"default": "direct",
|
||||
"interrupt_exist_connections": true
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "Bahamut",
|
||||
"outbounds": ["节点选择", "<all-country-tags>", "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": "节点选择"
|
||||
}
|
||||
}
|
||||
}
|
11
test/country_test.go
Normal file
11
test/country_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sub2sing-box/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCountry(t *testing.T) {
|
||||
log.Println(model.GetContryName("US 节点"))
|
||||
}
|
25
util/marshal.go
Normal file
25
util/marshal.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user