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

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
}