mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-29 06:31:56 +00:00
MASQUE client: Support WARP (#6878)
https://github.com/XTLS/Xray-core/pull/6844#issuecomment-5859092964 https://github.com/XTLS/Xray-core/pull/6862#issuecomment-5897508127 https://github.com/XTLS/Xray-core/pull/6878#issuecomment-6052448898
This commit is contained in:
1 parent
7da5dae650
commit
9e55a6ed18
14 files changed
+1126
-50
No files matched your search
@@ -1,7 +1,14 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
@@ -67,6 +74,121 @@ func TestMasqueConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasqueWarpConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MasqueConfig)
|
||||
}
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkcs8, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sec1, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
quote := func(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
server, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&server.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKey}))
|
||||
warpInput := func(key string, extra string) string {
|
||||
return `{` + extra + `"warp": {"privateKey": ` + quote(key) + `, "publicKey": ` + quote(publicPEM) + `, "address": ["172.16.0.2", "2606:4700:110:8a36::2/128"]}}`
|
||||
}
|
||||
address := []string{"172.16.0.2/32", "2606:4700:110:8a36::2/128"}
|
||||
|
||||
for _, input := range []string{
|
||||
string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})),
|
||||
string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: sec1})),
|
||||
base64.StdEncoding.EncodeToString(pkcs8),
|
||||
base64.StdEncoding.EncodeToString(sec1),
|
||||
} {
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: warpInput(input, ""),
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Host: "cloudflareaccess.com",
|
||||
Path: "/",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: warpInput(base64.StdEncoding.EncodeToString(sec1), `"host": "example.com", "path": "/warp", `),
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Host: "example.com",
|
||||
Path: "/warp",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p384DER, err := x509.MarshalPKCS8PrivateKey(p384)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ed, err := x509.MarshalPKCS8PrivateKey(ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withAddress := func(address string) string {
|
||||
return `{"warp": {"privateKey": ` + quote(base64.StdEncoding.EncodeToString(pkcs8)) + `, "publicKey": ` + quote(publicPEM) + `, "address": ` + address + `}}`
|
||||
}
|
||||
withPublicKey := func(key string) string {
|
||||
return `{"warp": {"privateKey": ` + quote(base64.StdEncoding.EncodeToString(pkcs8)) + `, "publicKey": ` + quote(key) + `, "address": ["172.16.0.2"]}}`
|
||||
}
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: withPublicKey(base64.StdEncoding.EncodeToString(publicKey)),
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Host: "cloudflareaccess.com",
|
||||
Path: "/",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: []string{"172.16.0.2/32"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
for _, input := range []string{
|
||||
`{"warp": {}}`,
|
||||
withAddress(`[]`),
|
||||
withPublicKey(""),
|
||||
withPublicKey("not a key"),
|
||||
withPublicKey(base64.StdEncoding.EncodeToString([]byte("not a key"))),
|
||||
withPublicKey(base64.StdEncoding.EncodeToString(pkcs8)),
|
||||
withAddress(`["172.16.0"]`),
|
||||
withAddress(`["172.16.0.2", "172.16.0.3"]`),
|
||||
withAddress(`["2606:4700::1", "2606:4700::2/128"]`),
|
||||
warpInput("not a key", ""),
|
||||
warpInput(base64.StdEncoding.EncodeToString([]byte("not a key")), ""),
|
||||
warpInput(base64.StdEncoding.EncodeToString(p384DER), ""),
|
||||
warpInput(base64.StdEncoding.EncodeToString(ed), ""),
|
||||
warpInput(base64.StdEncoding.EncodeToString(pkcs8), `"user": "u", "pass": "p", `),
|
||||
} {
|
||||
if _, err := loadJSON(creator)(input); err == nil {
|
||||
t.Errorf("expected an error for %s", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasqueOutboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(OutboundDetourConfig)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"maps"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -790,16 +795,49 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type MasqueWarpConfig struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Address []string `json:"address"`
|
||||
}
|
||||
|
||||
type MasqueConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
User string `json:"user"`
|
||||
Pass string `json:"pass"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Warp *MasqueWarpConfig `json:"warp"`
|
||||
}
|
||||
|
||||
func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
var warp *masque.Warp
|
||||
host := c.Host
|
||||
path := c.Path
|
||||
if c.Warp != nil {
|
||||
if c.User != "" || c.Pass != "" {
|
||||
return nil, errors.New(`"user" and "pass" can't be used with "warp"`)
|
||||
}
|
||||
key, err := parseWarpPrivateKey(c.Warp.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, errors.New(`invalid "privateKey" in "warp"`).Base(err)
|
||||
}
|
||||
publicKey, err := parseWarpPublicKey(c.Warp.PublicKey)
|
||||
if err != nil {
|
||||
return nil, errors.New(`invalid "publicKey" in "warp"`).Base(err)
|
||||
}
|
||||
address, err := parseWarpAddress(c.Warp.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
warp = &masque.Warp{PrivateKey: key, PublicKey: publicKey, Address: address}
|
||||
if host == "" {
|
||||
host = masque.WarpHost
|
||||
}
|
||||
if path == "" {
|
||||
path = masque.WarpPath
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
path = masque.DefaultPath
|
||||
}
|
||||
@@ -811,9 +849,9 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
if !strings.HasPrefix(path, "/") || strings.ContainsAny(path, "{}") {
|
||||
return nil, errors.New(`invalid "path": `, path, `, only the variables {target} and {ipproto} are supported`)
|
||||
}
|
||||
if c.Host != "" {
|
||||
if u, err := url.Parse("https://" + c.Host); err != nil || u.Host != c.Host {
|
||||
return nil, errors.New(`invalid "host": `, c.Host)
|
||||
if host != "" {
|
||||
if u, err := url.Parse("https://" + host); err != nil || u.Host != host {
|
||||
return nil, errors.New(`invalid "host": `, host)
|
||||
}
|
||||
}
|
||||
for k, v := range c.Headers {
|
||||
@@ -841,12 +879,83 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.User+":"+c.Pass))
|
||||
}
|
||||
return &masque.Config{
|
||||
Host: c.Host,
|
||||
Host: host,
|
||||
Path: path,
|
||||
Headers: headers,
|
||||
Warp: warp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseWarpAddress(list []string) ([]string, error) {
|
||||
if len(list) == 0 {
|
||||
return nil, errors.New(`"address" in "warp" is not set`)
|
||||
}
|
||||
var v4, v6 bool
|
||||
address := make([]string, 0, len(list))
|
||||
for _, s := range list {
|
||||
prefix, err := netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return nil, errors.New(`invalid "address" in "warp": `, s)
|
||||
}
|
||||
prefix = netip.PrefixFrom(addr, addr.BitLen())
|
||||
}
|
||||
if prefix.Addr().Is4() && v4 || prefix.Addr().Is6() && v6 {
|
||||
return nil, errors.New(`"address" in "warp" takes at most one IPv4 and one IPv6 address`)
|
||||
}
|
||||
v4 = v4 || prefix.Addr().Is4()
|
||||
v6 = v6 || prefix.Addr().Is6()
|
||||
address = append(address, prefix.String())
|
||||
}
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func decodeWarpKey(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil, errors.New("empty key")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(s)); block != nil {
|
||||
return block.Bytes, nil
|
||||
}
|
||||
der, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, errors.New("neither PEM nor base64").Base(err)
|
||||
}
|
||||
return der, nil
|
||||
}
|
||||
|
||||
func parseWarpPublicKey(s string) ([]byte, error) {
|
||||
der, err := decodeWarpKey(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := x509.ParsePKIXPublicKey(der); err != nil {
|
||||
return nil, errors.New("not a PKIX public key").Base(err)
|
||||
}
|
||||
return der, nil
|
||||
}
|
||||
|
||||
func parseWarpPrivateKey(s string) ([]byte, error) {
|
||||
der, err := decodeWarpKey(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var key any
|
||||
key, err = x509.ParsePKCS8PrivateKey(der)
|
||||
if err != nil {
|
||||
if key, err = x509.ParseECPrivateKey(der); err != nil {
|
||||
return nil, errors.New("neither a PKCS #8 nor a SEC 1 private key")
|
||||
}
|
||||
}
|
||||
ecKey, ok := key.(*ecdsa.PrivateKey)
|
||||
if !ok || ecKey.Curve != elliptic.P256() {
|
||||
return nil, errors.New("not an ECDSA P-256 key")
|
||||
}
|
||||
return x509.MarshalPKCS8PrivateKey(ecKey)
|
||||
}
|
||||
|
||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
if len(f) > 0 {
|
||||
return filesystem.ReadCert(f)
|
||||
|
||||
Reference in new issue
Block a user