Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling

This commit is contained in:
Sergei Maklagin
2026-04-29 22:11:30 +03:00
parent 09f9f114aa
commit 04908a6a67
158 changed files with 7994 additions and 2277 deletions

30
parser/clash/anytls.go Normal file
View File

@@ -0,0 +1,30 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type AnyTLSOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Password string `yaml:"password"`
UDP bool `yaml:"udp,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
func (a *AnyTLSOption) Build() any {
a.TLS = true
return &option.AnyTLSOutboundOptions{
DialerOptions: a.DialerOptions.Build(),
ServerOptions: a.ServerOptions.Build(),
OutboundTLSOptionsContainer: clashTLSOptions(a.Server, &a.TLSOptions),
Password: a.Password,
IdleSessionCheckInterval: badoption.Duration(a.IdleSessionCheckInterval),
IdleSessionTimeout: badoption.Duration(a.IdleSessionTimeout),
MinIdleSession: a.MinIdleSession,
}
}

181
parser/clash/base.go Normal file
View File

@@ -0,0 +1,181 @@
package clash
import (
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers badoption.HTTPHeader `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"`
}
type MuxOptions struct {
Enabled bool `yaml:"enabled,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
MaxConnections int `yaml:"max-connections,omitempty"`
MinStreams int `yaml:"min-streams,omitempty"`
MaxStreams int `yaml:"max-streams,omitempty"`
Padding bool `yaml:"padding,omitempty"`
BrutalOpts *BrutalOptions `yaml:"brutal-opts,omitempty"`
}
func (s *MuxOptions) Build() *option.OutboundMultiplexOptions {
if s == nil {
return nil
}
return &option.OutboundMultiplexOptions{
Enabled: s.Enabled,
Protocol: s.Protocol,
MaxConnections: s.MaxConnections,
MinStreams: s.MinStreams,
MaxStreams: s.MaxStreams,
Padding: s.Padding,
Brutal: s.BrutalOpts.Build(),
}
}
type BrutalOptions struct {
Enabled bool `yaml:"enabled,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
}
func (b *BrutalOptions) Build() *option.BrutalOptions {
if b == nil {
return nil
}
return &option.BrutalOptions{
Enabled: b.Enabled,
UpMbps: clashSpeedToIntMbps(b.Up),
DownMbps: clashSpeedToIntMbps(b.Down),
}
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id"`
}
func (r *RealityOptions) Build() *option.OutboundRealityOptions {
if r == nil {
return nil
}
return &option.OutboundRealityOptions{
Enabled: true,
PublicKey: r.PublicKey,
ShortID: r.ShortID,
}
}
type ECHOptions struct {
Enable bool `yaml:"enable,omitempty"`
Config string `yaml:"config,omitempty"`
}
func (e *ECHOptions) Build() *option.OutboundECHOptions {
if e == nil {
return nil
}
list, err := base64.StdEncoding.DecodeString(e.Config)
if err != nil {
return nil
}
return &option.OutboundECHOptions{
Enabled: e.Enable,
Config: trimStringArray(strings.Split(string(list), "\n")),
}
}
type TLSOptions struct {
TLS bool `yaml:"tls,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
Certificate string `yaml:"certificate,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
ECHOpts *ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts *RealityOptions `yaml:"reality-opts,omitempty"`
}
func (t *TLSOptions) Build() *option.OutboundTLSOptions {
if t == nil {
return nil
}
options := &option.OutboundTLSOptions{
Enabled: t.TLS,
ServerName: t.SNI,
Insecure: t.SkipCertVerify,
ALPN: t.ALPN,
UTLS: clashClientFingerprint(t.ClientFingerprint),
Certificate: trimStringArray(strings.Split(t.CustomCAString, "\n")),
CertificatePath: t.CustomCA,
ECH: t.ECHOpts.Build(),
Reality: t.RealityOpts.Build(),
}
if strings.HasPrefix(t.Certificate, "-----BEGIN ") {
options.ClientCertificate = trimStringArray(strings.Split(t.Certificate, "\n"))
} else {
options.ClientCertificatePath = t.Certificate
}
if strings.HasPrefix(t.PrivateKey, "-----BEGIN ") {
options.ClientKey = trimStringArray(strings.Split(t.PrivateKey, "\n"))
} else {
options.ClientKeyPath = t.PrivateKey
}
return options
}
type DialerOptions struct {
TFO bool `yaml:"tfo,omitempty"`
MPTCP bool `yaml:"mptcp,omitempty"`
Interface string `yaml:"interface-name,omitempty"`
RoutingMark int `yaml:"routing-mark,omitempty"`
DialerProxy string `yaml:"dialer-proxy,omitempty"`
}
func (b *DialerOptions) Build() option.DialerOptions {
return option.DialerOptions{
Detour: b.DialerProxy,
BindInterface: b.Interface,
TCPFastOpen: b.TFO,
TCPMultiPath: b.MPTCP,
RoutingMark: option.FwMark(b.RoutingMark),
}
}
type ServerOptions struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
}
func (s *ServerOptions) Build() option.ServerOptions {
return option.ServerOptions{
Server: s.Server,
ServerPort: uint16(s.Port),
}
}

23
parser/clash/http.go Normal file
View File

@@ -0,0 +1,23 @@
package clash
import "github.com/sagernet/sing-box/option"
type HttpOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
}
func (h *HttpOption) Build() any {
return &option.HTTPOutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
Username: h.UserName,
Password: h.Password,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, h.TLSOptions),
Headers: clashHeaders(h.Headers),
}
}

47
parser/clash/hysteria.go Normal file
View File

@@ -0,0 +1,47 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type HysteriaOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Ports string `yaml:"ports,omitempty"`
Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
Auth string `yaml:"auth,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
}
func (h *HysteriaOption) Build() any {
h.TLS = true
h.TFO = h.FastOpen
return &option.HysteriaOutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
ServerPorts: clashPorts(h.Ports),
HopInterval: badoption.Duration(h.HopInterval),
Up: clashSpeedToNetworkBytes(h.Up),
UpMbps: h.UpSpeed,
Down: clashSpeedToNetworkBytes(h.Down),
DownMbps: h.DownSpeed,
Obfs: h.Obfs,
Auth: []byte(h.Auth),
AuthString: h.AuthString,
ReceiveWindowConn: uint64(h.ReceiveWindowConn),
ReceiveWindow: uint64(h.ReceiveWindow),
DisableMTUDiscovery: h.DisableMTUDiscovery,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, &h.TLSOptions),
}
}

34
parser/clash/hysteria2.go Normal file
View File

@@ -0,0 +1,34 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type Hysteria2Option struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Password string `yaml:"password,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
}
func (h *Hysteria2Option) Build() any {
h.TLS = true
return &option.Hysteria2OutboundOptions{
DialerOptions: h.DialerOptions.Build(),
ServerOptions: h.ServerOptions.Build(),
ServerPorts: clashPorts(h.Ports),
HopInterval: badoption.Duration(h.HopInterval),
UpMbps: clashSpeedToIntMbps(h.Up),
DownMbps: clashSpeedToIntMbps(h.Down),
Obfs: clashHysteria2Obfs(h.Obfs, h.ObfsPassword),
Password: h.Password,
OutboundTLSOptionsContainer: clashTLSOptions(h.Server, &h.TLSOptions),
}
}

106
parser/clash/parser.go Normal file
View File

@@ -0,0 +1,106 @@
package clash
import (
"context"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"gopkg.in/yaml.v3"
)
type ClashConfig struct {
Proxies []ClashProxy `yaml:"proxies"`
}
type _ClashProxy struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Options Proxy `yaml:"-"`
SingType string `yaml:"-"`
}
type ClashProxy _ClashProxy
type Proxy interface {
Build() any
}
func (c *ClashProxy) UnmarshalYAML(value *yaml.Node) error {
err := value.Decode((*_ClashProxy)(c))
if err != nil {
return err
}
var options Proxy
switch c.Type {
case "ss":
c.SingType = C.TypeShadowsocks
options = &ShadowSocksOption{}
case "tuic":
c.SingType = C.TypeTUIC
options = &TuicOption{}
case "vmess":
c.SingType = C.TypeVMess
options = &VmessOption{}
case "vless":
c.SingType = C.TypeVLESS
options = &VlessOption{}
case "socks5":
c.SingType = C.TypeSOCKS
options = &Socks5Option{}
case "http":
c.SingType = C.TypeHTTP
options = &HttpOption{}
case "trojan":
c.SingType = C.TypeTrojan
options = &TrojanOption{}
case "hysteria":
c.SingType = C.TypeHysteria
options = &HysteriaOption{}
case "hysteria2":
c.SingType = C.TypeHysteria2
options = &Hysteria2Option{}
case "ssh":
c.SingType = C.TypeSSH
options = &SSHOption{}
case "anytls":
c.SingType = C.TypeAnyTLS
options = &AnyTLSOption{}
default:
return nil
}
err = value.Decode(options)
if err != nil {
return err
}
c.Options = options
return nil
}
func (c *ClashProxy) Build() option.Outbound {
outbound := option.Outbound{
Tag: c.Name,
Type: c.SingType,
}
if c.Options != nil {
outbound.Options = c.Options.Build()
}
return outbound
}
func ParseClashSubscription(_ context.Context, content string) ([]option.Outbound, error) {
config := &ClashConfig{}
err := yaml.Unmarshal([]byte(content), &config)
if err != nil {
return nil, E.Cause(err, "parse clash config")
}
outbounds := common.FilterIsInstance(config.Proxies, func(proxy ClashProxy) (option.Outbound, bool) {
if proxy.SingType == "" {
return option.Outbound{}, false
}
return proxy.Build(), true
})
return outbounds, nil
}

View File

@@ -0,0 +1,51 @@
package clash
import (
"strings"
"github.com/sagernet/sing-box/option"
F "github.com/sagernet/sing/common/format"
)
type ShadowSocksOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
Password string `yaml:"password"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (s *ShadowSocksOption) Build() any {
return &option.ShadowsocksOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
Password: s.Password,
Method: clashShadowsocksCipher(s.Cipher),
Plugin: clashPluginName(s.Plugin),
PluginOptions: clashPluginOptions(s.Plugin, s.PluginOpts),
Network: clashNetworks(s.UDP),
UDPOverTCP: &option.UDPOverTCPOptions{
Enabled: s.UDPOverTCP,
Version: uint8(s.UDPOverTCPVersion),
},
Multiplex: s.MuxOpts.Build(),
}
}
type shadowsocksPluginOptionsBuilder map[string]any
func (o shadowsocksPluginOptionsBuilder) Build() string {
var opts []string
for key, value := range o {
if value == nil {
continue
}
opts = append(opts, F.ToString(key, "=", value))
}
return strings.Join(opts, ";")
}

21
parser/clash/socks5.go Normal file
View File

@@ -0,0 +1,21 @@
package clash
import "github.com/sagernet/sing-box/option"
type Socks5Option struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
UDP bool `yaml:"udp,omitempty"`
}
func (s *Socks5Option) Build() any {
return &option.SOCKSOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
Username: s.UserName,
Password: s.Password,
Network: clashNetworks(s.UDP),
}
}

36
parser/clash/ssh.go Normal file
View File

@@ -0,0 +1,36 @@
package clash
import (
"strings"
"github.com/sagernet/sing-box/option"
)
type SSHOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
UserName string `yaml:"username"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
PrivateKeyPassphrase string `yaml:"private-key-passphrase,omitempty"`
HostKey []string `yaml:"host-key,omitempty"`
HostKeyAlgorithms []string `yaml:"host-key-algorithms,omitempty"`
}
func (s *SSHOption) Build() any {
options := &option.SSHOutboundOptions{
DialerOptions: s.DialerOptions.Build(),
ServerOptions: s.ServerOptions.Build(),
User: s.UserName,
Password: s.Password,
PrivateKeyPassphrase: s.PrivateKeyPassphrase,
HostKey: s.HostKey,
HostKeyAlgorithms: s.HostKeyAlgorithms,
}
if strings.Contains(s.PrivateKey, "PRIVATE KEY") {
options.PrivateKey = trimStringArray(strings.Split(s.PrivateKey, "\n"))
} else {
options.PrivateKeyPath = s.PrivateKey
}
return options
}

28
parser/clash/trojan.go Normal file
View File

@@ -0,0 +1,28 @@
package clash
import "github.com/sagernet/sing-box/option"
type TrojanOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
Password string `yaml:"password"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (t *TrojanOption) Build() any {
t.TLS = true
return &option.TrojanOutboundOptions{
DialerOptions: t.DialerOptions.Build(),
ServerOptions: t.ServerOptions.Build(),
Password: t.Password,
Network: clashNetworks(t.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(t.Server, &t.TLSOptions),
Multiplex: t.MuxOpts.Build(),
Transport: clashTransport(t.Network, HTTPOptions{}, HTTP2Options{}, t.GrpcOpts, t.WSOpts),
}
}

47
parser/clash/tuic.go Normal file
View File

@@ -0,0 +1,47 @@
package clash
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
type TuicOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid,omitempty"`
Password string `yaml:"password,omitempty"`
Ip string `yaml:"ip,omitempty"`
HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"`
DisableSni bool `yaml:"disable-sni,omitempty"`
ReduceRtt bool `yaml:"reduce-rtt,omitempty"`
UdpRelayMode string `yaml:"udp-relay-mode,omitempty"`
CongestionController string `yaml:"congestion-controller,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
UDPOverStream bool `yaml:"udp-over-stream,omitempty"`
}
func (t *TuicOption) Build() any {
t.TLS = true
t.TFO = t.FastOpen
options := &option.TUICOutboundOptions{
DialerOptions: t.DialerOptions.Build(),
ServerOptions: t.ServerOptions.Build(),
UUID: t.UUID,
Password: t.Password,
CongestionControl: t.CongestionController,
UDPRelayMode: t.UdpRelayMode,
UDPOverStream: t.UDPOverStream,
ZeroRTTHandshake: t.ReduceRtt,
Heartbeat: badoption.Duration(t.HeartbeatInterval),
OutboundTLSOptionsContainer: clashTLSOptions(t.Server, &t.TLSOptions),
}
if t.Ip != "" {
options.Server = t.Ip
}
if t.DisableSni {
options.TLS.DisableSNI = true
}
return options
}

205
parser/clash/utils.go Normal file
View File

@@ -0,0 +1,205 @@
package clash
import (
"strconv"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/byteformats"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badoption"
N "github.com/sagernet/sing/common/network"
)
func clashClientFingerprint(clientFingerprint string) *option.OutboundUTLSOptions {
if clientFingerprint == "" {
return nil
}
return &option.OutboundUTLSOptions{
Enabled: true,
Fingerprint: clientFingerprint,
}
}
func clashHeaders(headers map[string]string) map[string]badoption.Listable[string] {
if headers == nil {
return nil
}
result := make(map[string]badoption.Listable[string])
for key, value := range headers {
result[key] = []string{value}
}
return result
}
func clashHysteria2Obfs(obfs string, password string) *option.Hysteria2Obfs {
if obfs == "" {
return nil
}
return &option.Hysteria2Obfs{
Type: obfs,
Password: password,
}
}
func clashNetworks(udpEnabled bool) option.NetworkList {
if !udpEnabled {
return N.NetworkTCP
}
return ""
}
func clashPluginName(plugin string) string {
switch plugin {
case "obfs":
return "obfs-local"
}
return plugin
}
func clashPluginOptions(plugin string, opts map[string]any) string {
options := make(shadowsocksPluginOptionsBuilder)
switch plugin {
case "obfs":
options["obfs"] = opts["mode"]
options["obfs-host"] = opts["host"]
case "v2ray-plugin":
options["mode"] = opts["mode"]
options["tls"] = opts["tls"]
options["host"] = opts["host"]
options["path"] = opts["path"]
}
return options.Build()
}
func clashPorts(ports string) badoption.Listable[string] {
if ports == "" {
return nil
}
serverPorts := badoption.Listable[string]{}
ports = strings.ReplaceAll(ports, "/", ",")
for _, port := range strings.Split(ports, ",") {
if port == "" {
continue
}
port = strings.Replace(port, "-", ":", 1)
serverPorts = append(serverPorts, port)
}
return serverPorts
}
func clashShadowsocksCipher(cipher string) string {
switch cipher {
case "dummy":
return "none"
}
return cipher
}
func clashStringList(list []string) string {
if len(list) > 0 {
return list[0]
}
return ""
}
func clashSpeedToIntMbps(speed string) int {
if speed == "" {
return 0
}
if num, err := strconv.Atoi(speed); err == nil {
return num
}
networkBytes := byteformats.NetworkBytesCompat{}
if err := networkBytes.UnmarshalJSON([]byte(speed)); err != nil {
return 0
}
return int(networkBytes.Value() / byteformats.MByte * 8)
}
func clashSpeedToNetworkBytes(speed string) *byteformats.NetworkBytesCompat {
if speed == "" {
return nil
}
networkBytes := &byteformats.NetworkBytesCompat{}
if num, err := strconv.Atoi(speed); err == nil {
speed = F.ToString(num, "Mbps")
}
if err := networkBytes.UnmarshalJSON([]byte(speed)); err != nil {
return nil
}
return networkBytes
}
func clashTransport(network string, httpOpts HTTPOptions, h2Opts HTTP2Options, grpcOpts GrpcOptions, wsOpts WSOptions) *option.V2RayTransportOptions {
switch network {
case "http":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Method: httpOpts.Method,
Path: clashStringList(httpOpts.Path),
Headers: httpOpts.Headers,
},
}
case "h2":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Path: h2Opts.Path,
Host: h2Opts.Host,
},
}
case "grpc":
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeGRPC,
GRPCOptions: option.V2RayGRPCOptions{
ServiceName: grpcOpts.GrpcServiceName,
},
}
case "ws":
headers := clashHeaders(wsOpts.Headers)
if wsOpts.V2rayHttpUpgrade {
var host string
if headers != nil && headers["Host"] != nil {
host = headers["Host"][0]
}
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTPUpgrade,
HTTPUpgradeOptions: option.V2RayHTTPUpgradeOptions{
Host: host,
Path: wsOpts.Path,
Headers: headers,
},
}
}
return &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeWebsocket,
WebsocketOptions: option.V2RayWebsocketOptions{
Path: wsOpts.Path,
Headers: headers,
MaxEarlyData: uint32(wsOpts.MaxEarlyData),
EarlyDataHeaderName: wsOpts.EarlyDataHeaderName,
},
}
default:
return nil
}
}
func clashTLSOptions(server string, tlsOptions *TLSOptions) option.OutboundTLSOptionsContainer {
if tlsOptions != nil && tlsOptions.SNI == "" {
tlsOptions.SNI = server
}
return option.OutboundTLSOptionsContainer{
TLS: tlsOptions.Build(),
}
}
func trimStringArray(array []string) []string {
return common.Filter(array, func(it string) bool {
return strings.TrimSpace(it) != ""
})
}

49
parser/clash/vless.go Normal file
View File

@@ -0,0 +1,49 @@
package clash
import "github.com/sagernet/sing-box/option"
type VlessOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid"`
Flow string `yaml:"flow,omitempty"`
UDP bool `yaml:"udp,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
Network string `yaml:"network,omitempty"`
ServerName string `yaml:"servername,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (v *VlessOption) Build() any {
if v.TLSOptions != nil {
v.SNI = v.ServerName
}
switch v.PacketEncoding {
case "":
if v.PacketAddr {
v.PacketEncoding = "packetaddr"
} else {
v.PacketEncoding = "xudp"
}
case "packet":
v.PacketEncoding = "packetaddr"
}
return &option.VLESSOutboundOptions{
DialerOptions: v.DialerOptions.Build(),
ServerOptions: v.ServerOptions.Build(),
UUID: v.UUID,
Flow: v.Flow,
Network: clashNetworks(v.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(v.Server, v.TLSOptions),
Multiplex: v.MuxOpts.Build(),
Transport: clashTransport(v.Network, v.HTTPOpts, v.HTTP2Opts, v.GrpcOpts, v.WSOpts),
PacketEncoding: &v.PacketEncoding,
}
}

55
parser/clash/vmess.go Normal file
View File

@@ -0,0 +1,55 @@
package clash
import "github.com/sagernet/sing-box/option"
type VmessOption struct {
DialerOptions `yaml:",inline"`
ServerOptions `yaml:",inline"`
*TLSOptions `yaml:",inline"`
UUID string `yaml:"uuid"`
AlterID int `yaml:"alterId"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
ServerName string `yaml:"servername,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
MuxOpts *MuxOptions `yaml:"smux,omitempty"`
}
func (v *VmessOption) Build() any {
if v.TLSOptions != nil {
v.SNI = v.ServerName
}
switch v.PacketEncoding {
case "":
if v.XUDP {
v.PacketEncoding = "xudp"
} else if v.PacketAddr {
v.PacketEncoding = "packetaddr"
}
case "packet":
v.PacketEncoding = "packetaddr"
}
return &option.VMessOutboundOptions{
DialerOptions: v.DialerOptions.Build(),
ServerOptions: v.ServerOptions.Build(),
UUID: v.UUID,
Security: v.Cipher,
AlterId: v.AlterID,
GlobalPadding: v.GlobalPadding,
AuthenticatedLength: v.AuthenticatedLength,
Network: clashNetworks(v.UDP),
OutboundTLSOptionsContainer: clashTLSOptions(v.Server, v.TLSOptions),
PacketEncoding: v.PacketEncoding,
Multiplex: v.MuxOpts.Build(),
Transport: clashTransport(v.Network, v.HTTPOpts, v.HTTP2Opts, v.GrpcOpts, v.WSOpts),
}
}

71
parser/link/hysteria.go Normal file
View File

@@ -0,0 +1,71 @@
package link
import (
"net/url"
"strconv"
"strings"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
)
func parseHysteriaLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
var options option.HysteriaOutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerPort = common.StringToType[uint16](linkURL.Port())
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "auth":
options.AuthString = value
case "peer", "sni":
TLSOptions.ServerName = value
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
case "ca":
TLSOptions.CertificatePath = value
case "ca_str":
TLSOptions.Certificate = strings.Split(value, "\n")
case "up":
options.Up = &byteformats.NetworkBytesCompat{}
options.Up.UnmarshalJSON([]byte(value))
case "up_mbps":
options.UpMbps, _ = strconv.Atoi(value)
case "down":
options.Down = &byteformats.NetworkBytesCompat{}
options.Down.UnmarshalJSON([]byte(value))
case "down_mbps":
options.DownMbps, _ = strconv.Atoi(value)
case "obfs", "obfsParam":
options.Obfs = value
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
outbound := option.Outbound{
Type: C.TypeHysteria,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

61
parser/link/hysteria2.go Normal file
View File

@@ -0,0 +1,61 @@
package link
import (
"net/url"
"strconv"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
func parseHysteria2Link(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
var options option.Hysteria2OutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
Obfs := &option.Hysteria2Obfs{}
options.ServerPort = uint16(443)
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
if linkURL.User != nil {
options.Password = linkURL.User.Username()
}
if linkURL.Port() != "" {
options.ServerPort = common.StringToType[uint16](linkURL.Port())
}
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "up":
options.UpMbps, _ = strconv.Atoi(value)
case "down":
options.DownMbps, _ = strconv.Atoi(value)
case "obfs":
if value == "salamander" {
Obfs.Type = "salamander"
options.Obfs = Obfs
}
case "obfs-password":
Obfs.Password = value
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
}
}
outbound := option.Outbound{
Type: C.TypeHysteria2,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

42
parser/link/parser.go Normal file
View File

@@ -0,0 +1,42 @@
package link
import (
"regexp"
"strings"
"github.com/sagernet/sing-box/common"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func ParseSubscriptionLink(link string) (option.Outbound, error) {
reg := regexp.MustCompile(`^(.*?)(://)(.*?)([@?#].*)?$`)
result := reg.FindStringSubmatch(link)
if result == nil {
return option.Outbound{}, E.New("invalid link")
}
scheme := result[1]
switch scheme {
case "tuic":
return parseTuicLink(link)
case "trojan":
return parseTrojanLink(link)
case "vless":
return parseVLESSLink(link)
case "hysteria":
return parseHysteriaLink(link)
case "hy2", "hysteria2":
return parseHysteria2Link(link)
}
result[3], _ = common.DecodeBase64URLSafe(result[3])
link = strings.Join(result[1:], "")
switch scheme {
case "ss":
return parseShadowsocksLink(link)
case "vmess":
return parseVMessLink(link)
default:
return option.Outbound{}, E.New("unsupported scheme: ", scheme)
}
}

View File

@@ -0,0 +1,39 @@
package link
import (
"net/url"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func parseShadowsocksLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing user info")
}
var options option.ShadowsocksOutboundOptions
options.ServerOptions.Server = linkURL.Hostname()
options.ServerOptions.ServerPort = common.StringToType[uint16](linkURL.Port())
password, _ := linkURL.User.Password()
if password == "" {
return option.Outbound{}, E.New("bad user info")
}
options.Method = linkURL.User.Username()
options.Password = password
plugin := linkURL.Query().Get("plugin")
options.Plugin = shadowsocksPluginName(plugin)
options.PluginOptions = shadowsocksPluginOptions(plugin)
outbound := option.Outbound{
Type: C.TypeShadowsocks,
Tag: linkURL.Fragment,
}
outbound.Options = &options
return outbound, nil
}

89
parser/link/trojan.go Normal file
View File

@@ -0,0 +1,89 @@
package link
import (
"net/url"
"strings"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
func parseTrojanLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing password")
}
var options option.TrojanOutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerPort = common.StringToType[uint16](linkURL.Port())
options.Password = linkURL.User.Username()
proxy := map[string]string{}
for key, values := range linkURL.Query() {
value := values[0]
proxy[key] = value
}
for key, value := range proxy {
switch key {
case "insecure", "allowInsecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "serviceName", "sni", "peer":
TLSOptions.ServerName = value
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
case "fp":
TLSOptions.UTLS.Enabled = true
TLSOptions.UTLS.Fingerprint = value
case "type":
Transport := option.V2RayTransportOptions{
Type: "",
WebsocketOptions: option.V2RayWebsocketOptions{
Headers: map[string]badoption.Listable[string]{},
},
HTTPOptions: option.V2RayHTTPOptions{
Host: badoption.Listable[string]{},
Headers: map[string]badoption.Listable[string]{},
},
GRPCOptions: option.V2RayGRPCOptions{},
}
switch value {
case "ws":
Transport.Type = C.V2RayTransportTypeWebsocket
Transport.WebsocketOptions = v2rayTransportWs(proxy["host"], proxy["path"])
case "grpc":
Transport.Type = C.V2RayTransportTypeGRPC
if serviceName, exists := proxy["grpc-service-name"]; exists && serviceName != "" {
Transport.GRPCOptions.ServiceName = serviceName
}
default:
continue
}
options.Transport = &Transport
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
outbound := option.Outbound{
Type: C.TypeTrojan,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

81
parser/link/tuic.go Normal file
View File

@@ -0,0 +1,81 @@
package link
import (
"net/url"
"strings"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
func parseTuicLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing uuid")
}
var options option.TUICOutboundOptions
TLSOptions := option.OutboundTLSOptions{
Enabled: true,
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.UUID = linkURL.User.Username()
options.Password, _ = linkURL.User.Password()
options.ServerOptions.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerOptions.ServerPort = common.StringToType[uint16](linkURL.Port())
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "congestion_control":
if value != "cubic" {
options.CongestionControl = value
}
case "udp_relay_mode":
options.UDPRelayMode = value
case "udp_over_stream":
if value == "true" || value == "1" {
options.UDPOverStream = true
}
case "zero_rtt_handshake", "reduce_rtt":
if value == "true" || value == "1" {
options.ZeroRTTHandshake = true
}
case "heartbeat_interval":
options.Heartbeat = common.StringToType[badoption.Duration](value)
case "sni":
TLSOptions.ServerName = value
case "insecure", "skip-cert-verify", "allow_insecure":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "disable_sni":
if value == "1" || value == "true" {
TLSOptions.DisableSNI = true
}
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
}
}
if options.UDPOverStream {
options.UDPRelayMode = ""
}
outbound := option.Outbound{
Type: C.TypeTUIC,
Tag: linkURL.Fragment,
}
options.TLS = &TLSOptions
outbound.Options = &options
return outbound, nil
}

46
parser/link/utils.go Normal file
View File

@@ -0,0 +1,46 @@
package link
import (
"regexp"
"strings"
"github.com/sagernet/sing-box/common"
"github.com/sagernet/sing-box/option"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badoption"
)
func shadowsocksPluginName(plugin string) string {
if index := strings.Index(plugin, ";"); index != -1 {
return plugin[:index]
}
return plugin
}
func shadowsocksPluginOptions(plugin string) string {
if index := strings.Index(plugin, ";"); index != -1 {
return plugin[index+1:]
}
return ""
}
func v2rayTransportWsPath(WebsocketOptions *option.V2RayWebsocketOptions, path string) {
reg := regexp.MustCompile(`^(.*?)(?:\?ed=(\d*))?$`)
result := reg.FindStringSubmatch(path)
WebsocketOptions.Path = result[1]
if result[2] != "" {
WebsocketOptions.EarlyDataHeaderName = "Sec-WebSocket-Protocol"
WebsocketOptions.MaxEarlyData = common.StringToType[uint32](result[2])
}
}
func v2rayTransportWs(host string, path string) option.V2RayWebsocketOptions {
var WebsocketOptions option.V2RayWebsocketOptions
if host != "" {
WebsocketOptions.Headers = common.StringToType[badoption.HTTPHeader](F.ToString("Host: ", host))
}
if path != "" {
v2rayTransportWsPath(&WebsocketOptions, path)
}
return WebsocketOptions
}

114
parser/link/vless.go Normal file
View File

@@ -0,0 +1,114 @@
package link
import (
"net/url"
"strings"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
func parseVLESSLink(link string) (option.Outbound, error) {
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing uuid")
}
var options option.VLESSOutboundOptions
TLSOptions := option.OutboundTLSOptions{
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
options.UUID = linkURL.User.Username()
options.Server = linkURL.Hostname()
TLSOptions.ServerName = linkURL.Hostname()
options.ServerPort = common.StringToType[uint16](linkURL.Port())
proxy := map[string]string{}
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "key", "alpn", "seed", "path", "host":
proxy[key] = value
default:
proxy[key] = value
}
}
for key, value := range proxy {
switch key {
case "type":
Transport := option.V2RayTransportOptions{
HTTPOptions: option.V2RayHTTPOptions{
Host: badoption.Listable[string]{},
Headers: badoption.HTTPHeader{},
},
GRPCOptions: option.V2RayGRPCOptions{},
}
switch value {
case "ws":
Transport.Type = C.V2RayTransportTypeWebsocket
Transport.WebsocketOptions = v2rayTransportWs(proxy["host"], proxy["path"])
case "http":
Transport.Type = C.V2RayTransportTypeHTTP
if host, exists := proxy["host"]; exists && host != "" {
Transport.HTTPOptions.Host = strings.Split(host, ",")
}
if path, exists := proxy["path"]; exists && path != "" {
Transport.HTTPOptions.Path = path
}
case "grpc":
Transport.Type = C.V2RayTransportTypeGRPC
if serviceName, exists := proxy["serviceName"]; exists && serviceName != "" {
Transport.GRPCOptions.ServiceName = serviceName
}
default:
continue
}
options.Transport = &Transport
case "security":
if value == "tls" {
TLSOptions.Enabled = true
} else if value == "reality" {
TLSOptions.Enabled = true
TLSOptions.Reality.Enabled = true
}
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "serviceName", "sni", "peer":
TLSOptions.ServerName = value
case "alpn":
TLSOptions.ALPN = strings.Split(value, ",")
case "fp":
TLSOptions.UTLS.Enabled = true
TLSOptions.UTLS.Fingerprint = value
case "flow":
if value == "xtls-rprx-vision" {
options.Flow = "xtls-rprx-vision"
}
case "pbk":
TLSOptions.Reality.PublicKey = value
case "sid":
TLSOptions.Reality.ShortID = value
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
outbound := option.Outbound{
Type: C.TypeVLESS,
Tag: linkURL.Fragment,
}
if TLSOptions.Enabled {
options.TLS = &TLSOptions
}
outbound.Options = &options
return outbound, nil
}

160
parser/link/vmess.go Normal file
View File

@@ -0,0 +1,160 @@
package link
import (
"encoding/json"
"net/url"
"regexp"
"strconv"
"github.com/sagernet/sing-box/common"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
func parseVMessLink(link string) (option.Outbound, error) {
var proxy map[string]string
reg := regexp.MustCompile(`(\"[^:,]+?\"[ \t]*:[ \t]*)(\d+|true|false)`)
s := reg.ReplaceAllString(link, `$1"$2"`)
err := json.Unmarshal([]byte(s[8:]), &proxy)
if err != nil {
proxy = make(map[string]string)
linkURL, err := url.Parse(link)
if err != nil {
return option.Outbound{}, err
}
if linkURL.User == nil || linkURL.User.Username() == "" {
return option.Outbound{}, E.New("missing uuid")
}
proxy["id"] = linkURL.User.Username()
proxy["add"] = linkURL.Hostname()
proxy["port"] = linkURL.Port()
proxy["ps"] = linkURL.Fragment
for key, values := range linkURL.Query() {
value := values[0]
switch key {
case "type":
if value == "http" {
proxy["net"] = "tcp"
proxy["type"] = "http"
}
case "encryption":
proxy["scy"] = value
case "alterId":
proxy["aid"] = value
case "key", "alpn", "seed", "path", "host":
proxy[key] = value
default:
proxy[key] = value
}
}
}
outbound := option.Outbound{
Type: C.TypeVMess,
}
options := option.VMessOutboundOptions{
Security: "auto",
}
TLSOptions := option.OutboundTLSOptions{
ECH: &option.OutboundECHOptions{},
UTLS: &option.OutboundUTLSOptions{},
Reality: &option.OutboundRealityOptions{},
}
for key, value := range proxy {
switch key {
case "ps":
outbound.Tag = value
case "add":
options.Server = value
TLSOptions.ServerName = value
case "port":
options.ServerPort = common.StringToType[uint16](value)
case "id":
options.UUID = value
case "scy":
options.Security = value
case "aid":
options.AlterId, _ = strconv.Atoi(value)
case "packet_encoding":
options.PacketEncoding = value
case "xudp":
if value == "1" || value == "true" {
options.PacketEncoding = "xudp"
}
case "tls":
if value == "1" || value == "true" || value == "tls" {
TLSOptions.Enabled = true
}
case "insecure", "skip-cert-verify":
if value == "1" || value == "true" {
TLSOptions.Insecure = true
}
case "fp":
TLSOptions.UTLS.Enabled = true
TLSOptions.UTLS.Fingerprint = value
case "net":
Transport := option.V2RayTransportOptions{
Type: "",
WebsocketOptions: option.V2RayWebsocketOptions{
Headers: badoption.HTTPHeader{},
},
HTTPOptions: option.V2RayHTTPOptions{
Host: badoption.Listable[string]{},
Headers: map[string]badoption.Listable[string]{},
},
GRPCOptions: option.V2RayGRPCOptions{},
}
switch value {
case "ws":
Transport.Type = C.V2RayTransportTypeWebsocket
Transport.WebsocketOptions = v2rayTransportWs(proxy["host"], proxy["path"])
case "h2":
Transport.Type = C.V2RayTransportTypeHTTP
TLSOptions.Enabled = true
if host, exists := proxy["host"]; exists && host != "" {
Transport.HTTPOptions.Host = []string{host}
}
if path, exists := proxy["path"]; exists && path != "" {
Transport.HTTPOptions.Path = path
}
case "tcp":
if tType, exists := proxy["type"]; exists {
if tType != "http" {
continue
}
Transport.Type = C.V2RayTransportTypeHTTP
if method, exists := proxy["method"]; exists {
Transport.HTTPOptions.Method = method
}
if host, exists := proxy["host"]; exists && host != "" {
Transport.HTTPOptions.Host = []string{host}
}
if path, exists := proxy["path"]; exists && path != "" {
Transport.HTTPOptions.Path = path
}
if headers, exists := proxy["headers"]; exists {
Transport.HTTPOptions.Headers = common.StringToType[badoption.HTTPHeader](headers)
}
}
case "grpc":
Transport.Type = C.V2RayTransportTypeGRPC
if host, exists := proxy["host"]; exists && host != "" {
Transport.GRPCOptions.ServiceName = host
}
default:
continue
}
options.Transport = &Transport
case "tfo", "tcp-fast-open", "tcp_fast_open":
if value == "1" || value == "true" {
options.TCPFastOpen = true
}
}
}
if TLSOptions.Enabled {
options.TLS = &TLSOptions
}
outbound.Options = &options
return outbound, nil
}

31
parser/parser.go Normal file
View File

@@ -0,0 +1,31 @@
package parser
import (
"context"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/parser/clash"
"github.com/sagernet/sing-box/parser/raw"
"github.com/sagernet/sing-box/parser/singbox"
"github.com/sagernet/sing-box/parser/sip008"
E "github.com/sagernet/sing/common/exceptions"
)
var subscriptionParsers = []func(ctx context.Context, content string) ([]option.Outbound, error){
singbox.ParseBoxSubscription,
clash.ParseClashSubscription,
sip008.ParseSIP008Subscription,
raw.ParseRawSubscription,
}
func ParseSubscription(ctx context.Context, content string) ([]option.Outbound, error) {
var pErr error
for _, parser := range subscriptionParsers {
servers, err := parser(ctx, content)
if len(servers) > 0 {
return servers, nil
}
pErr = E.Errors(pErr, err)
}
return nil, E.Cause(pErr, "no servers found")
}

50
parser/raw/parser.go Normal file
View File

@@ -0,0 +1,50 @@
package raw
import (
"context"
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/parser/link"
E "github.com/sagernet/sing/common/exceptions"
)
func ParseRawSubscription(ctx context.Context, content string) ([]option.Outbound, error) {
if base64Content, err := DecodeBase64URLSafe(content); err == nil {
servers, _ := parseRawSubscription(base64Content)
if len(servers) > 0 {
return servers, err
}
}
return parseRawSubscription(content)
}
func parseRawSubscription(content string) ([]option.Outbound, error) {
var servers []option.Outbound
content = strings.ReplaceAll(content, "\r\n", "\n")
linkList := strings.Split(content, "\n")
for _, linkLine := range linkList {
server, err := link.ParseSubscriptionLink(linkLine)
if err != nil {
continue
}
servers = append(servers, server)
}
if len(servers) == 0 {
return nil, E.New("no servers found")
}
return servers, nil
}
func DecodeBase64URLSafe(content string) (string, error) {
s := strings.ReplaceAll(content, " ", "-")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, "+", "-")
s = strings.ReplaceAll(s, "=", "")
result, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return content, nil
}
return string(result), nil
}

58
parser/singbox/parser.go Normal file
View File

@@ -0,0 +1,58 @@
package singbox
import (
"context"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
)
type _SingBoxDocument struct {
Outbounds []option.Outbound `json:"outbounds"`
}
type SingBoxDocument _SingBoxDocument
func (o *SingBoxDocument) UnmarshalJSONContext(ctx context.Context, inputContent []byte) error {
var content badjson.JSONObject
err := content.UnmarshalJSONContext(ctx, inputContent)
if err != nil {
return err
}
outbounds, ok := content.Get("outbounds")
if !ok {
return E.New("missing outbounds in sing-box configuration")
}
var outs badjson.JSONArray
for i, outbound := range outbounds.(badjson.JSONArray) {
typeVal, loaded := outbound.(*badjson.JSONObject).Get("type")
if !loaded {
return E.New("missing type in outbound[", i, "]")
}
switch typeVal.(string) {
case C.TypeDirect, C.TypeBlock, C.TypeDNS, C.TypeSelector, C.TypeURLTest:
continue
default:
outs = append(outs, outbound)
}
}
content.Put("outbounds", outs)
inputContent, err = content.MarshalJSONContext(ctx)
if err != nil {
return err
}
return json.UnmarshalContext(ctx, inputContent, (*_SingBoxDocument)(o))
}
func ParseBoxSubscription(ctx context.Context, content string) ([]option.Outbound, error) {
options, err := json.UnmarshalExtendedContext[SingBoxDocument](ctx, []byte(content))
if err != nil {
return nil, err
}
if len(options.Outbounds) == 0 {
return nil, E.New("no servers found")
}
return options.Outbounds, nil
}

53
parser/sip008/parser.go Normal file
View File

@@ -0,0 +1,53 @@
package sip008
import (
"context"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
)
type ShadowsocksDocument struct {
Version int `json:"version"`
Servers []ShadowsocksServerDocument `json:"servers"`
}
type ShadowsocksServerDocument struct {
ID string `json:"id"`
Remarks string `json:"remarks"`
Server string `json:"server"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Method string `json:"method"`
Plugin string `json:"plugin"`
PluginOpts string `json:"plugin_opts"`
}
func ParseSIP008Subscription(_ context.Context, content string) ([]option.Outbound, error) {
var document ShadowsocksDocument
err := json.Unmarshal([]byte(content), &document)
if err != nil {
return nil, E.Cause(err, "parse SIP008 document")
}
var servers []option.Outbound
for _, server := range document.Servers {
servers = append(servers, option.Outbound{
Type: C.TypeShadowsocks,
Tag: server.Remarks,
Options: &option.ShadowsocksOutboundOptions{
ServerOptions: option.ServerOptions{
Server: server.Server,
ServerPort: uint16(server.ServerPort),
},
Password: server.Password,
Method: server.Method,
Plugin: server.Plugin,
PluginOptions: server.PluginOpts,
},
})
}
return servers, nil
}