mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 19:07:58 +03:00
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:
@@ -4,8 +4,10 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
gotls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -30,6 +32,7 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/app/log"
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
"github.com/xtls/xray-core/app/router"
|
||||
"github.com/xtls/xray-core/common"
|
||||
clog "github.com/xtls/xray-core/common/log"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
@@ -38,6 +41,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/dokodemo"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
"github.com/xtls/xray-core/testing/servers/tcp"
|
||||
@@ -57,7 +61,7 @@ var (
|
||||
|
||||
const (
|
||||
masqueEchoPort = 7
|
||||
masqueAuthorization = "Basic dTpw"
|
||||
masqueAuthorization = "Basic dUBleGFtcGxlLmNvbTpw"
|
||||
)
|
||||
|
||||
func startMasqueServer(t *testing.T, h2 bool) (net.Port, [32]byte) {
|
||||
@@ -384,33 +388,65 @@ func TestMasqueHTTP2(t *testing.T) {
|
||||
testMasque(t, true)
|
||||
}
|
||||
|
||||
func testMasque(t *testing.T, h2 bool) {
|
||||
serverPort, certHash := startMasqueServer(t, h2)
|
||||
|
||||
tcpPort := tcp.PickPort()
|
||||
tcp6Port := tcp.PickPort()
|
||||
udpPort := udp.PickPort()
|
||||
dokodemoTo := func(port net.Port, addr netip.Addr, network net.Network) *core.InboundHandlerConfig {
|
||||
return &core.InboundHandlerConfig{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(port)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(net.IPAddress(addr.AsSlice())),
|
||||
RewritePort: masqueEchoPort,
|
||||
AllowedNetworks: []net.Network{network},
|
||||
}),
|
||||
}
|
||||
func masqueDokodemo(port net.Port, addr netip.Addr, network net.Network) *core.InboundHandlerConfig {
|
||||
return &core.InboundHandlerConfig{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(port)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(net.IPAddress(addr.AsSlice())),
|
||||
RewritePort: masqueEchoPort,
|
||||
AllowedNetworks: []net.Network{network},
|
||||
}),
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
}
|
||||
|
||||
func masqueStreamSettings(tlsConfig *tls.Config, config *transmasque.Config) *internet.StreamConfig {
|
||||
return &internet.StreamConfig{
|
||||
ProtocolName: "masque",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "masque",
|
||||
Settings: serial.ToTypedMessage(config),
|
||||
},
|
||||
},
|
||||
SecurityType: serial.GetMessageType(&tls.Config{}),
|
||||
SecuritySettings: []*serial.TypedMessage{serial.ToTypedMessage(tlsConfig)},
|
||||
}
|
||||
}
|
||||
|
||||
func masqueClientTLS(certHash [32]byte, alpn ...string) *tls.Config {
|
||||
return &tls.Config{
|
||||
ServerName: "localhost",
|
||||
PinnedPeerCertSha256: [][]byte{certHash[:]},
|
||||
NextProtocol: alpn,
|
||||
}
|
||||
}
|
||||
|
||||
func masqueOutbound(serverPort net.Port, certHash [32]byte, h2 bool, authorization string) *core.OutboundHandlerConfig {
|
||||
tlsConfig := masqueClientTLS(certHash)
|
||||
if h2 {
|
||||
tlsConfig.NextProtocol = []string{http2.NextProtoTLS}
|
||||
}
|
||||
clientConfig := &core.Config{
|
||||
return &core.OutboundHandlerConfig{
|
||||
ProxySettings: serial.ToTypedMessage(&masque.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: masqueStreamSettings(tlsConfig, &transmasque.Config{
|
||||
Path: transmasque.DefaultPath,
|
||||
Headers: map[string]string{"Authorization": authorization},
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func masqueClientConfig(serverPort net.Port, certHash [32]byte, h2 bool, authorization string, tcpPort, tcp6Port, udpPort net.Port, v4, v6 netip.Addr) *core.Config {
|
||||
return &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
@@ -418,44 +454,17 @@ func testMasque(t *testing.T, h2 bool) {
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
dokodemoTo(tcpPort, masqueServerV4, net.Network_TCP),
|
||||
dokodemoTo(tcp6Port, masqueServerV6, net.Network_TCP),
|
||||
dokodemoTo(udpPort, masqueServerV4, net.Network_UDP),
|
||||
masqueDokodemo(tcpPort, v4, net.Network_TCP),
|
||||
masqueDokodemo(tcp6Port, v6, net.Network_TCP),
|
||||
masqueDokodemo(udpPort, v4, net.Network_UDP),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&masque.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
},
|
||||
}),
|
||||
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
|
||||
StreamSettings: &internet.StreamConfig{
|
||||
ProtocolName: "masque",
|
||||
TransportSettings: []*internet.TransportConfig{
|
||||
{
|
||||
ProtocolName: "masque",
|
||||
Settings: serial.ToTypedMessage(&transmasque.Config{
|
||||
Path: transmasque.DefaultPath,
|
||||
Headers: map[string]string{"Authorization": masqueAuthorization},
|
||||
}),
|
||||
},
|
||||
},
|
||||
SecurityType: serial.GetMessageType(&tls.Config{}),
|
||||
SecuritySettings: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(tlsConfig),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
masqueOutbound(serverPort, certHash, h2, authorization),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
servers, err := InitializeServerConfigs(clientConfig)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
func testMasqueTraffic(t *testing.T, tcpPort, tcp6Port, udpPort net.Port) {
|
||||
var errg errgroup.Group
|
||||
for range 3 {
|
||||
errg.Go(testTCPConn(tcpPort, 1024*1024, time.Second*20))
|
||||
@@ -466,3 +475,215 @@ func testMasque(t *testing.T, h2 bool) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testMasque(t *testing.T, h2 bool) {
|
||||
serverPort, certHash := startMasqueServer(t, h2)
|
||||
|
||||
tcpPort := tcp.PickPort()
|
||||
tcp6Port := tcp.PickPort()
|
||||
udpPort := udp.PickPort()
|
||||
clientConfig := masqueClientConfig(serverPort, certHash, h2, masqueAuthorization, tcpPort, tcp6Port, udpPort, masqueServerV4, masqueServerV6)
|
||||
|
||||
servers, err := InitializeServerConfigs(clientConfig)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
testMasqueTraffic(t, tcpPort, tcp6Port, udpPort)
|
||||
}
|
||||
|
||||
func masqueServerInbound(serverPort net.Port, certificate *tls.Certificate, alpn ...string) *core.InboundHandlerConfig {
|
||||
return &core.InboundHandlerConfig{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
StreamSettings: masqueStreamSettings(&tls.Config{
|
||||
Certificate: []*tls.Certificate{certificate},
|
||||
NextProtocol: alpn,
|
||||
}, &transmasque.Config{Path: transmasque.DefaultPath}),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&masque.ServerConfig{
|
||||
Users: []*protocol.User{{
|
||||
Email: "u@example.com",
|
||||
Account: serial.ToTypedMessage(&masque.Account{Password: "p"}),
|
||||
}},
|
||||
Address: []string{"10.14.0.1/24", "fd14::1/64"},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func masqueServerConfig(serverPort net.Port, certificate *tls.Certificate, h2 bool, tcpDest, udpDest net.Destination) *core.Config {
|
||||
var alpn []string
|
||||
if h2 {
|
||||
alpn = []string{http2.NextProtoTLS}
|
||||
}
|
||||
redirect := func(tag string, dest net.Destination) *core.OutboundHandlerConfig {
|
||||
return &core.OutboundHandlerConfig{
|
||||
Tag: tag,
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
DestinationOverride: &freedom.DestinationOverride{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(dest.Address),
|
||||
Port: uint32(dest.Port),
|
||||
},
|
||||
},
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
}
|
||||
}
|
||||
return &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
serial.ToTypedMessage(&router.Config{
|
||||
Rule: []*router.RoutingRule{
|
||||
{Networks: []net.Network{net.Network_TCP}, TargetTag: &router.RoutingRule_Tag{Tag: "tcp"}},
|
||||
{Networks: []net.Network{net.Network_UDP}, TargetTag: &router.RoutingRule_Tag{Tag: "udp"}},
|
||||
},
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
masqueServerInbound(serverPort, certificate, alpn...),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
redirect("tcp", tcpDest),
|
||||
redirect("udp", udpDest),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testMasqueServer(t *testing.T, h2 bool, authorization string) error {
|
||||
tcpServer := tcp.Server{MsgProcessor: xor}
|
||||
tcpDest, err := tcpServer.Start()
|
||||
common.Must(err)
|
||||
defer tcpServer.Close()
|
||||
udpServer := udp.Server{MsgProcessor: xor}
|
||||
udpDest, err := udpServer.Start()
|
||||
common.Must(err)
|
||||
defer udpServer.Close()
|
||||
|
||||
ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
||||
serverPort := udp.PickPort()
|
||||
if h2 {
|
||||
serverPort = tcp.PickPort()
|
||||
}
|
||||
tcpPort := tcp.PickPort()
|
||||
tcp6Port := tcp.PickPort()
|
||||
udpPort := udp.PickPort()
|
||||
servers, err := InitializeServerConfigs(
|
||||
masqueServerConfig(serverPort, tls.ParseCertificate(ct), h2, tcpDest, udpDest),
|
||||
masqueClientConfig(serverPort, ctHash, h2, authorization, tcpPort, tcp6Port, udpPort, netip.MustParseAddr("192.0.2.1"), netip.MustParseAddr("2001:db8::1")),
|
||||
)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
if authorization != masqueAuthorization {
|
||||
return testTCPConn(tcpPort, 1024, time.Second*5)()
|
||||
}
|
||||
testMasqueTraffic(t, tcpPort, tcp6Port, udpPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMasqueServer(t *testing.T) {
|
||||
testMasqueServer(t, false, masqueAuthorization)
|
||||
}
|
||||
|
||||
func TestMasqueServerHTTP2(t *testing.T) {
|
||||
testMasqueServer(t, true, masqueAuthorization)
|
||||
}
|
||||
|
||||
func TestMasqueServerRejectsWrongPassword(t *testing.T) {
|
||||
for _, h2 := range []bool{false, true} {
|
||||
if err := testMasqueServer(t, h2, "Basic dUBleGFtcGxlLmNvbTp3cm9uZw=="); err == nil {
|
||||
t.Errorf("a wrong password got through (h2: %v)", h2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func masqueIPPacket(src, dst netip.Addr, payload []byte) []byte {
|
||||
if src.Is4() {
|
||||
p := make([]byte, 20, 20+len(payload))
|
||||
p[0] = 0x45
|
||||
binary.BigEndian.PutUint16(p[2:], uint16(20+len(payload)))
|
||||
p[8] = 64
|
||||
p[9] = 253
|
||||
copy(p[12:], src.AsSlice())
|
||||
copy(p[16:], dst.AsSlice())
|
||||
return append(p, payload...)
|
||||
}
|
||||
p := make([]byte, 40, 40+len(payload))
|
||||
p[0] = 0x60
|
||||
binary.BigEndian.PutUint16(p[4:], uint16(len(payload)))
|
||||
p[6] = 253
|
||||
p[7] = 64
|
||||
copy(p[8:], src.AsSlice())
|
||||
copy(p[24:], dst.AsSlice())
|
||||
return append(p, payload...)
|
||||
}
|
||||
|
||||
func masqueIPAddrs(p []byte) (src, dst netip.Addr) {
|
||||
if p[0]>>4 == 4 {
|
||||
return netip.AddrFrom4([4]byte(p[12:16])), netip.AddrFrom4([4]byte(p[16:20]))
|
||||
}
|
||||
return netip.AddrFrom16([16]byte(p[8:24])), netip.AddrFrom16([16]byte(p[24:40]))
|
||||
}
|
||||
|
||||
func TestMasqueServerClientToClient(t *testing.T) {
|
||||
ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
||||
serverPort := udp.PickPort()
|
||||
servers, err := InitializeServerConfigs(&core.Config{
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
masqueServerInbound(serverPort, tls.ParseCertificate(ct), http3.NextProtoH3, http2.NextProtoTLS),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
||||
},
|
||||
})
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
dial := func(alpn ...string) *transmasque.Conn {
|
||||
streamSettings, err := internet.ToMemoryStreamConfig(masqueStreamSettings(masqueClientTLS(ctHash, alpn...), &transmasque.Config{
|
||||
Path: transmasque.DefaultPath,
|
||||
Headers: map[string]string{"Authorization": masqueAuthorization},
|
||||
}))
|
||||
common.Must(err)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
conn, err := transmasque.Dial(ctx, net.TCPDestination(net.LocalHostIP, serverPort), streamSettings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return conn.(*transmasque.Conn)
|
||||
}
|
||||
h3 := dial()
|
||||
h2 := dial(http2.NextProtoTLS)
|
||||
|
||||
for _, c := range []struct{ from, to *transmasque.Conn }{{h3, h2}, {h2, h3}} {
|
||||
for i := range c.from.LocalAddrs() {
|
||||
src, dst := c.from.LocalAddrs()[i], c.to.LocalAddrs()[i]
|
||||
payload := make([]byte, 1000)
|
||||
rand.Read(payload)
|
||||
if _, err := c.from.Write(masqueIPPacket(src, dst, payload)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
received := make(chan []byte, 1)
|
||||
go func() {
|
||||
b := make([]byte, 2048)
|
||||
n, _ := c.to.Read(b)
|
||||
received <- b[:n]
|
||||
}()
|
||||
select {
|
||||
case p := <-received:
|
||||
gotSrc, gotDst := masqueIPAddrs(p)
|
||||
if gotSrc != src || gotDst != dst || !bytes.HasSuffix(p, payload) {
|
||||
t.Fatalf("unexpected packet from %s to %s: %x", gotSrc, gotDst, p)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("no packet from %s to %s", src, dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user