mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 05:44:12 +08:00
add link package
This commit is contained in:
parent
c1eb9e807a
commit
fd9184bbde
34
common/link/link.go
Normal file
34
common/link/link.go
Normal file
@ -0,0 +1,34 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
// Link is the interface for v2ray links
|
||||
type Link interface {
|
||||
// Detail returns human readable string
|
||||
Options() *option.Outbound
|
||||
// String unmarshals Link to string
|
||||
String() string
|
||||
}
|
||||
|
||||
// Parse parses a link string to Link
|
||||
func Parse(arg string) (Link, error) {
|
||||
ps, err := getParsers(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errs := make([]error, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
lk, err := p.Parse(arg)
|
||||
if err == nil {
|
||||
return lk, nil
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return nil, errs[0]
|
||||
}
|
||||
return nil, E.Errors(errs...)
|
||||
}
|
52
common/link/ng.go
Normal file
52
common/link/ng.go
Normal file
@ -0,0 +1,52 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
common.Must(RegisterParser(&Parser{
|
||||
Name: "V2RayNG",
|
||||
Scheme: []string{"vmess"},
|
||||
Parse: func(input string) (Link, error) {
|
||||
return ParseVMessV2RayNG(input)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// VMessV2RayNG is the vmess link of V2RayNG
|
||||
type VMessV2RayNG struct {
|
||||
vmess
|
||||
}
|
||||
|
||||
// String implements Link
|
||||
func (v VMessV2RayNG) String() string {
|
||||
b, _ := json.Marshal(v)
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// ParseVMessV2RayNG parses V2RayN vemss link
|
||||
func ParseVMessV2RayNG(vmess string) (*VMessV2RayNG, error) {
|
||||
if !strings.HasPrefix(vmess, "vmess://") {
|
||||
return nil, fmt.Errorf("vmess unreconized: %s", vmess)
|
||||
}
|
||||
|
||||
b64 := vmess[8:]
|
||||
b, err := base64Decode(b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &VMessV2RayNG{}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.OrigLink = vmess
|
||||
|
||||
return v, nil
|
||||
}
|
53
common/link/parsers.go
Normal file
53
common/link/parsers.go
Normal file
@ -0,0 +1,53 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
// ParseFunc is parser function to load links, like "vmess://..."
|
||||
type ParseFunc func(input string) (Link, error)
|
||||
|
||||
// Parser is parser load v2ray links with specified schemes
|
||||
type Parser struct {
|
||||
Name string
|
||||
Scheme []string
|
||||
Parse ParseFunc
|
||||
}
|
||||
|
||||
var (
|
||||
parsers = make(map[string][]*Parser)
|
||||
)
|
||||
|
||||
// RegisterParser add a new link parser.
|
||||
func RegisterParser(parser *Parser) error {
|
||||
for _, scheme := range parser.Scheme {
|
||||
s := strings.ToLower(scheme)
|
||||
ps := parsers[s]
|
||||
if len(ps) == 0 {
|
||||
ps = make([]*Parser, 0)
|
||||
}
|
||||
parsers[s] = append(ps, parser)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getParsers(link string) ([]*Parser, error) {
|
||||
u, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
return nil, E.New("invalid link")
|
||||
}
|
||||
s := strings.ToLower(u.Scheme)
|
||||
ps := parsers[s]
|
||||
if len(ps) == 0 {
|
||||
return nil, fmt.Errorf("unsupported link scheme: %s", u.Scheme)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
116
common/link/ss.go
Normal file
116
common/link/ss.go
Normal file
@ -0,0 +1,116 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ Link = (*SSLink)(nil)
|
||||
|
||||
func init() {
|
||||
common.Must(RegisterParser(&Parser{
|
||||
Name: "Shadowsocks",
|
||||
Scheme: []string{"ss"},
|
||||
Parse: func(input string) (Link, error) {
|
||||
return ParseShadowSocks(input)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// ParseShadowSocks parses official vemss link to Link
|
||||
func ParseShadowSocks(ss string) (*SSLink, error) {
|
||||
url, err := url.Parse(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if url.Scheme != "ss" {
|
||||
return nil, E.New("not a ss:// link")
|
||||
}
|
||||
port, err := strconv.ParseUint(url.Port(), 10, 16)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid port")
|
||||
}
|
||||
link := &SSLink{
|
||||
OrigLink: ss,
|
||||
Address: url.Hostname(),
|
||||
Port: uint16(port),
|
||||
Ps: url.Fragment,
|
||||
}
|
||||
queries := url.Query()
|
||||
for key, values := range queries {
|
||||
switch key {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported shadowsocks parameter: %s=%v", key, values)
|
||||
}
|
||||
}
|
||||
if uname := url.User.Username(); uname != "" {
|
||||
if pass, ok := url.User.Password(); ok {
|
||||
link.Method = uname
|
||||
link.Password = pass
|
||||
} else {
|
||||
dec, err := base64Decode(uname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts := strings.Split(string(dec), ":")
|
||||
link.Method = parts[0]
|
||||
if len(parts) > 1 {
|
||||
link.Password = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
// SSLink represents a parsed shadowsocks link
|
||||
type SSLink struct {
|
||||
Method string `json:"method,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Ps string `json:"ps,omitempty"`
|
||||
|
||||
OrigLink string `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
// String implements Link
|
||||
func (v SSLink) String() string {
|
||||
return v.OrigLink
|
||||
}
|
||||
|
||||
// Options implements Link
|
||||
func (v *SSLink) Options() *option.Outbound {
|
||||
return &option.Outbound{
|
||||
Type: "shadowsocks",
|
||||
Tag: v.Ps,
|
||||
ShadowsocksOptions: option.ShadowsocksOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: v.Address,
|
||||
ServerPort: v.Port,
|
||||
},
|
||||
Method: v.Method,
|
||||
Password: v.Password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func base64Decode(b64 string) ([]byte, error) {
|
||||
b64 = strings.TrimSpace(b64)
|
||||
stdb64 := b64
|
||||
if pad := len(b64) % 4; pad != 0 {
|
||||
stdb64 += strings.Repeat("=", 4-pad)
|
||||
}
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(stdb64)
|
||||
if err != nil {
|
||||
return base64.URLEncoding.DecodeString(b64)
|
||||
}
|
||||
return b, nil
|
||||
}
|
70
common/link/vmess.go
Normal file
70
common/link/vmess.go
Normal file
@ -0,0 +1,70 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type vmess struct {
|
||||
Ver string `json:"v,omitempty"`
|
||||
Add string `json:"add,omitempty"`
|
||||
Aid int `json:"aid,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Net string `json:"net,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Ps string `json:"ps,omitempty"`
|
||||
TLS string `json:"tls,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
OrigLink string `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
// Options implements Link
|
||||
func (v *vmess) Options() *option.Outbound {
|
||||
out := &option.Outbound{
|
||||
Type: "vmess",
|
||||
Tag: v.Ps,
|
||||
VMessOptions: option.VMessOutboundOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: v.Add,
|
||||
ServerPort: v.Port,
|
||||
},
|
||||
UUID: v.ID,
|
||||
AlterId: v.Aid,
|
||||
Security: "auto",
|
||||
},
|
||||
}
|
||||
|
||||
opt := &option.V2RayTransportOptions{}
|
||||
|
||||
switch v.Net {
|
||||
case "":
|
||||
opt = nil
|
||||
case "tcp", "h2", "http":
|
||||
opt.Type = C.V2RayTransportTypeHTTP
|
||||
opt.HTTPOptions.Path = strings.Split(v.Path, ",")[0]
|
||||
if v.Host != "" {
|
||||
opt.HTTPOptions.Host = strings.Split(v.Host, ",")
|
||||
opt.HTTPOptions.Headers["Host"] = opt.HTTPOptions.Host[0]
|
||||
}
|
||||
case "ws":
|
||||
opt.Type = C.V2RayTransportTypeWebsocket
|
||||
opt.WebsocketOptions.Path = v.Path
|
||||
opt.WebsocketOptions.Headers = map[string]string{
|
||||
"Host": v.Host,
|
||||
}
|
||||
}
|
||||
|
||||
if v.TLS == "tls" {
|
||||
out.VMessOptions.TLS = &option.OutboundTLSOptions{
|
||||
Insecure: true,
|
||||
ServerName: v.Host,
|
||||
}
|
||||
}
|
||||
|
||||
out.VMessOptions.Transport = opt
|
||||
return out
|
||||
}
|
122
common/link/vmess_quantumult.go
Normal file
122
common/link/vmess_quantumult.go
Normal file
@ -0,0 +1,122 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func init() {
|
||||
common.Must(RegisterParser(&Parser{
|
||||
Name: "Quantumult",
|
||||
Scheme: []string{"vmess"},
|
||||
Parse: func(input string) (Link, error) {
|
||||
return ParseVMessQuantumult(input)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// VMessQuantumult is the vmess link of Quantumult
|
||||
type VMessQuantumult struct {
|
||||
vmess
|
||||
}
|
||||
|
||||
// String implements Link
|
||||
func (v VMessQuantumult) String() string {
|
||||
/*
|
||||
let obfs = `,obfs=${jsonConf.net === 'ws' ? 'ws' : 'http'},obfs-path="${jsonConf.path || '/'}",obfs-header="Host:${jsonConf.host || jsonConf.add}[Rr][Nn]User-Agent:${ua}"`
|
||||
let quanVmess = `${jsonConf.ps} = vmess,${jsonConf.add},${jsonConf.port},${method},"${jsonConf.id}",over-tls=${jsonConf.tls === 'tls' ? 'true' : 'false'},certificate=1${jsonConf.type === 'none' && jsonConf.net !== 'ws' ? '' : obfs},group=${group}`
|
||||
*/
|
||||
|
||||
method := "aes-128-gcm"
|
||||
vbase := fmt.Sprintf("%s = vmess,%s,%d,%s,\"%s\",over-tls=%v,certificate=1", v.Ps, v.Add, v.Port, method, v.ID, v.TLS == "tls")
|
||||
|
||||
var obfs string
|
||||
if (v.Net == "ws" || v.Net == "http") && (v.Type == "none" || v.Type == "") {
|
||||
if v.Path == "" {
|
||||
v.Path = "/"
|
||||
}
|
||||
if v.Host == "" {
|
||||
v.Host = v.Add
|
||||
}
|
||||
obfs = fmt.Sprintf(`,obfs=ws,obfs-path="%s",obfs-header="Host:%s[Rr][Nn]User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16A5366a"`, v.Path, v.Host)
|
||||
}
|
||||
|
||||
vbase += obfs
|
||||
vbase += ",group=Fndroid"
|
||||
return "vmess://" + base64.URLEncoding.EncodeToString([]byte(vbase))
|
||||
}
|
||||
|
||||
// ParseVMessQuantumult parses Quantumult vemss link
|
||||
func ParseVMessQuantumult(vmess string) (*VMessQuantumult, error) {
|
||||
if !strings.HasPrefix(vmess, "vmess://") {
|
||||
return nil, fmt.Errorf("vmess unreconized: %s", vmess)
|
||||
}
|
||||
b64 := vmess[8:]
|
||||
b, err := base64Decode(b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := string(b)
|
||||
v := &VMessQuantumult{}
|
||||
v.OrigLink = vmess
|
||||
v.Ver = "2"
|
||||
|
||||
psn := strings.SplitN(info, " = ", 2)
|
||||
if len(psn) != 2 {
|
||||
return nil, fmt.Errorf("part error: %s", info)
|
||||
}
|
||||
|
||||
v.Ps = psn[0]
|
||||
params := strings.Split(psn[1], ",")
|
||||
port, err := strconv.ParseUint(params[2], 10, 16)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid port")
|
||||
}
|
||||
v.Add = params[1]
|
||||
v.Port = uint16(port)
|
||||
v.ID = strings.Trim(params[4], "\"")
|
||||
v.Aid = 0
|
||||
v.Net = "tcp"
|
||||
v.Type = "none"
|
||||
|
||||
if len(params) > 4 {
|
||||
for _, pkv := range params[5:] {
|
||||
kvp := strings.SplitN(pkv, "=", 2)
|
||||
switch kvp[0] {
|
||||
case "over-tls":
|
||||
if kvp[1] == "true" {
|
||||
v.TLS = "tls"
|
||||
}
|
||||
case "obfs":
|
||||
switch kvp[1] {
|
||||
case "ws", "http":
|
||||
v.Net = kvp[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported quantumult vmess obfs parameter: %s", kvp[1])
|
||||
}
|
||||
case "obfs-path":
|
||||
v.Path = strings.Trim(kvp[1], "\"")
|
||||
case "obfs-header":
|
||||
hd := strings.Trim(kvp[1], "\"")
|
||||
for _, hl := range strings.Split(hd, "[Rr][Nn]") {
|
||||
if strings.HasPrefix(hl, "Host:") {
|
||||
host := hl[5:]
|
||||
if host != v.Add {
|
||||
v.Host = host
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
125
common/link/vmess_rocket.go
Normal file
125
common/link/vmess_rocket.go
Normal file
@ -0,0 +1,125 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ Link = (*VMessRocket)(nil)
|
||||
|
||||
func init() {
|
||||
common.Must(RegisterParser(&Parser{
|
||||
Name: "VMess ShadowRocket",
|
||||
Scheme: []string{"vmess"},
|
||||
Parse: func(input string) (Link, error) {
|
||||
return ParseVMessRocket(input)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// VMessRocket is the vmess link of ShadowRocket
|
||||
type VMessRocket struct {
|
||||
vmess
|
||||
}
|
||||
|
||||
// String implements Link
|
||||
func (v VMessRocket) String() string {
|
||||
mhp := fmt.Sprintf("%s:%s@%s:%d", v.Type, v.ID, v.Add, v.Port)
|
||||
qs := url.Values{}
|
||||
qs.Add("remarks", v.Ps)
|
||||
if v.Net == "ws" {
|
||||
qs.Add("obfs", "websocket")
|
||||
}
|
||||
if v.Host != "" {
|
||||
qs.Add("obfsParam", v.Host)
|
||||
}
|
||||
if v.Path != "" {
|
||||
qs.Add("path", v.Host)
|
||||
}
|
||||
if v.TLS == "tls" {
|
||||
qs.Add("tls", "1")
|
||||
}
|
||||
|
||||
url := url.URL{
|
||||
Scheme: "vmess",
|
||||
Host: base64.URLEncoding.EncodeToString([]byte(mhp)),
|
||||
RawQuery: qs.Encode(),
|
||||
}
|
||||
|
||||
return url.String()
|
||||
}
|
||||
|
||||
// ParseVMessRocket parses ShadowRocket vemss link string to VMessRocket
|
||||
func ParseVMessRocket(vmess string) (*VMessRocket, error) {
|
||||
url, err := url.Parse(vmess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if url.Scheme != "vmess" {
|
||||
return nil, E.New("not a vmess:// link")
|
||||
}
|
||||
link := &VMessRocket{}
|
||||
link.Ver = "2"
|
||||
link.OrigLink = vmess
|
||||
|
||||
b64 := url.Host
|
||||
b, err := base64Decode(b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mhp := strings.SplitN(string(b), ":", 3)
|
||||
if len(mhp) != 3 {
|
||||
return nil, fmt.Errorf("vmess unreconized: method:host:port -- %v", mhp)
|
||||
}
|
||||
port, err := strconv.ParseUint(mhp[2], 10, 16)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid port")
|
||||
}
|
||||
// mhp[0] is the encryption method
|
||||
link.Port = uint16(port)
|
||||
idadd := strings.SplitN(mhp[1], "@", 2)
|
||||
if len(idadd) != 2 {
|
||||
return nil, fmt.Errorf("vmess unreconized: id@addr -- %v", idadd)
|
||||
}
|
||||
link.ID = idadd[0]
|
||||
link.Add = idadd[1]
|
||||
link.Aid = 0
|
||||
|
||||
for key, values := range url.Query() {
|
||||
switch key {
|
||||
case "remarks":
|
||||
link.Ps = firstValueOf(values)
|
||||
case "path":
|
||||
link.Path = firstValueOf(values)
|
||||
case "tls":
|
||||
link.TLS = firstValueOf(values)
|
||||
case "obfs":
|
||||
v := firstValueOf(values)
|
||||
switch v {
|
||||
case "websocket":
|
||||
link.Net = "ws"
|
||||
case "none":
|
||||
link.Net = ""
|
||||
}
|
||||
case "obfsParam":
|
||||
link.Host = firstValueOf(values)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported shadowrocket vmess parameter: %s=%v", key, values)
|
||||
}
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func firstValueOf(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user