diff --git a/common/link/ng.go b/common/link/ng.go deleted file mode 100644 index 4d36c40f..00000000 --- a/common/link/ng.go +++ /dev/null @@ -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 -} diff --git a/common/link/number.go b/common/link/number.go new file mode 100644 index 00000000..abbd87d7 --- /dev/null +++ b/common/link/number.go @@ -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 +} diff --git a/common/link/vmess.go b/common/link/vmess.go index 823a7436..ca572541 100644 --- a/common/link/vmess.go +++ b/common/link/vmess.go @@ -1,67 +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"` +// Vmess is the base struct of vmess link +type Vmess struct { + Tag string + Server string + ServerPort uint16 + UUID string + AlterID int + Security string + Transport string + TransportPath string + Host string + TLS bool + TLSAllowInsecure bool } // Options implements Link -func (v *vmess) Options() *option.Outbound { +func (v *Vmess) Options() *option.Outbound { out := &option.Outbound{ Type: "vmess", - Tag: v.Ps, + Tag: v.Tag, VMessOptions: option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ - Server: v.Add, - ServerPort: v.Port, + Server: v.Server, + ServerPort: v.ServerPort, }, - UUID: v.ID, - AlterId: v.Aid, - Security: "auto", + UUID: v.UUID, + AlterId: v.AlterID, + Security: v.Security, }, } - 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 { + out.VMessOptions.TLS = &option.OutboundTLSOptions{ + Insecure: v.TLSAllowInsecure, + ServerName: v.Host, } } - if v.TLS == "tls" { - out.VMessOptions.TLS = &option.OutboundTLSOptions{ - Insecure: true, - ServerName: v.Host, + opt := &option.V2RayTransportOptions{ + Type: v.Transport, + } + + 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 diff --git a/common/link/vmess_ng.go b/common/link/vmess_ng.go new file mode 100644 index 00000000..508ae244 --- /dev/null +++ b/common/link/vmess_ng.go @@ -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 +} diff --git a/common/link/vmess_ng_test.go b/common/link/vmess_ng_test.go new file mode 100644 index 00000000..af568207 --- /dev/null +++ b/common/link/vmess_ng_test.go @@ -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) + } + } +} diff --git a/common/link/vmess_quantumult.go b/common/link/vmess_quantumult.go index 3768ed31..bbc0ee38 100644 --- a/common/link/vmess_quantumult.go +++ b/common/link/vmess_quantumult.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" ) @@ -23,7 +24,7 @@ func init() { // VMessQuantumult is the vmess link of Quantumult type VMessQuantumult struct { - vmess + Vmess } // Parse implements Link @@ -31,31 +32,30 @@ func (l *VMessQuantumult) Parse(u *url.URL) error { if u.Scheme != "vmess" { return E.New("not a vmess link") } - b, err := base64Decode(u.Host) + b, err := base64Decode(u.Host + u.Path) if err != nil { return err } info := string(b) - l.Ver = "2" psn := strings.SplitN(info, " = ", 2) if len(psn) != 2 { return fmt.Errorf("part error: %s", info) } - l.Ps = psn[0] + l.Tag = psn[0] params := strings.Split(psn[1], ",") port, err := strconv.ParseUint(params[2], 10, 16) if err != nil { return E.Cause(err, "invalid port") } - l.Add = params[1] - l.Port = uint16(port) - l.ID = strings.Trim(params[4], "\"") - l.Aid = 0 - l.Net = "tcp" - l.Type = "none" + l.Server = params[1] + l.ServerPort = uint16(port) + l.Security = params[3] + l.UUID = strings.Trim(params[4], "\"") + l.AlterID = 0 + l.Transport = "" if len(params) > 4 { for _, pkv := range params[5:] { @@ -63,30 +63,36 @@ func (l *VMessQuantumult) Parse(u *url.URL) error { switch kvp[0] { case "over-tls": if kvp[1] == "true" { - l.TLS = "tls" + l.TLS = true } case "obfs": switch kvp[1] { - case "ws", "http": - l.Net = kvp[1] + case "ws": + l.Transport = C.V2RayTransportTypeWebsocket + case "http": + l.Transport = C.V2RayTransportTypeHTTP default: return fmt.Errorf("unsupported quantumult vmess obfs parameter: %s", kvp[1]) } case "obfs-path": - l.Path = strings.Trim(kvp[1], "\"") + l.TransportPath = 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 != l.Add { - l.Host = host - } + l.Host = hl[5:] break } } - default: - return fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv) + case "certificate": + switch kvp[1] { + case "0": + l.TLSAllowInsecure = true + default: + l.TLSAllowInsecure = false + } + // default: + // return fmt.Errorf("unsupported quantumult vmess parameter: %s", pkv) } } } diff --git a/common/link/vmess_quantumult_test.go b/common/link/vmess_quantumult_test.go new file mode 100644 index 00000000..53d6e933 --- /dev/null +++ b/common/link/vmess_quantumult_test.go @@ -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) + } + } +} diff --git a/common/link/vmess_rocket.go b/common/link/vmess_rocket.go index bbeb2773..ee3a9016 100644 --- a/common/link/vmess_rocket.go +++ b/common/link/vmess_rocket.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" ) @@ -25,7 +26,9 @@ func init() { // VMessRocket is the vmess link of ShadowRocket type VMessRocket struct { - vmess + Vmess + + Ver string } // Parse implements Link @@ -50,35 +53,34 @@ func (l *VMessRocket) Parse(u *url.URL) error { return E.Cause(err, "invalid port") } // mhp[0] is the encryption method - l.Port = uint16(port) + l.ServerPort = uint16(port) idadd := strings.SplitN(mhp[1], "@", 2) if len(idadd) != 2 { return fmt.Errorf("vmess unreconized: id@addr -- %v", idadd) } - l.ID = idadd[0] - l.Add = idadd[1] - l.Aid = 0 + l.UUID = idadd[0] + l.Server = idadd[1] + l.AlterID = 0 + l.Security = "auto" for key, values := range u.Query() { switch key { case "remarks": - l.Ps = firstValueOf(values) + l.Tag = firstValueOf(values) case "path": - l.Path = firstValueOf(values) + l.TransportPath = firstValueOf(values) case "tls": - l.TLS = firstValueOf(values) + l.TLS = firstValueOf(values) == "tls" case "obfs": v := firstValueOf(values) switch v { - case "websocket": - l.Net = "ws" - case "none": - l.Net = "" + case "ws", "websocket": + l.Transport = C.V2RayTransportTypeWebsocket + case "http": + l.Transport = C.V2RayTransportTypeHTTP } case "obfsParam": l.Host = firstValueOf(values) - default: - return fmt.Errorf("unsupported shadowrocket vmess parameter: %s=%v", key, values) } } return nil diff --git a/common/link/vmess_rocket_test.go b/common/link/vmess_rocket_test.go new file mode 100644 index 00000000..cc0ac06a --- /dev/null +++ b/common/link/vmess_rocket_test.go @@ -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) + } + } +}