diff --git a/.gitignore b/.gitignore index 16db01c..8e47708 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist *test.go template.json +.idea \ No newline at end of file diff --git a/common/convert.go b/common/convert.go index f1e4574..e975560 100644 --- a/common/convert.go +++ b/common/convert.go @@ -9,7 +9,8 @@ import ( "regexp" "sort" "strings" - model2 "sub2sing-box/model" + C "sub2sing-box/constant" + "sub2sing-box/model" "sub2sing-box/parser" "sub2sing-box/util" ) @@ -28,7 +29,7 @@ func Convert( result := "" var err error - proxyList, err := ConvertSubscriptionsToSProxy(subscriptions) + outbounds, err := ConvertSubscriptionsToSProxy(subscriptions) if err != nil { return "", err } @@ -37,18 +38,18 @@ func Convert( if err != nil { return "", err } - proxyList = append(proxyList, p) + outbounds = append(outbounds, p) } if delete != "" { - proxyList, err = DeleteProxy(proxyList, delete) + outbounds, err = DeleteProxy(outbounds, delete) if err != nil { return "", err } } for k, v := range rename { - proxyList, err = RenameProxy(proxyList, k, v) + outbounds, err = RenameProxy(outbounds, k, v) if err != nil { return "", err } @@ -56,14 +57,14 @@ func Convert( keep := make(map[int]bool) set := make(map[string]struct { - Proxy model2.Proxy + Proxy model.Outbound Count int }) - for i, p := range proxyList { + for i, p := range outbounds { if _, exists := set[p.Tag]; !exists { keep[i] = true set[p.Tag] = struct { - Proxy model2.Proxy + Proxy model.Outbound Count int }{p, 0} } else { @@ -71,32 +72,16 @@ func Convert( p2, _ := json.Marshal(set[p.Tag]) if string(p1) != string(p2) { set[p.Tag] = struct { - Proxy model2.Proxy + Proxy model.Outbound Count int }{p, set[p.Tag].Count + 1} keep[i] = true - proxyList[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count) + outbounds[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count) } else { keep[i] = false } } } - var newProxyList []model2.Proxy - for i, p := range proxyList { - if keep[i] { - newProxyList = append(newProxyList, p) - } - } - proxyList = newProxyList - var outbounds []model2.Outbound - ps, err := json.Marshal(&proxyList) - if err != nil { - return "", err - } - err = json.Unmarshal(ps, &outbounds) - if err != nil { - return "", err - } if group { outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType) } @@ -116,52 +101,65 @@ func Convert( return string(result), nil } -func AddCountryGroup(proxies []model2.Outbound, groupType string, sortKey string, sortType string) []model2.Outbound { - newGroup := make(map[string]model2.Outbound) +func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound { + newGroup := make(map[string]model.Outbound) for _, p := range proxies { - if p.Type != "selector" && p.Type != "urltest" { - country := model2.GetContryName(p.Tag) + if p.Type != C.TypeSelector && p.Type != C.TypeURLTest { + country := model.GetContryName(p.Tag) if group, ok := newGroup[country]; ok { - group.Outbounds = append(group.Outbounds, p.Tag) + group.SetOutbounds(append(group.GetOutbounds(), p.Tag)) newGroup[country] = group } else { - newGroup[country] = model2.Outbound{ - Tag: country, - Type: groupType, - Outbounds: []string{p.Tag}, - InterruptExistConnections: true, + if groupType == C.TypeSelector { + newGroup[country] = model.Outbound{ + Tag: country, + Type: groupType, + SelectorOptions: model.SelectorOutboundOptions{ + Outbounds: []string{p.Tag}, + InterruptExistConnections: true, + }, + } + } else if groupType == C.TypeURLTest { + newGroup[country] = model.Outbound{ + Tag: country, + Type: groupType, + URLTestOptions: model.URLTestOutboundOptions{ + Outbounds: []string{p.Tag}, + InterruptExistConnections: true, + }, + } } } } } - var groups []model2.Outbound + var groups []model.Outbound for _, p := range newGroup { groups = append(groups, p) } if sortType == "asc" { switch sortKey { case "tag": - sort.Sort(model2.SortByTag(groups)) + sort.Sort(model.SortByTag(groups)) case "num": - sort.Sort(model2.SortByNumber(groups)) + sort.Sort(model.SortByNumber(groups)) default: - sort.Sort(model2.SortByTag(groups)) + sort.Sort(model.SortByTag(groups)) } } else { switch sortKey { case "tag": - sort.Sort(sort.Reverse(model2.SortByTag(groups))) + sort.Sort(sort.Reverse(model.SortByTag(groups))) case "num": - sort.Sort(sort.Reverse(model2.SortByNumber(groups))) + sort.Sort(sort.Reverse(model.SortByNumber(groups))) default: - sort.Sort(sort.Reverse(model2.SortByTag(groups))) + sort.Sort(sort.Reverse(model.SortByTag(groups))) } } return append(proxies, groups...) } -func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) { - var config model2.Config +func MergeTemplate(outbounds []model.Outbound, template string) (string, error) { + var config model.Config var err error if strings.HasPrefix(template, "http") { data, err := util.Fetch(template, 3) @@ -179,15 +177,15 @@ func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) } } config, err = ReadTemplate(template) + if err != nil { + return "", err + } } proxyTags := make([]string, 0) groupTags := make([]string, 0) - groups := make(map[string]model2.Outbound) - if err != nil { - return "", err - } + groups := make(map[string]model.Outbound) for _, p := range outbounds { - if model2.IsCountryGroup(p.Tag) { + if model.IsCountryGroup(p.Tag) { groupTags = append(groupTags, p.Tag) reg := regexp.MustCompile("[A-Za-z]{2}") country := reg.FindString(p.Tag) @@ -198,22 +196,24 @@ func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) } reg := regexp.MustCompile("<[A-Za-z]{2}>") for i, outbound := range config.Outbounds { - var parsedOutbound []string = make([]string, 0) - for _, o := range outbound.Outbounds { - if o == "" { - parsedOutbound = append(parsedOutbound, proxyTags...) - } else if o == "" { - parsedOutbound = append(parsedOutbound, groupTags...) - } else if reg.MatchString(o) { - country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>")) - if group, ok := groups[country]; ok { - parsedOutbound = append(parsedOutbound, group.Outbounds...) + if outbound.Type == C.TypeSelector || outbound.Type == C.TypeURLTest { + var parsedOutbound []string = make([]string, 0) + for _, o := range outbound.GetOutbounds() { + if o == "" { + parsedOutbound = append(parsedOutbound, proxyTags...) + } else if o == "" { + parsedOutbound = append(parsedOutbound, groupTags...) + } else if reg.MatchString(o) { + country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>")) + if group, ok := groups[country]; ok { + parsedOutbound = append(parsedOutbound, group.GetOutbounds()...) + } + } else { + parsedOutbound = append(parsedOutbound, o) } - } else { - parsedOutbound = append(parsedOutbound, o) } + config.Outbounds[i].SetOutbounds(parsedOutbound) } - config.Outbounds[i].Outbounds = parsedOutbound } config.Outbounds = append(config.Outbounds, outbounds...) data, err := json.Marshal(config) @@ -223,17 +223,17 @@ func MergeTemplate(outbounds []model2.Outbound, template string) (string, error) return string(data), nil } -func ConvertCProxyToSProxy(proxy string) (model2.Proxy, error) { +func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) { for prefix, parseFunc := range parser.ParserMap { if strings.HasPrefix(proxy, prefix) { proxy, err := parseFunc(proxy) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } return proxy, nil } } - return model2.Proxy{}, errors.New("Unknown proxy format") + return model.Outbound{}, errors.New("Unknown proxy format") } func ConvertCProxyToJson(proxy string) (string, error) { @@ -248,8 +248,8 @@ func ConvertCProxyToJson(proxy string) (string, error) { return string(data), nil } -func ConvertSubscriptionsToSProxy(urls []string) ([]model2.Proxy, error) { - proxyList := make([]model2.Proxy, 0) +func ConvertSubscriptionsToSProxy(urls []string) ([]model.Outbound, error) { + proxyList := make([]model.Outbound, 0) for _, url := range urls { data, err := util.Fetch(url, 3) if err != nil { @@ -287,25 +287,25 @@ func ConvertSubscriptionsToJson(urls []string) (string, error) { return string(result), nil } -func ReadTemplate(path string) (model2.Config, error) { +func ReadTemplate(path string) (model.Config, error) { data, err := os.ReadFile(path) if err != nil { - return model2.Config{}, err + return model.Config{}, err } - var res model2.Config + var res model.Config err = json.Unmarshal(data, &res) if err != nil { - return model2.Config{}, err + return model.Config{}, err } return res, nil } -func DeleteProxy(proxies []model2.Proxy, regex string) ([]model2.Proxy, error) { +func DeleteProxy(proxies []model.Outbound, regex string) ([]model.Outbound, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err } - var newProxies []model2.Proxy + var newProxies []model.Outbound for _, p := range proxies { if !reg.MatchString(p.Tag) { newProxies = append(newProxies, p) @@ -314,7 +314,7 @@ func DeleteProxy(proxies []model2.Proxy, regex string) ([]model2.Proxy, error) { return newProxies, nil } -func RenameProxy(proxies []model2.Proxy, regex string, replaceText string) ([]model2.Proxy, error) { +func RenameProxy(proxies []model.Outbound, regex string, replaceText string) ([]model.Outbound, error) { reg, err := regexp.Compile(regex) if err != nil { return nil, err diff --git a/constant/proxy.go b/constant/proxy.go new file mode 100644 index 0000000..ba4e3bf --- /dev/null +++ b/constant/proxy.go @@ -0,0 +1,31 @@ +package constant + +const ( + TypeTun = "tun" + TypeRedirect = "redirect" + TypeTProxy = "tproxy" + TypeDirect = "direct" + TypeBlock = "block" + TypeDNS = "dns" + TypeSOCKS = "socks" + TypeHTTP = "http" + TypeMixed = "mixed" + TypeShadowsocks = "shadowsocks" + TypeVMess = "vmess" + TypeTrojan = "trojan" + TypeNaive = "naive" + TypeWireGuard = "wireguard" + TypeHysteria = "hysteria" + TypeTor = "tor" + TypeSSH = "ssh" + TypeShadowTLS = "shadowtls" + TypeShadowsocksR = "shadowsocksr" + TypeVLESS = "vless" + TypeTUIC = "tuic" + TypeHysteria2 = "hysteria2" +) + +const ( + TypeSelector = "selector" + TypeURLTest = "urltest" +) diff --git a/go.mod b/go.mod index 89786ee..8e73ba2 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module sub2sing-box go 1.21.5 -require github.com/spf13/cobra v1.8.0 +require ( + github.com/spf13/cobra v1.8.0 + golang.org/x/text v0.9.0 +) require ( github.com/bytedance/sonic v1.9.1 // indirect @@ -26,7 +29,6 @@ require ( golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/model/config.go b/model/config.go index b587140..052c607 100644 --- a/model/config.go +++ b/model/config.go @@ -51,8 +51,8 @@ type Config struct { Log *LogOptions `json:"log,omitempty"` DNS *DNSOptions `json:"dns,omitempty"` NTP *NTPOptions `json:"ntp,omitempty"` - Inbounds Listable[Inbound] `json:"inbounds,omitempty"` - Outbounds Listable[Outbound] `json:"outbounds,omitempty"` + Inbounds []Inbound `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` Route *RouteOptions `json:"route,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"` } @@ -65,12 +65,12 @@ type LogOptions struct { } type DNSOptions struct { - Servers Listable[DNSServerOptions] `json:"servers,omitempty"` - Rules Listable[DNSRule] `json:"rules,omitempty"` - Final string `json:"final,omitempty"` - ReverseMapping bool `json:"reverse_mapping,omitempty"` - FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"` - Strategy string `json:"strategy,omitempty"` + Servers Listable[DNSServerOptions] `json:"servers,omitempty"` + Rules Listable[DNSRule] `json:"rules,omitempty"` + Final string `json:"final,omitempty"` + ReverseMapping bool `json:"reverse_mapping,omitempty"` + FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"` + Strategy string `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` @@ -162,29 +162,29 @@ type Inbound struct { Tag string `json:"tag,omitempty"` InterfaceName string `json:"interface_name,omitempty"` MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Inet4Address Listable[string] `json:"inet4_address,omitempty"` - Inet6Address Listable[string] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[string] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[string] `json:"inet6_route_address,omitempty"` - Inet4RouteExcludeAddress Listable[string] `json:"inet4_route_exclude_address,omitempty"` - Inet6RouteExcludeAddress Listable[string] `json:"inet6_route_exclude_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` + GSO bool `json:"gso,omitempty"` + Inet4Address Listable[string] `json:"inet4_address,omitempty"` + Inet6Address Listable[string] `json:"inet6_address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + Inet4RouteAddress Listable[string] `json:"inet4_route_address,omitempty"` + Inet6RouteAddress Listable[string] `json:"inet6_route_address,omitempty"` + Inet4RouteExcludeAddress Listable[string] `json:"inet4_route_exclude_address,omitempty"` + Inet6RouteExcludeAddress Listable[string] `json:"inet6_route_exclude_address,omitempty"` + IncludeInterface Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` + IncludePackage Listable[string] `json:"include_package,omitempty"` + ExcludePackage Listable[string] `json:"exclude_package,omitempty"` + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` UDPTimeout string `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - SniffEnabled bool `json:"sniff,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` + SniffEnabled bool `json:"sniff,omitempty"` SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` SniffTimeout string `json:"sniff_timeout,omitempty"` DomainStrategy string `json:"domain_strategy,omitempty"` @@ -203,111 +203,12 @@ type HTTPProxyOptions struct { MatchDomain Listable[string] `json:"match_domain,omitempty"` } -type Outbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress string `json:"inet4_bind_address,omitempty"` - Inet6BindAddress string `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout string `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - DomainStrategy string `json:"domain_strategy,omitempty"` - FallbackDelay string `json:"fallback_delay,omitempty"` - OverrideAddress string `json:"override_address,omitempty"` - OverridePort uint16 `json:"override_port,omitempty"` - ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` - Server string `json:"server,omitempty"` - ServerPort uint16 `json:"server_port,omitempty"` - Version string `json:"version,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Path string `json:"path,omitempty"` - Headers map[string]Listable[string] `json:"headers,omitempty"` - Method string `json:"method,omitempty"` - Plugin string `json:"plugin,omitempty"` - PluginOptions string `json:"plugin_opts,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - UUID string `json:"uuid,omitempty"` - Security string `json:"security,omitempty"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - SystemInterface bool `json:"system_interface,omitempty"` - GSO bool `json:"gso,omitempty"` - InterfaceName string `json:"interface_name,omitempty"` - LocalAddress Listable[string] `json:"local_address,omitempty"` - PrivateKey string `json:"private_key,omitempty"` - Peers Listable[WireGuardPeer] `json:"peers,omitempty"` - PeerPublicKey string `json:"peer_public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - Reserved Listable[uint8] `json:"reserved,omitempty"` - Workers int `json:"workers,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Obfs `json:"obfs,omitempty"` - Auth Listable[byte] `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - ExecutablePath string `json:"executable_path,omitempty"` - ExtraArgs Listable[string] `json:"extra_args,omitempty"` - DataDirectory string `json:"data_directory,omitempty"` - Options map[string]string `json:"torrc,omitempty"` - User string `json:"user,omitempty"` - PrivateKeyPath string `json:"private_key_path,omitempty"` - PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` - HostKey Listable[string] `json:"host_key,omitempty"` - HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"` - ClientVersion string `json:"client_version,omitempty"` - ObfsParam string `json:"obfs_param,omitempty"` - Protocol string `json:"protocol,omitempty"` - ProtocolParam string `json:"protocol_param,omitempty"` - Flow string `json:"flow,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` - Default string `json:"default,omitempty"` - Outbounds Listable[string] `json:"outbounds,omitempty"` - URL string `json:"url,omitempty"` - Interval string `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - IdleTimeout string `json:"idle_timeout,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type WireGuardPeer struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - PublicKey string `json:"public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - AllowedIPs Listable[string] `json:"allowed_ips,omitempty"` - Reserved Listable[uint8] `json:"reserved,omitempty"` -} - type RouteOptions struct { - GeoIP *GeoIPOptions `json:"geoip,omitempty"` - Geosite *GeositeOptions `json:"geosite,omitempty"` - Rules Listable[Rule] `json:"rules,omitempty"` - RuleSet Listable[RuleSet] `json:"rule_set,omitempty"` - Final string `json:"final,omitempty"` + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Geosite *GeositeOptions `json:"geosite,omitempty"` + Rules Listable[Rule] `json:"rules,omitempty"` + RuleSet Listable[RuleSet] `json:"rule_set,omitempty"` + Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` diff --git a/model/direct.go b/model/direct.go new file mode 100644 index 0000000..64464b6 --- /dev/null +++ b/model/direct.go @@ -0,0 +1,8 @@ +package model + +type DirectOutboundOptions struct { + DialerOptions + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` + ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` +} diff --git a/model/group.go b/model/group.go new file mode 100644 index 0000000..aa0511b --- /dev/null +++ b/model/group.go @@ -0,0 +1,16 @@ +package model + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval string `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout string `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/model/hysteria.go b/model/hysteria.go index bf299af..91d0e73 100644 --- a/model/hysteria.go +++ b/model/hysteria.go @@ -1,18 +1,18 @@ package model -type Hysteria struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Auth []byte `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` +type HysteriaOutboundOptions struct { + DialerOptions + ServerOptions + Up string `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down string `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindow uint64 `json:"recv_window,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer } diff --git a/model/hysteria2.go b/model/hysteria2.go index 066a341..5a2f117 100644 --- a/model/hysteria2.go +++ b/model/hysteria2.go @@ -5,14 +5,14 @@ type Hysteria2Obfs struct { Password string `json:"password,omitempty"` } -type Hysteria2 struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` +type Hysteria2OutboundOptions struct { + DialerOptions + ServerOptions + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/model/outbound.go b/model/outbound.go new file mode 100644 index 0000000..62a2f9d --- /dev/null +++ b/model/outbound.go @@ -0,0 +1,167 @@ +package model + +import ( + "encoding/json" + "errors" + "reflect" + C "sub2sing-box/constant" + "sub2sing-box/util" +) + +type _Outbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + DirectOptions DirectOutboundOptions `json:"-"` + SocksOptions SocksOutboundOptions `json:"-"` + HTTPOptions HTTPOutboundOptions `json:"-"` + ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` + VMessOptions VMessOutboundOptions `json:"-"` + TrojanOptions TrojanOutboundOptions `json:"-"` + WireGuardOptions WireGuardOutboundOptions `json:"-"` + HysteriaOptions HysteriaOutboundOptions `json:"-"` + TorOptions TorOutboundOptions `json:"-"` + SSHOptions SSHOutboundOptions `json:"-"` + ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"` + ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"` + VLESSOptions VLESSOutboundOptions `json:"-"` + TUICOptions TUICOutboundOptions `json:"-"` + Hysteria2Options Hysteria2OutboundOptions `json:"-"` + SelectorOptions SelectorOutboundOptions `json:"-"` + URLTestOptions URLTestOutboundOptions `json:"-"` +} +type Outbound _Outbound + +func (h *Outbound) RawOptions() (any, error) { + var rawOptionsPtr any + switch h.Type { + case C.TypeDirect: + rawOptionsPtr = &h.DirectOptions + case C.TypeBlock, C.TypeDNS: + rawOptionsPtr = nil + case C.TypeSOCKS: + rawOptionsPtr = &h.SocksOptions + case C.TypeHTTP: + rawOptionsPtr = &h.HTTPOptions + case C.TypeShadowsocks: + rawOptionsPtr = &h.ShadowsocksOptions + case C.TypeVMess: + rawOptionsPtr = &h.VMessOptions + case C.TypeTrojan: + rawOptionsPtr = &h.TrojanOptions + case C.TypeWireGuard: + rawOptionsPtr = &h.WireGuardOptions + case C.TypeHysteria: + rawOptionsPtr = &h.HysteriaOptions + case C.TypeTor: + rawOptionsPtr = &h.TorOptions + case C.TypeSSH: + rawOptionsPtr = &h.SSHOptions + case C.TypeShadowTLS: + rawOptionsPtr = &h.ShadowTLSOptions + case C.TypeShadowsocksR: + rawOptionsPtr = &h.ShadowsocksROptions + case C.TypeVLESS: + rawOptionsPtr = &h.VLESSOptions + case C.TypeTUIC: + rawOptionsPtr = &h.TUICOptions + case C.TypeHysteria2: + rawOptionsPtr = &h.Hysteria2Options + case C.TypeSelector: + rawOptionsPtr = &h.SelectorOptions + case C.TypeURLTest: + rawOptionsPtr = &h.URLTestOptions + case "": + return nil, errors.New("missing outbound type") + default: + return nil, errors.New("unknown outbound type: " + h.Type) + } + return rawOptionsPtr, nil +} + +func (h *Outbound) MarshalJSON() ([]byte, error) { + rawOptions, err := h.RawOptions() + if err != nil { + return nil, err + } + result, err := util.MergeAndMarshal(struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + }{ + Type: h.Type, + Tag: h.Tag, + }, rawOptions) + if err != nil { + return nil, err + } + return []byte(result), nil +} + +func (h *Outbound) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_Outbound)(h)) + if err != nil { + return err + } + rawOptions, err := h.RawOptions() + if err != nil { + return err + } + if rawOptions == nil { + return nil + } + err = json.Unmarshal(bytes, rawOptions) + if err != nil { + return err + } + rawOptionsType := reflect.TypeOf(rawOptions).Elem() + hValue := reflect.ValueOf(h).Elem() + for i := 0; i < hValue.NumField(); i++ { + fieldType := hValue.Field(i).Type() + if fieldType == rawOptionsType { + hValue.Field(i).Set(reflect.ValueOf(rawOptions).Elem()) + return nil + } + } + return errors.New("unknown outbound type: " + h.Type) +} + +func (h *Outbound) GetOutbounds() []string { + if h.Type == C.TypeSelector { + return h.SelectorOptions.Outbounds + } + if h.Type == C.TypeURLTest { + return h.URLTestOptions.Outbounds + } + return nil +} + +func (h *Outbound) SetOutbounds(outbounds []string) { + if h.Type == C.TypeSelector { + h.SelectorOptions.Outbounds = outbounds + } + if h.Type == C.TypeURLTest { + h.URLTestOptions.Outbounds = outbounds + } +} + +type DialerOptions struct { + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress string `json:"inet4_bind_address,omitempty"` + Inet6BindAddress string `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark int `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout string `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy string `json:"domain_strategy,omitempty"` + FallbackDelay string `json:"fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` +} + +type ServerOptions struct { + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` +} diff --git a/model/proxy.go b/model/proxy.go deleted file mode 100644 index 2c76e55..0000000 --- a/model/proxy.go +++ /dev/null @@ -1,94 +0,0 @@ -package model - -import ( - "encoding/json" -) - -type Proxy struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Shadowsocks `json:"-"` - VMess `json:"-"` - VLESS `json:"-"` - Trojan `json:"-"` - TUIC `json:"-"` - Hysteria `json:"-"` - Hysteria2 `json:"-"` -} - -func (p *Proxy) MarshalJSON() ([]byte, error) { - switch p.Type { - case "shadowsocks": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Shadowsocks - }{ - Type: p.Type, - Tag: p.Tag, - Shadowsocks: p.Shadowsocks, - }) - case "vmess": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - VMess - }{ - Type: p.Type, - Tag: p.Tag, - VMess: p.VMess, - }) - case "vless": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - VLESS - }{ - Type: p.Type, - Tag: p.Tag, - VLESS: p.VLESS, - }) - case "trojan": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Trojan - }{ - Type: p.Type, - Tag: p.Tag, - Trojan: p.Trojan, - }) - case "tuic": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - TUIC - }{ - Type: p.Type, - Tag: p.Tag, - TUIC: p.TUIC, - }) - case "hysteria": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Hysteria - }{ - Type: p.Type, - Tag: p.Tag, - Hysteria: p.Hysteria, - }) - case "hysteria2": - return json.Marshal(&struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - Hysteria2 - }{ - Type: p.Type, - Tag: p.Tag, - Hysteria2: p.Hysteria2, - }) - default: - return json.Marshal(p) - } -} diff --git a/model/shadowsocks.go b/model/shadowsocks.go index 5cdc8b1..038de5e 100644 --- a/model/shadowsocks.go +++ b/model/shadowsocks.go @@ -1,8 +1,8 @@ package model -type Shadowsocks struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` +type ShadowsocksOutboundOptions struct { + DialerOptions + ServerOptions Method string `json:"method"` Password string `json:"password"` Plugin string `json:"plugin,omitempty"` diff --git a/model/shadowsocksr.go b/model/shadowsocksr.go new file mode 100644 index 0000000..7cbc051 --- /dev/null +++ b/model/shadowsocksr.go @@ -0,0 +1,13 @@ +package model + +type ShadowsocksROutboundOptions struct { + DialerOptions + ServerOptions + Method string `json:"method"` + Password string `json:"password"` + Obfs string `json:"obfs,omitempty"` + ObfsParam string `json:"obfs_param,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolParam string `json:"protocol_param,omitempty"` + Network string `json:"network,omitempty"` +} diff --git a/model/shadowtls.go b/model/shadowtls.go new file mode 100644 index 0000000..30cd488 --- /dev/null +++ b/model/shadowtls.go @@ -0,0 +1,19 @@ +package model + +type ShadowTLSUser struct { + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` +} + +type ShadowTLSHandshakeOptions struct { + ServerOptions + DialerOptions +} + +type ShadowTLSOutboundOptions struct { + DialerOptions + ServerOptions + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/model/simple.go b/model/simple.go new file mode 100644 index 0000000..adf7439 --- /dev/null +++ b/model/simple.go @@ -0,0 +1,21 @@ +package model + +type SocksOutboundOptions struct { + DialerOptions + ServerOptions + Version string `json:"version,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network Listable[string] `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` +} + +type HTTPOutboundOptions struct { + DialerOptions + ServerOptions + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer + Path string `json:"path,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} diff --git a/model/sort.go b/model/sort.go index 737644d..e80e8eb 100644 --- a/model/sort.go +++ b/model/sort.go @@ -1,15 +1,32 @@ package model import ( + C "sub2sing-box/constant" + "golang.org/x/text/collate" "golang.org/x/text/language" ) type SortByNumber []Outbound -func (a SortByNumber) Len() int { return len(a) } -func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortByNumber) Less(i, j int) bool { return len(a[i].Outbounds) < len(a[j].Outbounds) } +func (a SortByNumber) Len() int { return len(a) } +func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortByNumber) Less(i, j int) bool { + var size1, size2 int + if a[i].Type == C.TypeSelector { + size1 = len(a[i].SelectorOptions.Outbounds) + } + if a[i].Type == C.TypeURLTest { + size1 = len(a[j].URLTestOptions.Outbounds) + } + if a[j].Type == C.TypeSelector { + size2 = len(a[j].SelectorOptions.Outbounds) + } + if a[j].Type == C.TypeURLTest { + size2 = len(a[j].URLTestOptions.Outbounds) + } + return size1 < size2 +} type SortByTag []Outbound diff --git a/model/ssh.go b/model/ssh.go new file mode 100644 index 0000000..3aa962f --- /dev/null +++ b/model/ssh.go @@ -0,0 +1,14 @@ +package model + +type SSHOutboundOptions struct { + DialerOptions + ServerOptions + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + PrivateKey Listable[string] `json:"private_key,omitempty"` + PrivateKeyPath string `json:"private_key_path,omitempty"` + PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` + HostKey Listable[string] `json:"host_key,omitempty"` + HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"` + ClientVersion string `json:"client_version,omitempty"` +} diff --git a/model/tls.go b/model/tls.go index 6542944..64426de 100644 --- a/model/tls.go +++ b/model/tls.go @@ -34,3 +34,7 @@ type OutboundRealityOptions struct { PublicKey string `json:"public_key,omitempty"` ShortID string `json:"short_id,omitempty"` } + +type OutboundTLSOptionsContainer struct { + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/model/tor.go b/model/tor.go new file mode 100644 index 0000000..4e04421 --- /dev/null +++ b/model/tor.go @@ -0,0 +1,9 @@ +package model + +type TorOutboundOptions struct { + DialerOptions + ExecutablePath string `json:"executable_path,omitempty"` + ExtraArgs []string `json:"extra_args,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + Options map[string]string `json:"torrc,omitempty"` +} diff --git a/model/trojan.go b/model/trojan.go index 5e827fa..a1176c0 100644 --- a/model/trojan.go +++ b/model/trojan.go @@ -1,11 +1,11 @@ package model -type Trojan struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - Password string `json:"password"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` +type TrojanOutboundOptions struct { + DialerOptions + ServerOptions + Password string `json:"password"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/model/tuic.go b/model/tuic.go index 304c57d..6cf3c91 100644 --- a/model/tuic.go +++ b/model/tuic.go @@ -1,15 +1,15 @@ package model -type TUIC struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` +type TUICOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat string `json:"heartbeat,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer } diff --git a/model/v2ray_transport.go b/model/v2ray_transport.go index 945014e..46ac7a6 100644 --- a/model/v2ray_transport.go +++ b/model/v2ray_transport.go @@ -53,8 +53,8 @@ func (o *V2RayTransportOptions) MarshalJSON() ([]byte, error) { } type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` + Host Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` Method string `json:"method,omitempty"` Headers map[string]string `json:"headers,omitempty"` IdleTimeout string `json:"idle_timeout,omitempty"` diff --git a/model/vless.go b/model/vless.go index 74e355e..d656087 100644 --- a/model/vless.go +++ b/model/vless.go @@ -1,12 +1,12 @@ package model -type VLESS struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` +type VLESSOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` PacketEncoding *string `json:"packet_encoding,omitempty"` diff --git a/model/vmess.go b/model/vmess.go index 472bbfb..2063987 100644 --- a/model/vmess.go +++ b/model/vmess.go @@ -18,17 +18,17 @@ type VmessJson struct { Fp string `json:"fp"` } -type VMess struct { - Server string `json:"server"` - ServerPort uint16 `json:"server_port"` - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network string `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` +type VMessOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network string `json:"network,omitempty"` + OutboundTLSOptionsContainer + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/model/wireguard.go b/model/wireguard.go new file mode 100644 index 0000000..5d1dfd2 --- /dev/null +++ b/model/wireguard.go @@ -0,0 +1,28 @@ +package model + +import "net/netip" + +type WireGuardOutboundOptions struct { + DialerOptions + SystemInterface bool `json:"system_interface,omitempty"` + GSO bool `json:"gso,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + LocalAddress Listable[netip.Prefix] `json:"local_address"` + PrivateKey string `json:"private_key"` + Peers []WireGuardPeer `json:"peers,omitempty"` + ServerOptions + PeerPublicKey string `json:"peer_public_key"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` + Workers int `json:"workers,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Network string `json:"network,omitempty"` +} + +type WireGuardPeer struct { + ServerOptions + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs Listable[string] `json:"allowed_ips,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` +} diff --git a/parser/hysteria.go b/parser/hysteria.go index ab1fbb9..854e910 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) //hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks @@ -23,23 +23,23 @@ import ( //- obfsParam: Obfuscation password (optional) //- remarks: remarks (optional) -func ParseHysteria(proxy string) (model2.Proxy, error) { +func ParseHysteria(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "hysteria://") { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) serverInfo := strings.SplitN(parts[0], ":", 2) if len(serverInfo) != 2 { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } params, err := url.ParseQuery(parts[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } host := serverInfo[0] port, err := strconv.Atoi(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } protocol := params.Get("protocol") auth := params.Get("auth") @@ -64,23 +64,27 @@ func ParseHysteria(proxy string) (model2.Proxy, error) { } insecureBool, err := strconv.ParseBool(insecure) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria Url") + return model.Outbound{}, errors.New("invalid hysteria Url") } - result := model2.Proxy{ + result := model.Outbound{ Type: "hysteria", Tag: remarks, - Hysteria: model2.Hysteria{ - Server: host, - ServerPort: uint16(port), - Up: upmbps, - Down: downmbps, - Auth: []byte(auth), - Obfs: obfs, - Network: protocol, - TLS: &model2.OutboundTLSOptions{ - Enabled: true, - Insecure: insecureBool, - ALPN: alpn, + HysteriaOptions: model.HysteriaOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: host, + ServerPort: uint16(port), + }, + Up: upmbps, + Down: downmbps, + Auth: []byte(auth), + Obfs: obfs, + Network: protocol, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + Insecure: insecureBool, + ALPN: alpn, + }, }, }, } diff --git a/parser/hysteria2.go b/parser/hysteria2.go index 7d1cc45..88b97c0 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -5,14 +5,14 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) // hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com -func ParseHysteria2(proxy string) (model2.Proxy, error) { +func ParseHysteria2(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) serverInfo := strings.SplitN(parts[1], "/?", 2) @@ -20,36 +20,38 @@ func ParseHysteria2(proxy string) (model2.Proxy, error) { if len(serverAndPort) == 1 { serverAndPort = append(serverAndPort, "443") } else if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } params, err := url.ParseQuery(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } port, err := strconv.Atoi(serverAndPort[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid hysteria2 Url") + return model.Outbound{}, errors.New("invalid hysteria2 Url") } remarks := params.Get("name") server := serverAndPort[0] password := parts[0] network := params.Get("network") - result := model2.Proxy{ + result := model.Outbound{ Type: "hysteria2", Tag: remarks, - Hysteria2: model2.Hysteria2{ - Server: server, - ServerPort: uint16(port), - Password: password, - Obfs: &model2.Hysteria2Obfs{ + Hysteria2Options: model.Hysteria2OutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Password: password, + Obfs: &model.Hysteria2Obfs{ Type: params.Get("obfs"), Password: params.Get("obfs-password"), }, - TLS: &model2.OutboundTLSOptions{ - Enabled: params.Get("pinSHA256") != "", - Insecure: params.Get("insecure") == "1", - ServerName: params.Get("sni"), - Certificate: []string{params.Get("pinSHA256")}, + OutboundTLSOptionsContainer: model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{Enabled: params.Get("pinSHA256") != "", + Insecure: params.Get("insecure") == "1", + ServerName: params.Get("sni"), + Certificate: []string{params.Get("pinSHA256")}}, }, Network: network, }, diff --git a/parser/parsers_map.go b/parser/parsers_map.go index 8fb615f..6837179 100644 --- a/parser/parsers_map.go +++ b/parser/parsers_map.go @@ -4,7 +4,7 @@ import ( "sub2sing-box/model" ) -var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ +var ParserMap map[string]func(string) (model.Outbound, error) = map[string]func(string) (model.Outbound, error){ "ss://": ParseShadowsocks, "vmess://": ParseVmess, "trojan://": ParseTrojan, diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index 0c2f184..2ce68d9 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -5,43 +5,43 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" "sub2sing-box/util" ) -func ParseShadowsocks(proxy string) (model2.Proxy, error) { +func ParseShadowsocks(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "ss://") { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } if !strings.Contains(parts[0], ":") { decoded, err := util.DecodeBase64(parts[0]) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } parts[0] = decoded } credentials := strings.SplitN(parts[0], ":", 2) if len(credentials) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPort := strings.SplitN(serverInfo[0], ":", 2) if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid ss Url") + return model.Outbound{}, errors.New("invalid ss Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } remarks := "" if len(serverInfo) == 2 { unescape, err := url.QueryUnescape(serverInfo[1]) if err != nil { - return model2.Proxy{}, errors.New("invalid ss Url" + err.Error()) + return model.Outbound{}, errors.New("invalid ss Url" + err.Error()) } remarks = strings.TrimSpace(unescape) } else { @@ -50,14 +50,16 @@ func ParseShadowsocks(proxy string) (model2.Proxy, error) { method := credentials[0] password := credentials[1] server := strings.TrimSpace(serverAndPort[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "shadowsocks", Tag: remarks, - Shadowsocks: model2.Shadowsocks{ - Method: method, - Password: password, - Server: server, - ServerPort: uint16(port), + ShadowsocksOptions: model.ShadowsocksOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Method: method, + Password: password, }, } return result, nil diff --git a/parser/trojan.go b/parser/trojan.go index 4d8b896..d838dd2 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) -func ParseTrojan(proxy string) (model2.Proxy, error) { +func ParseTrojan(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "trojan://") { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, errors.New("invalid trojan Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, errors.New("invalid trojan Url") } serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) params, err := url.ParseQuery(serverAndPortAndParams[1]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid trojan Url") + return model.Outbound{}, errors.New("invalid trojan Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } remarks := "" if len(serverInfo) == 2 { @@ -38,14 +38,16 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } server := strings.TrimSpace(serverAndPort[0]) password := strings.TrimSpace(parts[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "trojan", Tag: remarks, - Trojan: model2.Trojan{ - Server: server, - ServerPort: uint16(port), - Password: password, - Network: params.Get("type"), + TrojanOptions: model.TrojanOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + Password: password, + Network: params.Get("type"), }, } if params.Get("security") == "xtls" || params.Get("security") == "tls" { @@ -55,31 +57,35 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.Trojan.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - ServerName: params.Get("sni"), + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + ServerName: params.Get("sni"), + }, } } if params.Get("security") == "reality" { - result.Trojan.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - Reality: &model2.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - UTLS: &model2.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + result.TrojanOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ServerName: params.Get("sni"), + Reality: &model.OutboundRealityOptions{ + Enabled: true, + PublicKey: params.Get("pbk"), + ShortID: params.Get("sid"), + }, + UTLS: &model.OutboundUTLSOptions{ + Enabled: params.Get("fp") != "", + Fingerprint: params.Get("fp"), + }, }, } } if params.Get("type") == "ws" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model2.V2RayWebsocketOptions{ + WebsocketOptions: model.V2RayWebsocketOptions{ Path: params.Get("path"), Headers: map[string]string{ "Host": params.Get("host"), @@ -88,24 +94,24 @@ func ParseTrojan(proxy string) (model2.Proxy, error) { } } if params.Get("type") == "http" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "http", - HTTPOptions: model2.V2RayHTTPOptions{ + HTTPOptions: model.V2RayHTTPOptions{ Host: []string{params.Get("host")}, Path: params.Get("path"), }, } } if params.Get("type") == "quic" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", - QUICOptions: model2.V2RayQUICOptions{}, + QUICOptions: model.V2RayQUICOptions{}, } } if params.Get("type") == "grpc" { - result.Trojan.Transport = &model2.V2RayTransportOptions{ + result.TrojanOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model2.V2RayGRPCOptions{ + GRPCOptions: model.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } diff --git a/parser/vless.go b/parser/vless.go index 9005cf5..38986f2 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -5,30 +5,30 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" ) -func ParseVless(proxy string) (model2.Proxy, error) { +func ParseVless(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "vless://") { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, errors.New("invalid vless Url") } parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) if len(parts) != 2 { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, errors.New("invalid vless Url") } serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) params, err := url.ParseQuery(serverAndPortAndParams[1]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } if len(serverAndPort) != 2 { - return model2.Proxy{}, errors.New("invalid vless Url") + return model.Outbound{}, errors.New("invalid vless Url") } port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } remarks := "" if len(serverInfo) == 2 { @@ -37,25 +37,27 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { remarks, err = url.QueryUnescape(serverInfo[1]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } } } else { remarks, err = url.QueryUnescape(serverAndPort[0]) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } } server := strings.TrimSpace(serverAndPort[0]) uuid := strings.TrimSpace(parts[0]) - result := model2.Proxy{ + result := model.Outbound{ Type: "vless", Tag: remarks, - VLESS: model2.VLESS{ - Server: server, - ServerPort: uint16(port), - UUID: uuid, - Flow: params.Get("flow"), + VLESSOptions: model.VLESSOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: server, + ServerPort: uint16(port), + }, + UUID: uuid, + Flow: params.Get("flow"), }, } if params.Get("security") == "tls" { @@ -65,10 +67,12 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ALPN: alpn, - Insecure: params.Get("allowInsecure") == "1", + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ALPN: alpn, + Insecure: params.Get("allowInsecure") == "1", + }, } } if params.Get("security") == "reality" { @@ -78,45 +82,49 @@ func ParseVless(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - ServerName: params.Get("sni"), - UTLS: &model2.OutboundUTLSOptions{ - Enabled: params.Get("fp") != "", - Fingerprint: params.Get("fp"), + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + ServerName: params.Get("sni"), + UTLS: &model.OutboundUTLSOptions{ + Enabled: params.Get("fp") != "", + Fingerprint: params.Get("fp"), + }, + Reality: &model.OutboundRealityOptions{ + Enabled: true, + PublicKey: params.Get("pbk"), + ShortID: params.Get("sid"), + }, + ALPN: alpn, }, - Reality: &model2.OutboundRealityOptions{ - Enabled: true, - PublicKey: params.Get("pbk"), - ShortID: params.Get("sid"), - }, - ALPN: alpn, } } if params.Get("type") == "ws" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "ws", - WebsocketOptions: model2.V2RayWebsocketOptions{ + WebsocketOptions: model.V2RayWebsocketOptions{ Path: params.Get("path"), - Headers: map[string]string{ - "Host": params.Get("host"), - }, }, } + if params.Get("host") != "" { + result.VLESSOptions.Transport.WebsocketOptions.Headers["Host"] = params.Get("host") + } } if params.Get("type") == "quic" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", - QUICOptions: model2.V2RayQUICOptions{}, + QUICOptions: model.V2RayQUICOptions{}, } - result.VLESS.TLS = &model2.OutboundTLSOptions{ - Enabled: true, + result.VLESSOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + }, } } if params.Get("type") == "grpc" { - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", - GRPCOptions: model2.V2RayGRPCOptions{ + GRPCOptions: model.V2RayGRPCOptions{ ServiceName: params.Get("serviceName"), }, } @@ -124,11 +132,11 @@ func ParseVless(proxy string) (model2.Proxy, error) { if params.Get("type") == "http" { host, err := url.QueryUnescape(params.Get("host")) if err != nil { - return model2.Proxy{}, err + return model.Outbound{}, err } - result.VLESS.Transport = &model2.V2RayTransportOptions{ + result.VLESSOptions.Transport = &model.V2RayTransportOptions{ Type: "http", - HTTPOptions: model2.V2RayHTTPOptions{ + HTTPOptions: model.V2RayHTTPOptions{ Host: strings.Split(host, ","), }, } diff --git a/parser/vmess.go b/parser/vmess.go index 3288118..8bf7b1c 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -6,29 +6,29 @@ import ( "net/url" "strconv" "strings" - model2 "sub2sing-box/model" + "sub2sing-box/model" "sub2sing-box/util" ) -func ParseVmess(proxy string) (model2.Proxy, error) { +func ParseVmess(proxy string) (model.Outbound, error) { if !strings.HasPrefix(proxy, "vmess://") { - return model2.Proxy{}, errors.New("invalid vmess url") + return model.Outbound{}, errors.New("invalid vmess url") } base64, err := util.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } - var vmess model2.VmessJson + var vmess model.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } port := 0 switch vmess.Port.(type) { case string: port, err = strconv.Atoi(vmess.Port.(string)) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } case float64: port = int(vmess.Port.(float64)) @@ -38,7 +38,7 @@ func ParseVmess(proxy string) (model2.Proxy, error) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model2.Proxy{}, errors.New("invalid vmess url" + err.Error()) + return model.Outbound{}, errors.New("invalid vmess url" + err.Error()) } case float64: aid = int(vmess.Aid.(float64)) @@ -52,15 +52,17 @@ func ParseVmess(proxy string) (model2.Proxy, error) { name = vmess.Ps } - result := model2.Proxy{ + result := model.Outbound{ Type: "vmess", Tag: name, - VMess: model2.VMess{ - Server: vmess.Add, - ServerPort: uint16(port), - UUID: vmess.Id, - AlterId: aid, - Security: vmess.Scy, + VMessOptions: model.VMessOutboundOptions{ + ServerOptions: model.ServerOptions{ + Server: vmess.Add, + ServerPort: uint16(port), + }, + UUID: vmess.Id, + AlterId: aid, + Security: vmess.Scy, }, } @@ -71,12 +73,14 @@ func ParseVmess(proxy string) (model2.Proxy, error) { } else { alpn = nil } - result.VMess.TLS = &model2.OutboundTLSOptions{ - Enabled: true, - UTLS: &model2.OutboundUTLSOptions{ - Fingerprint: vmess.Fp, + result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + UTLS: &model.OutboundUTLSOptions{ + Fingerprint: vmess.Fp, + }, + ALPN: alpn, }, - ALPN: alpn, } } @@ -87,46 +91,47 @@ func ParseVmess(proxy string) (model2.Proxy, error) { if vmess.Host == "" { vmess.Host = vmess.Add } - ws := model2.V2RayWebsocketOptions{ - Path: vmess.Path, - Headers: map[string]string{ - "Host": vmess.Host, + result.VMessOptions.Transport = &model.V2RayTransportOptions{ + Type: "ws", + WebsocketOptions: model.V2RayWebsocketOptions{ + Path: vmess.Path, + Headers: map[string]string{ + "Host": vmess.Host, + }, }, } - result.VMess.Transport = &model2.V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: ws, - } } if vmess.Net == "quic" { - quic := model2.V2RayQUICOptions{} - result.VMess.Transport = &model2.V2RayTransportOptions{ + quic := model.V2RayQUICOptions{} + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "quic", QUICOptions: quic, } - result.VMess.TLS = &model2.OutboundTLSOptions{ - Enabled: true, + result.VMessOptions.OutboundTLSOptionsContainer = model.OutboundTLSOptionsContainer{ + TLS: &model.OutboundTLSOptions{ + Enabled: true, + }, } } if vmess.Net == "grpc" { - grpc := model2.V2RayGRPCOptions{ + grpc := model.V2RayGRPCOptions{ ServiceName: vmess.Path, PermitWithoutStream: true, } - result.VMess.Transport = &model2.V2RayTransportOptions{ + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "grpc", GRPCOptions: grpc, } } if vmess.Net == "h2" { - httpOps := model2.V2RayHTTPOptions{ + httpOps := model.V2RayHTTPOptions{ Host: strings.Split(vmess.Host, ","), Path: vmess.Path, } - result.VMess.Transport = &model2.V2RayTransportOptions{ + result.VMessOptions.Transport = &model.V2RayTransportOptions{ Type: "http", HTTPOptions: httpOps, } diff --git a/util/marshal.go b/util/marshal.go new file mode 100644 index 0000000..389d8b3 --- /dev/null +++ b/util/marshal.go @@ -0,0 +1,25 @@ +package util + +import "encoding/json" + +func MergeAndMarshal(args ...interface{}) (string, error) { + merged := make(map[string]interface{}) + for _, arg := range args { + jsonBytes, err := json.Marshal(arg) + if err != nil { + return "", err + } + var m map[string]interface{} + if err := json.Unmarshal(jsonBytes, &m); err != nil { + return "", err + } + for k, v := range m { + merged[k] = v + } + } + result, err := json.Marshal(merged) + if err != nil { + return "", err + } + return string(result), nil +}