Proxy: Add MASQUE inbound (IETF CONNECT-IP server, RFC 9484) (#6844)

Completes https://github.com/XTLS/Xray-core/pull/6807 and https://github.com/XTLS/Xray-core/pull/6810
This commit is contained in:
Cluvex
2026-09-27 19:16:54 +00:00
committed by GitHub
parent 65e853ed84
commit 7a018833ec
22 changed files with 3339 additions and 91 deletions
+66
View File
@@ -2,9 +2,11 @@ package conf
import (
"net/netip"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/masque"
"google.golang.org/protobuf/proto"
)
@@ -35,3 +37,67 @@ func (c *MasqueClientConfig) Build() (proto.Message, error) {
RemoteDns: c.RemoteDNS,
}, nil
}
type MasqueUserConfig struct {
Pass string `json:"pass"`
Level uint32 `json:"level"`
Email string `json:"email"`
}
type MasqueServerConfig struct {
Users []*MasqueUserConfig `json:"users"`
Clients []*MasqueUserConfig `json:"clients"`
Address []string `json:"address"`
MTU uint32 `json:"mtu"`
}
func (c *MasqueServerConfig) Build() (proto.Message, error) {
if c.Clients != nil {
c.Users = c.Clients
}
config := &masque.ServerConfig{
Address: c.Address,
Mtu: c.MTU,
}
emails := make(map[string]bool)
for _, user := range c.Users {
if user.Email == "" {
return nil, errors.New(`MASQUE: "email" is empty`)
}
if strings.Contains(user.Email, ":") {
return nil, errors.New(`MASQUE: invalid "email" `, user.Email)
}
if user.Pass == "" {
return nil, errors.New(`MASQUE: "pass" of `, user.Email, ` is empty`)
}
email := strings.ToLower(user.Email)
if emails[email] {
return nil, errors.New(`MASQUE: duplicate "email" `, user.Email)
}
emails[email] = true
config.Users = append(config.Users, &protocol.User{
Email: user.Email,
Level: user.Level,
Account: serial.ToTypedMessage(&masque.Account{Password: user.Pass}),
})
}
if len(c.Address) == 0 {
return nil, errors.New(`MASQUE: "address" is not set`)
}
var v4, v6 bool
for _, s := range c.Address {
prefix, err := netip.ParsePrefix(s)
if err != nil {
return nil, errors.New(`MASQUE: invalid "address" `, s).Base(err)
}
if prefix.Addr().Is4() && v4 || prefix.Addr().Is6() && v6 {
return nil, errors.New(`MASQUE: "address" takes at most one IPv4 and one IPv6 prefix`)
}
v4 = v4 || prefix.Addr().Is4()
v6 = v6 || prefix.Addr().Is6()
}
if c.MTU != 0 && (c.MTU < 1280 || c.MTU > 65535) {
return nil, errors.New(`MASQUE: "mtu" must be between 1280 and 65535`)
}
return config, nil
}
+103
View File
@@ -4,7 +4,10 @@ import (
"encoding/json"
"testing"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf"
masqueproxy "github.com/xtls/xray-core/proxy/masque"
"github.com/xtls/xray-core/transport/internet/masque"
)
@@ -37,6 +40,14 @@ func TestMasqueConfig(t *testing.T) {
Parser: loadJSON(creator),
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
},
{
Input: `{"user": "u", "pass": "p:q", "headers": {"X-Token": "a"}}`,
Parser: loadJSON(creator),
Output: &masque.Config{
Path: "/.well-known/masque/ip/*/*/",
Headers: map[string]string{"Authorization": "Basic dTpwOnE=", "X-Token": "a"},
},
},
})
for _, input := range []string{
@@ -47,6 +58,8 @@ func TestMasqueConfig(t *testing.T) {
`{"headers": {"Capsule-Protocol": "?0"}}`,
`{"headers": {"X Token": "a"}}`,
`{"headers": {"X-Token": "a\r\nb"}}`,
`{"user": "u:v", "pass": "p"}`,
`{"user": "u", "pass": "p", "headers": {"authorization": "Basic dTpw"}}`,
} {
if _, err := loadJSON(creator)(input); err == nil {
t.Errorf("expected an error for %s", input)
@@ -83,3 +96,93 @@ func TestMasqueOutboundConfig(t *testing.T) {
}
}
}
func TestMasqueServerConfig(t *testing.T) {
creator := func() Buildable {
return new(MasqueServerConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"users": [{"email": "u@example.com", "pass": "p", "level": 1}],
"address": ["10.13.0.1/24", "fd13::1/64"],
"mtu": 1400
}`,
Parser: loadJSON(creator),
Output: &masqueproxy.ServerConfig{
Users: []*protocol.User{{
Email: "u@example.com",
Level: 1,
Account: serial.ToTypedMessage(&masqueproxy.Account{Password: "p"}),
}},
Address: []string{"10.13.0.1/24", "fd13::1/64"},
Mtu: 1400,
},
},
{
Input: `{"clients": [{"email": "u", "pass": "p:q"}], "address": ["10.13.0.1/24"]}`,
Parser: loadJSON(creator),
Output: &masqueproxy.ServerConfig{
Users: []*protocol.User{{
Email: "u",
Account: serial.ToTypedMessage(&masqueproxy.Account{Password: "p:q"}),
}},
Address: []string{"10.13.0.1/24"},
},
},
{
Input: `{"address": ["10.13.0.1/24"]}`,
Parser: loadJSON(creator),
Output: &masqueproxy.ServerConfig{
Address: []string{"10.13.0.1/24"},
},
},
})
for _, input := range []string{
`{"users": [{"email": "u:v", "pass": "p"}], "address": ["10.13.0.1/24"]}`,
`{"users": [{"email": "", "pass": "p"}], "address": ["10.13.0.1/24"]}`,
`{"users": [{"pass": "p"}], "address": ["10.13.0.1/24"]}`,
`{"users": [{"email": "u", "pass": ""}], "address": ["10.13.0.1/24"]}`,
`{"users": [{"email": "u", "pass": "p"}, {"email": "U", "pass": "q"}], "address": ["10.13.0.1/24"]}`,
`{"users": [{"email": "u", "pass": "p"}]}`,
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1"]}`,
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24", "10.14.0.1/24"]}`,
`{"users": [{"email": "u", "pass": "p"}], "address": ["fd13::1/64", "fd14::1/64"]}`,
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24"], "mtu": 1000}`,
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24"], "mtu": 70000}`,
} {
if _, err := loadJSON(creator)(input); err == nil {
t.Errorf("expected an error for %s", input)
}
}
}
func TestMasqueInboundConfig(t *testing.T) {
build := func(s string) error {
c := new(InboundDetourConfig)
if err := json.Unmarshal([]byte(s), c); err != nil {
return err
}
_, err := c.Build()
return err
}
if err := build(`{
"protocol": "masque",
"port": 443,
"settings": {"users": [{"email": "u@example.com", "pass": "p"}], "address": ["10.13.0.1/24"]},
"streamSettings": {"network": "masque", "security": "tls"}
}`); err != nil {
t.Error(err)
}
if err := build(`{
"protocol": "vless",
"port": 443,
"settings": {"users": [{"id": "27848739-7e62-4138-9fd3-098a63964b6b"}], "decryption": "none"},
"streamSettings": {"network": "masque", "security": "tls"}
}`); err == nil {
t.Error("expected an error for the masque transport on a vless inbound")
}
}
+20 -1
View File
@@ -1,7 +1,9 @@
package conf
import (
"encoding/base64"
"encoding/json"
"maps"
"math/big"
"net/url"
"sort"
@@ -791,6 +793,8 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
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"`
}
@@ -819,12 +823,27 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
switch strings.ToLower(k) {
case "host", "capsule-protocol":
return nil, errors.New(`"headers" can't contain "`, k, `"`)
case "authorization":
if c.User != "" || c.Pass != "" {
return nil, errors.New(`"headers" can't contain "`, k, `" when "user" or "pass" is set`)
}
}
}
headers := c.Headers
if c.User != "" || c.Pass != "" {
if strings.Contains(c.User, ":") {
return nil, errors.New(`invalid "user": `, c.User)
}
headers = maps.Clone(c.Headers)
if headers == nil {
headers = make(map[string]string)
}
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.User+":"+c.Pass))
}
return &masque.Config{
Host: c.Host,
Path: path,
Headers: c.Headers,
Headers: headers,
}, nil
}
+4
View File
@@ -33,6 +33,7 @@ var (
"trojan": func() interface{} { return new(TrojanServerConfig) },
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} },
"hysteria": func() interface{} { return new(HysteriaServerConfig) },
"masque": func() interface{} { return new(MasqueServerConfig) },
"tun": func() interface{} { return new(TunConfig) },
}, "protocol", "settings")
@@ -205,6 +206,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
if err != nil {
return nil, errors.New("failed to build inbound handler for protocol ", c.Protocol).Base(err)
}
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" {
return nil, errors.New("the masque transport can only be used by the masque inbound")
}
return &core.InboundHandlerConfig{
Tag: c.Tag,