mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling
This commit is contained in:
30
parser/clash/anytls.go
Normal file
30
parser/clash/anytls.go
Normal 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
181
parser/clash/base.go
Normal 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
23
parser/clash/http.go
Normal 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
47
parser/clash/hysteria.go
Normal 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
34
parser/clash/hysteria2.go
Normal 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
106
parser/clash/parser.go
Normal 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
|
||||
}
|
||||
51
parser/clash/shadowsocks.go
Normal file
51
parser/clash/shadowsocks.go
Normal 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
21
parser/clash/socks5.go
Normal 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
36
parser/clash/ssh.go
Normal 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
28
parser/clash/trojan.go
Normal 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
47
parser/clash/tuic.go
Normal 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
205
parser/clash/utils.go
Normal 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
49
parser/clash/vless.go
Normal 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
55
parser/clash/vmess.go
Normal 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
71
parser/link/hysteria.go
Normal 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
61
parser/link/hysteria2.go
Normal 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
42
parser/link/parser.go
Normal 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)
|
||||
}
|
||||
}
|
||||
39
parser/link/shadowsocks.go
Normal file
39
parser/link/shadowsocks.go
Normal 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
89
parser/link/trojan.go
Normal 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
81
parser/link/tuic.go
Normal 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
46
parser/link/utils.go
Normal 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
114
parser/link/vless.go
Normal 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
160
parser/link/vmess.go
Normal 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
31
parser/parser.go
Normal 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
50
parser/raw/parser.go
Normal 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
58
parser/singbox/parser.go
Normal 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
53
parser/sip008/parser.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user