mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-27 21:46:58 +00:00
Proxy: Add MASQUE outbound & transport (IETF CONNECT-IP, RFC 9484) (#6807)
Closes https://github.com/XTLS/Xray-core/issues/5495#issuecomment-3710683679
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type MasqueClientConfig struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
RemoteDNS []string `json:"remoteDNS"`
|
||||
}
|
||||
|
||||
func (c *MasqueClientConfig) Build() (proto.Message, error) {
|
||||
if c.Address == nil {
|
||||
return nil, errors.New(`MASQUE: "address" is not set`)
|
||||
}
|
||||
if c.Port == 0 {
|
||||
return nil, errors.New(`MASQUE: "port" is not set`)
|
||||
}
|
||||
for _, s := range c.RemoteDNS {
|
||||
if _, err := netip.ParseAddr(s); err != nil {
|
||||
return nil, errors.New(`MASQUE: invalid "remoteDNS" `, s).Base(err)
|
||||
}
|
||||
}
|
||||
return &masque.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: c.Address.Build(),
|
||||
Port: uint32(c.Port),
|
||||
},
|
||||
RemoteDns: c.RemoteDNS,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
)
|
||||
|
||||
func TestMasqueConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MasqueConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/"},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"host": "example.com:8443",
|
||||
"path": "/.well-known/masque/ip/{target}/{ipproto}/",
|
||||
"headers": {"Authorization": "Basic dTpw"}
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Host: "example.com:8443",
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Headers: map[string]string{"Authorization": "Basic dTpw"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{"path": "/masque/ip{?target,ipproto}"}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
|
||||
},
|
||||
})
|
||||
|
||||
for _, input := range []string{
|
||||
`{"path": "/masque/{target}/{ipproto}/{dns}"}`,
|
||||
`{"path": "masque"}`,
|
||||
`{"host": "example.com/path"}`,
|
||||
`{"headers": {"host": "example.com"}}`,
|
||||
`{"headers": {"Capsule-Protocol": "?0"}}`,
|
||||
`{"headers": {"X Token": "a"}}`,
|
||||
`{"headers": {"X-Token": "a\r\nb"}}`,
|
||||
} {
|
||||
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)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := build(`{
|
||||
"protocol": "masque",
|
||||
"settings": {"address": "example.com", "port": 443},
|
||||
"streamSettings": {"network": "masque", "security": "tls"},
|
||||
"mux": {"enabled": false, "concurrency": -1}
|
||||
}`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for _, input := range []string{
|
||||
`{"protocol": "masque", "settings": {"address": "example.com"}, "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1}}`,
|
||||
`{"protocol": "freedom", "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
} {
|
||||
if err := build(input); err == nil {
|
||||
t.Errorf("expected an error for %s", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ func (p TransportProtocol) Build() (string, error) {
|
||||
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
|
||||
case "hysteria":
|
||||
return "hysteria", nil
|
||||
case "masque":
|
||||
return "masque", nil
|
||||
case "xdrive":
|
||||
return "xdrive", nil
|
||||
default:
|
||||
@@ -61,6 +63,7 @@ type StreamConfig struct {
|
||||
WSSettings *WebSocketConfig `json:"wsSettings"`
|
||||
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
|
||||
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
|
||||
MASQUESettings *MasqueConfig `json:"masqueSettings"`
|
||||
XDRIVESettings *XDriveConfig `json:"xdriveSettings"`
|
||||
SocketSettings *SocketConfig `json:"sockopt"`
|
||||
}
|
||||
@@ -195,6 +198,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
Settings: serial.ToTypedMessage(hs),
|
||||
})
|
||||
}
|
||||
if c.MASQUESettings != nil {
|
||||
ms, err := c.MASQUESettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build MASQUE config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "masque",
|
||||
Settings: serial.ToTypedMessage(ms),
|
||||
})
|
||||
}
|
||||
if c.XDRIVESettings != nil {
|
||||
xs, err := c.XDRIVESettings.Build()
|
||||
if err != nil {
|
||||
|
||||
@@ -20,10 +20,12 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria"
|
||||
"github.com/xtls/xray-core/transport/internet/kcp"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/transport/internet/websocket"
|
||||
"github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -786,6 +788,46 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type MasqueConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
path := c.Path
|
||||
if path == "" {
|
||||
path = masque.DefaultPath
|
||||
}
|
||||
path = strings.NewReplacer(
|
||||
"{target}", "*", "{ipproto}", "*",
|
||||
"{?target,ipproto}", "?target=*&ipproto=*", "{?ipproto,target}", "?ipproto=*&target=*",
|
||||
"{&target,ipproto}", "&target=*&ipproto=*", "{&ipproto,target}", "&ipproto=*&target=*",
|
||||
).Replace(path)
|
||||
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)
|
||||
}
|
||||
}
|
||||
for k, v := range c.Headers {
|
||||
if !httpguts.ValidHeaderFieldName(k) || !httpguts.ValidHeaderFieldValue(v) {
|
||||
return nil, errors.New(`invalid header in "headers": `, strconv.Quote(k))
|
||||
}
|
||||
switch strings.ToLower(k) {
|
||||
case "host", "capsule-protocol":
|
||||
return nil, errors.New(`"headers" can't contain "`, k, `"`)
|
||||
}
|
||||
}
|
||||
return &masque.Config{
|
||||
Host: c.Host,
|
||||
Path: path,
|
||||
Headers: c.Headers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
if len(f) > 0 {
|
||||
return filesystem.ReadCert(f)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
@@ -48,6 +49,7 @@ var (
|
||||
"vmess": func() interface{} { return new(VMessOutboundConfig) },
|
||||
"trojan": func() interface{} { return new(TrojanClientConfig) },
|
||||
"hysteria": func() interface{} { return new(HysteriaClientConfig) },
|
||||
"masque": func() interface{} { return new(MasqueClientConfig) },
|
||||
"dns": func() interface{} { return new(DNSOutboundConfig) },
|
||||
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} },
|
||||
}, "protocol", "settings")
|
||||
@@ -338,6 +340,14 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := ts.(*masque.ClientConfig); ok {
|
||||
if ms := senderSettings.MultiplexSettings; ms != nil && ms.Enabled {
|
||||
return nil, errors.New(`masque outbound does not support "mux"`)
|
||||
}
|
||||
} else if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
|
||||
return nil, errors.New("the masque transport can only be used by the masque outbound")
|
||||
}
|
||||
|
||||
if fc, ok := ts.(*freedom.Config); ok {
|
||||
if senderSettings.StreamSettings != nil &&
|
||||
senderSettings.StreamSettings.SocketSettings != nil &&
|
||||
|
||||
Reference in New Issue
Block a user