vmess links refactor

This commit is contained in:
jebbs 2022-10-17 14:52:59 +08:00
parent a66a89308a
commit 27a2d2c0f5
9 changed files with 360 additions and 120 deletions

View File

@ -1,44 +0,0 @@
package link
import (
"encoding/json"
"net/url"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
func init() {
common.Must(RegisterParser(&Parser{
Name: "V2RayNG",
Scheme: []string{"vmess"},
Parse: func(u *url.URL) (Link, error) {
link := &VMessV2RayNG{}
return link, link.Parse(u)
},
}))
}
// VMessV2RayNG is the vmess link of V2RayNG
type VMessV2RayNG struct {
vmess
}
// Parse implements Link
func (l *VMessV2RayNG) Parse(u *url.URL) error {
if u.Scheme != "vmess" {
return E.New("not a vmess link")
}
b64 := u.Host
b, err := base64Decode(b64)
if err != nil {
return err
}
if err := json.Unmarshal(b, l); err != nil {
return err
}
return nil
}

37
common/link/number.go Normal file
View File

@ -0,0 +1,37 @@
package link
import (
"encoding/json"
"fmt"
"strconv"
)
// number supports json unmarshaling from number or string
type number int64
// UnmarshalJSON implements json.Unmarshaler
func (i *number) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case string:
switch value {
case "", "null":
*i = 0
default:
var err error
v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
*i = number(v)
}
case float64:
*i = number(value)
default:
return fmt.Errorf("invalid var int: %v", v)
}
return nil
}

View File

@ -1,67 +1,70 @@
package link package link
import ( import (
"strings"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
) )
type vmess struct { // Vmess is the base struct of vmess link
Ver string `json:"v,omitempty"` type Vmess struct {
Add string `json:"add,omitempty"` Tag string
Aid int `json:"aid,omitempty"` Server string
Host string `json:"host,omitempty"` ServerPort uint16
ID string `json:"id,omitempty"` UUID string
Net string `json:"net,omitempty"` AlterID int
Path string `json:"path,omitempty"` Security string
Port uint16 `json:"port,omitempty"` Transport string
Ps string `json:"ps,omitempty"` TransportPath string
TLS string `json:"tls,omitempty"` Host string
Type string `json:"type,omitempty"` TLS bool
TLSAllowInsecure bool
} }
// Options implements Link // Options implements Link
func (v *vmess) Options() *option.Outbound { func (v *Vmess) Options() *option.Outbound {
out := &option.Outbound{ out := &option.Outbound{
Type: "vmess", Type: "vmess",
Tag: v.Ps, Tag: v.Tag,
VMessOptions: option.VMessOutboundOptions{ VMessOptions: option.VMessOutboundOptions{
ServerOptions: option.ServerOptions{ ServerOptions: option.ServerOptions{
Server: v.Add, Server: v.Server,
ServerPort: v.Port, ServerPort: v.ServerPort,
}, },
UUID: v.ID, UUID: v.UUID,
AlterId: v.Aid, AlterId: v.AlterID,
Security: "auto", Security: v.Security,
}, },
} }
opt := &option.V2RayTransportOptions{} if v.TLS {
out.VMessOptions.TLS = &option.OutboundTLSOptions{
switch v.Net { Insecure: v.TLSAllowInsecure,
case "": ServerName: v.Host,
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" { opt := &option.V2RayTransportOptions{
out.VMessOptions.TLS = &option.OutboundTLSOptions{ Type: v.Transport,
Insecure: true, }
ServerName: v.Host,
switch v.Transport {
case "":
opt = nil
case C.V2RayTransportTypeHTTP:
opt.HTTPOptions.Path = v.TransportPath
if v.Host != "" {
opt.HTTPOptions.Host = []string{v.Host}
opt.HTTPOptions.Headers["Host"] = v.Host
} }
case C.V2RayTransportTypeWebsocket:
opt.WebsocketOptions.Path = v.TransportPath
opt.WebsocketOptions.Headers = map[string]string{
"Host": v.Host,
}
case C.V2RayTransportTypeQUIC:
// do nothing
case C.V2RayTransportTypeGRPC:
opt.GRPCOptions.ServiceName = v.Host
} }
out.VMessOptions.Transport = opt out.VMessOptions.Transport = opt

92
common/link/vmess_ng.go Normal file
View File

@ -0,0 +1,92 @@
package link
import (
"encoding/json"
"net/url"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
func init() {
common.Must(RegisterParser(&Parser{
Name: "V2RayNG",
Scheme: []string{"vmess"},
Parse: func(u *url.URL) (Link, error) {
link := &VMessV2RayNG{}
return link, link.Parse(u)
},
}))
}
// VMessV2RayNG is the vmess link of V2RayNG
type VMessV2RayNG struct {
Vmess
Ver string
}
type _vmessV2RayNG struct {
V number `json:"v,omitempty"`
Ps string `json:"ps,omitempty"`
Add string `json:"add,omitempty"`
Port number `json:"port,omitempty"`
ID string `json:"id,omitempty"`
Aid number `json:"aid,omitempty"`
Scy string `json:"scy,omitempty"`
Security string `json:"security,omitempty"`
SkipCertVerify bool `json:"skip-cert-verify,omitempty"`
Net string `json:"net,omitempty"`
Type string `json:"type,omitempty"`
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
TLS string `json:"tls,omitempty"`
SNI string `json:"sni,omitempty"`
ALPN string `json:"alpn,omitempty"`
}
// Parse implements Link
func (l *VMessV2RayNG) Parse(u *url.URL) error {
if u.Scheme != "vmess" {
return E.New("not a vmess link")
}
b64 := u.Host + u.Path
b, err := base64Decode(b64)
if err != nil {
return err
}
v := _vmessV2RayNG{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
l.Tag = v.Ps
l.Server = v.Add
l.ServerPort = uint16(v.Port)
l.UUID = v.ID
l.AlterID = int(v.Aid)
if v.Scy != "" {
l.Security = v.Scy
} else {
l.Security = v.Security
}
l.Host = v.Host
l.TransportPath = v.Path
l.TLS = v.TLS == "tls"
l.TLSAllowInsecure = v.SkipCertVerify
// _ = v.Type
// _ = v.SNI
// _ = v.ALPN
switch v.Net {
case "ws", "websocket":
l.Transport = C.V2RayTransportTypeWebsocket
case "http":
l.Transport = C.V2RayTransportTypeHTTP
}
return nil
}

View File

@ -0,0 +1,48 @@
package link_test
import (
"net/url"
"testing"
"github.com/sagernet/sing-box/common/link"
C "github.com/sagernet/sing-box/constant"
)
func TestVMessV2RayNG(t *testing.T) {
tests := []struct {
link string
want link.Vmess
}{
{
link: "vmess://ewoiYWRkIjogIjE5Mi4xNjguMTAwLjEiLAoidiI6ICIyIiwKInBzIjogInBzIiwKInBvcnQiOiA0NDMsCiJpZCI6ICJ1dWlkIiwKImFpZCI6ICI0IiwKIm5ldCI6ICJ3cyIsCiJ0eXBlIjogInR5cGUiLAoiaG9zdCI6ICJob3N0IiwKInBhdGgiOiAiL3BhdGgiLAoidGxzIjogInRscyIsCiJzbmkiOiAic25pIiwKImFscG4iOiJhbHBuIiwKInNlY3VyaXR5IjogImF1dG8iLAoic2tpcC1jZXJ0LXZlcmlmeSI6IGZhbHNlCn0=",
want: link.Vmess{
Tag: "ps",
Server: "192.168.100.1",
ServerPort: 443,
UUID: "uuid",
AlterID: 4,
Security: "auto",
Host: "host",
Transport: C.V2RayTransportTypeWebsocket,
TransportPath: "/path",
TLS: true,
TLSAllowInsecure: false,
},
},
}
for _, tt := range tests {
u, err := url.Parse(tt.link)
if err != nil {
t.Fatal(err)
}
link := link.VMessV2RayNG{}
err = link.Parse(u)
if err != nil {
t.Error(err)
return
}
if link.Vmess != tt.want {
t.Errorf("want %#v, got %#v", tt.want, link.Vmess)
}
}
}

View File

@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@ -23,7 +24,7 @@ func init() {
// VMessQuantumult is the vmess link of Quantumult // VMessQuantumult is the vmess link of Quantumult
type VMessQuantumult struct { type VMessQuantumult struct {
vmess Vmess
} }
// Parse implements Link // Parse implements Link
@ -31,31 +32,30 @@ func (l *VMessQuantumult) Parse(u *url.URL) error {
if u.Scheme != "vmess" { if u.Scheme != "vmess" {
return E.New("not a vmess link") return E.New("not a vmess link")
} }
b, err := base64Decode(u.Host) b, err := base64Decode(u.Host + u.Path)
if err != nil { if err != nil {
return err return err
} }
info := string(b) info := string(b)
l.Ver = "2"
psn := strings.SplitN(info, " = ", 2) psn := strings.SplitN(info, " = ", 2)
if len(psn) != 2 { if len(psn) != 2 {
return fmt.Errorf("part error: %s", info) return fmt.Errorf("part error: %s", info)
} }
l.Ps = psn[0] l.Tag = psn[0]
params := strings.Split(psn[1], ",") params := strings.Split(psn[1], ",")
port, err := strconv.ParseUint(params[2], 10, 16) port, err := strconv.ParseUint(params[2], 10, 16)
if err != nil { if err != nil {
return E.Cause(err, "invalid port") return E.Cause(err, "invalid port")
} }
l.Add = params[1] l.Server = params[1]
l.Port = uint16(port) l.ServerPort = uint16(port)
l.ID = strings.Trim(params[4], "\"") l.Security = params[3]
l.Aid = 0 l.UUID = strings.Trim(params[4], "\"")
l.Net = "tcp" l.AlterID = 0
l.Type = "none" l.Transport = ""
if len(params) > 4 { if len(params) > 4 {
for _, pkv := range params[5:] { for _, pkv := range params[5:] {
@ -63,30 +63,36 @@ func (l *VMessQuantumult) Parse(u *url.URL) error {
switch kvp[0] { switch kvp[0] {
case "over-tls": case "over-tls":
if kvp[1] == "true" { if kvp[1] == "true" {
l.TLS = "tls" l.TLS = true
} }
case "obfs": case "obfs":
switch kvp[1] { switch kvp[1] {
case "ws", "http": case "ws":
l.Net = kvp[1] l.Transport = C.V2RayTransportTypeWebsocket
case "http":
l.Transport = C.V2RayTransportTypeHTTP
default: default:
return fmt.Errorf("unsupported quantumult vmess obfs parameter: %s", kvp[1]) return fmt.Errorf("unsupported quantumult vmess obfs parameter: %s", kvp[1])
} }
case "obfs-path": case "obfs-path":
l.Path = strings.Trim(kvp[1], "\"") l.TransportPath = strings.Trim(kvp[1], "\"")
case "obfs-header": case "obfs-header":
hd := strings.Trim(kvp[1], "\"") hd := strings.Trim(kvp[1], "\"")
for _, hl := range strings.Split(hd, "[Rr][Nn]") { for _, hl := range strings.Split(hd, "[Rr][Nn]") {
if strings.HasPrefix(hl, "Host:") { if strings.HasPrefix(hl, "Host:") {
host := hl[5:] l.Host = hl[5:]
if host != l.Add {
l.Host = host
}
break break
} }
} }
default: case "certificate":
return fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv) switch kvp[1] {
case "0":
l.TLSAllowInsecure = true
default:
l.TLSAllowInsecure = false
}
// default:
// return fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv)
} }
} }
} }

View File

@ -0,0 +1,48 @@
package link_test
import (
"net/url"
"testing"
"github.com/sagernet/sing-box/common/link"
C "github.com/sagernet/sing-box/constant"
)
func TestVMessQuantumult(t *testing.T) {
tests := []struct {
link string
want link.Vmess
}{
{
link: "vmess://cHMgPSB2bWVzcywxOTIuMTY4LjEwMC4xLDQ0MyxhZXMtMTI4LWdjbSwidXVpZCIsb3Zlci10bHM9dHJ1ZSxjZXJ0aWZpY2F0ZT0wLG9iZnM9d3Msb2Jmcy1wYXRoPSIvcGF0aCIsb2Jmcy1oZWFkZXI9Ikhvc3Q6aG9zdFtScl1bTm5dd2hhdGV2ZXI=",
want: link.Vmess{
Tag: "ps",
Server: "192.168.100.1",
ServerPort: 443,
UUID: "uuid",
AlterID: 0,
Security: "aes-128-gcm",
Host: "host",
Transport: C.V2RayTransportTypeWebsocket,
TransportPath: "/path",
TLS: true,
TLSAllowInsecure: true,
},
},
}
for _, tt := range tests {
u, err := url.Parse(tt.link)
if err != nil {
t.Fatal(err)
}
link := link.VMessQuantumult{}
err = link.Parse(u)
if err != nil {
t.Error(err)
return
}
if link.Vmess != tt.want {
t.Errorf("want %#v, got %#v", tt.want, link.Vmess)
}
}
}

View File

@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@ -25,7 +26,9 @@ func init() {
// VMessRocket is the vmess link of ShadowRocket // VMessRocket is the vmess link of ShadowRocket
type VMessRocket struct { type VMessRocket struct {
vmess Vmess
Ver string
} }
// Parse implements Link // Parse implements Link
@ -50,35 +53,34 @@ func (l *VMessRocket) Parse(u *url.URL) error {
return E.Cause(err, "invalid port") return E.Cause(err, "invalid port")
} }
// mhp[0] is the encryption method // mhp[0] is the encryption method
l.Port = uint16(port) l.ServerPort = uint16(port)
idadd := strings.SplitN(mhp[1], "@", 2) idadd := strings.SplitN(mhp[1], "@", 2)
if len(idadd) != 2 { if len(idadd) != 2 {
return fmt.Errorf("vmess unreconized: id@addr -- %v", idadd) return fmt.Errorf("vmess unreconized: id@addr -- %v", idadd)
} }
l.ID = idadd[0] l.UUID = idadd[0]
l.Add = idadd[1] l.Server = idadd[1]
l.Aid = 0 l.AlterID = 0
l.Security = "auto"
for key, values := range u.Query() { for key, values := range u.Query() {
switch key { switch key {
case "remarks": case "remarks":
l.Ps = firstValueOf(values) l.Tag = firstValueOf(values)
case "path": case "path":
l.Path = firstValueOf(values) l.TransportPath = firstValueOf(values)
case "tls": case "tls":
l.TLS = firstValueOf(values) l.TLS = firstValueOf(values) == "tls"
case "obfs": case "obfs":
v := firstValueOf(values) v := firstValueOf(values)
switch v { switch v {
case "websocket": case "ws", "websocket":
l.Net = "ws" l.Transport = C.V2RayTransportTypeWebsocket
case "none": case "http":
l.Net = "" l.Transport = C.V2RayTransportTypeHTTP
} }
case "obfsParam": case "obfsParam":
l.Host = firstValueOf(values) l.Host = firstValueOf(values)
default:
return fmt.Errorf("unsupported shadowrocket vmess parameter: %s=%v", key, values)
} }
} }
return nil return nil

View File

@ -0,0 +1,48 @@
package link_test
import (
"net/url"
"testing"
"github.com/sagernet/sing-box/common/link"
C "github.com/sagernet/sing-box/constant"
)
func TestVMessRocket(t *testing.T) {
tests := []struct {
link string
want link.Vmess
}{
{
link: "vmess://YXV0bzp1dWlkQDE5Mi4xNjguMTAwLjE6NDQz/?remarks=remarks&obfs=ws&path=/path&obfsParam=host&tls=tls",
want: link.Vmess{
Tag: "remarks",
Server: "192.168.100.1",
ServerPort: 443,
UUID: "uuid",
AlterID: 0,
Security: "auto",
Host: "host",
Transport: C.V2RayTransportTypeWebsocket,
TransportPath: "/path",
TLS: true,
TLSAllowInsecure: false,
},
},
}
for _, tt := range tests {
u, err := url.Parse(tt.link)
if err != nil {
t.Fatal(err)
}
link := link.VMessRocket{}
err = link.Parse(u)
if err != nil {
t.Error(err)
return
}
if link.Vmess != tt.want {
t.Errorf("want %#v, got %#v", tt.want, link.Vmess)
}
}
}