瀏覽代碼

Add vless outbound and xudp

世界 3 年之前
父節點
當前提交
38088f28b0

+ 1 - 0
constant/proxy.go

@@ -20,6 +20,7 @@ const (
 	TypeSSH          = "ssh"
 	TypeShadowTLS    = "shadowtls"
 	TypeShadowsocksR = "shadowsocksr"
+	TypeVLESS        = "vless"
 )
 
 const (

+ 5 - 0
option/outbound.go

@@ -22,6 +22,7 @@ type _Outbound struct {
 	SSHOptions          SSHOutboundOptions          `json:"-"`
 	ShadowTLSOptions    ShadowTLSOutboundOptions    `json:"-"`
 	ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
+	VLESSOptions        VLESSOutboundOptions        `json:"-"`
 	SelectorOptions     SelectorOutboundOptions     `json:"-"`
 }
 
@@ -56,6 +57,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
 		v = h.ShadowTLSOptions
 	case C.TypeShadowsocksR:
 		v = h.ShadowsocksROptions
+	case C.TypeVLESS:
+		v = h.VLESSOptions
 	case C.TypeSelector:
 		v = h.SelectorOptions
 	default:
@@ -97,6 +100,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.ShadowTLSOptions
 	case C.TypeShadowsocksR:
 		v = &h.ShadowsocksROptions
+	case C.TypeVLESS:
+		v = &h.VLESSOptions
 	case C.TypeSelector:
 		v = &h.SelectorOptions
 	default:

+ 11 - 0
option/vless.go

@@ -0,0 +1,11 @@
+package option
+
+type VLESSOutboundOptions struct {
+	DialerOptions
+	ServerOptions
+	UUID           string                 `json:"uuid"`
+	Network        NetworkList            `json:"network,omitempty"`
+	TLS            *OutboundTLSOptions    `json:"tls,omitempty"`
+	Transport      *V2RayTransportOptions `json:"transport,omitempty"`
+	PacketEncoding string                 `json:"packet_encoding,omitempty"`
+}

+ 2 - 0
outbound/builder.go

@@ -43,6 +43,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
 	case C.TypeShadowsocksR:
 		return NewShadowsocksR(ctx, router, logger, options.Tag, options.ShadowsocksROptions)
+	case C.TypeVLESS:
+		return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
 	case C.TypeSelector:
 		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 	default:

+ 146 - 0
outbound/vless.go

@@ -0,0 +1,146 @@
+package outbound
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/common/tls"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2ray"
+	"github.com/sagernet/sing-box/transport/vless"
+	"github.com/sagernet/sing-vmess/packetaddr"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Outbound = (*VLESS)(nil)
+
+type VLESS struct {
+	myOutboundAdapter
+	dialer     N.Dialer
+	client     *vless.Client
+	serverAddr M.Socksaddr
+	tlsConfig  tls.Config
+	transport  adapter.V2RayClientTransport
+	packetAddr bool
+	xudp       bool
+}
+
+func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) {
+	outbound := &VLESS{
+		myOutboundAdapter: myOutboundAdapter{
+			protocol: C.TypeVLESS,
+			network:  options.Network.Build(),
+			router:   router,
+			logger:   logger,
+			tag:      tag,
+		},
+		dialer:     dialer.New(router, options.DialerOptions),
+		serverAddr: options.ServerOptions.Build(),
+	}
+	var err error
+	if options.TLS != nil {
+		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
+		if err != nil {
+			return nil, err
+		}
+	}
+	if options.Transport != nil {
+		outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
+		if err != nil {
+			return nil, E.Cause(err, "create client transport: ", options.Transport.Type)
+		}
+	}
+	switch options.PacketEncoding {
+	case "":
+	case "packetaddr":
+		outbound.packetAddr = true
+	case "xudp":
+		outbound.xudp = true
+	default:
+		return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
+	}
+	outbound.client, err = vless.NewClient(options.UUID)
+	if err != nil {
+		return nil, err
+	}
+	return outbound, nil
+}
+
+func (h *VLESS) Close() error {
+	return common.Close(h.transport)
+}
+
+func (h *VLESS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	ctx, metadata := adapter.AppendContext(ctx)
+	metadata.Outbound = h.tag
+	metadata.Destination = destination
+	var conn net.Conn
+	var err error
+	if h.transport != nil {
+		conn, err = h.transport.DialContext(ctx)
+	} else {
+		conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
+		if err == nil && h.tlsConfig != nil {
+			conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
+		}
+	}
+	if err != nil {
+		return nil, err
+	}
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+	case N.NetworkUDP:
+	}
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		h.logger.InfoContext(ctx, "outbound connection to ", destination)
+		return h.client.DialEarlyConn(conn, destination), nil
+	case N.NetworkUDP:
+		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
+		return h.client.DialPacketConn(conn, destination), nil
+	default:
+		return nil, E.Extend(N.ErrUnknownNetwork, network)
+	}
+}
+
+func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
+	ctx, metadata := adapter.AppendContext(ctx)
+	metadata.Outbound = h.tag
+	metadata.Destination = destination
+	var conn net.Conn
+	var err error
+	if h.transport != nil {
+		conn, err = h.transport.DialContext(ctx)
+	} else {
+		conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
+		if err == nil && h.tlsConfig != nil {
+			conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
+		}
+	}
+	if err != nil {
+		return nil, err
+	}
+	if h.xudp {
+		return h.client.DialXUDPPacketConn(conn, destination), nil
+	} else if h.packetAddr {
+		return packetaddr.NewConn(h.client.DialPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination), nil
+	} else {
+		return h.client.DialPacketConn(conn, destination), nil
+	}
+}
+
+func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return NewEarlyConnection(ctx, h, conn, metadata)
+}
+
+func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return NewPacketConnection(ctx, h, conn, metadata)
+}

+ 3 - 1
test/clash_test.go

@@ -38,6 +38,7 @@ const (
 	ImageNginx                 = "nginx:stable"
 	ImageShadowTLS             = "ghcr.io/ihciah/shadow-tls:latest"
 	ImageShadowsocksR          = "teddysun/shadowsocks-r:latest"
+	ImageXRayCore              = "teddysun/xray:latest"
 )
 
 var allImages = []string{
@@ -51,6 +52,7 @@ var allImages = []string{
 	ImageNginx,
 	ImageShadowTLS,
 	ImageShadowsocksR,
+	ImageXRayCore,
 }
 
 var localIP = netip.MustParseAddr("127.0.0.1")
@@ -379,7 +381,7 @@ func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.Pack
 
 	rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)}
 
-	times := 50
+	times := 2
 	chunkSize := int64(1024)
 
 	pingCh, pongCh, test := newLargeDataPair()

+ 25 - 0
test/config/vless-server.json

@@ -0,0 +1,25 @@
+{
+  "log": {
+    "loglevel": "debug"
+  },
+  "inbounds": [
+    {
+      "listen": "0.0.0.0",
+      "port": 1234,
+      "protocol": "vless",
+      "settings": {
+        "decryption": "none",
+        "clients": [
+          {
+            "id": "b831381d-6324-4d53-ad4f-8cda48b30811"
+          }
+        ]
+      }
+    }
+  ],
+  "outbounds": [
+    {
+      "protocol": "freedom"
+    }
+  ]
+}

+ 120 - 0
test/vless_test.go

@@ -0,0 +1,120 @@
+package main
+
+import (
+	"net/netip"
+	"os"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+
+	"github.com/spyzhov/ajson"
+	"github.com/stretchr/testify/require"
+)
+
+func TestVLESS(t *testing.T) {
+	content, err := os.ReadFile("config/vless-server.json")
+	require.NoError(t, err)
+	config, err := ajson.Unmarshal(content)
+	require.NoError(t, err)
+
+	user := newUUID()
+	inbound := config.MustKey("inbounds").MustIndex(0)
+	inbound.MustKey("port").SetNumeric(float64(serverPort))
+	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String())
+
+	content, err = ajson.Marshal(config)
+	require.NoError(t, err)
+
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageV2RayCore,
+		Ports:      []uint16{serverPort, testPort},
+		EntryPoint: "v2ray",
+		Stdin:      content,
+	})
+
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: user.String(),
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestVLESSXRay(t *testing.T) {
+	testVLESSXray(t, "")
+}
+
+func TestVLESSXUDP(t *testing.T) {
+	testVLESSXray(t, "xudp")
+}
+
+func testVLESSXray(t *testing.T, packetEncoding string) {
+	content, err := os.ReadFile("config/vless-server.json")
+	require.NoError(t, err)
+	config, err := ajson.Unmarshal(content)
+	require.NoError(t, err)
+
+	user := newUUID()
+	inbound := config.MustKey("inbounds").MustIndex(0)
+	inbound.MustKey("port").SetNumeric(float64(serverPort))
+	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String())
+
+	content, err = ajson.Marshal(config)
+	require.NoError(t, err)
+
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageXRayCore,
+		Ports:      []uint16{serverPort, testPort},
+		EntryPoint: "xray",
+		Stdin:      content,
+	})
+
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID:           user.String(),
+					PacketEncoding: packetEncoding,
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}

+ 48 - 48
test/vmess_test.go

@@ -13,21 +13,24 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func newUUID() uuid.UUID {
+	user, _ := uuid.DefaultGenerator.NewV4()
+	return user
+}
+
 func _TestVMessAuto(t *testing.T) {
 	security := "auto"
-	user, err := uuid.DefaultGenerator.NewV4()
-	require.NoError(t, err)
 	t.Run("self", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, false)
+		testVMessSelf(t, security, 0, false, false, false)
 	})
 	t.Run("packetaddr", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, true)
+		testVMessSelf(t, security, 0, false, false, true)
 	})
 	t.Run("inbound", func(t *testing.T) {
-		testVMessInboundWithV2Ray(t, security, user, 0, false)
+		testVMessInboundWithV2Ray(t, security, 0, false)
 	})
 	t.Run("outbound", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 0)
+		testVMessOutboundWithV2Ray(t, security, false, false, 0)
 	})
 }
 
@@ -56,102 +59,97 @@ func TestVMess(t *testing.T) {
 }
 
 func testVMess0(t *testing.T, security string) {
-	user, err := uuid.DefaultGenerator.NewV4()
-	require.NoError(t, err)
 	t.Run("self", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, false)
+		testVMessSelf(t, security, 0, false, false, false)
 	})
 	t.Run("self-legacy", func(t *testing.T) {
-		testVMessSelf(t, security, user, 1, false, false, false)
+		testVMessSelf(t, security, 1, false, false, false)
 	})
 	t.Run("packetaddr", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, true)
+		testVMessSelf(t, security, 0, false, false, true)
 	})
 	t.Run("outbound", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 0)
+		testVMessOutboundWithV2Ray(t, security, false, false, 0)
 	})
 	t.Run("outbound-legacy", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 1)
+		testVMessOutboundWithV2Ray(t, security, false, false, 1)
 	})
 }
 
 func testVMess1(t *testing.T, security string) {
-	user, err := uuid.DefaultGenerator.NewV4()
-	require.NoError(t, err)
 	t.Run("self", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, false)
+		testVMessSelf(t, security, 0, false, false, false)
 	})
 	t.Run("self-legacy", func(t *testing.T) {
-		testVMessSelf(t, security, user, 1, false, false, false)
+		testVMessSelf(t, security, 1, false, false, false)
 	})
 	t.Run("packetaddr", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, true)
+		testVMessSelf(t, security, 0, false, false, true)
 	})
 	t.Run("inbound", func(t *testing.T) {
-		testVMessInboundWithV2Ray(t, security, user, 0, false)
+		testVMessInboundWithV2Ray(t, security, 0, false)
 	})
 	t.Run("outbound", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 0)
+		testVMessOutboundWithV2Ray(t, security, false, false, 0)
 	})
 	t.Run("outbound-legacy", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 1)
+		testVMessOutboundWithV2Ray(t, security, false, false, 1)
 	})
 }
 
 func testVMess2(t *testing.T, security string) {
-	user, err := uuid.DefaultGenerator.NewV4()
-	require.NoError(t, err)
 	t.Run("self", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, false)
+		testVMessSelf(t, security, 0, false, false, false)
 	})
 	t.Run("self-padding", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, true, false, false)
+		testVMessSelf(t, security, 0, true, false, false)
 	})
 	t.Run("self-authid", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, true, false)
+		testVMessSelf(t, security, 0, false, true, false)
 	})
 	t.Run("self-padding-authid", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, true, true, false)
+		testVMessSelf(t, security, 0, true, true, false)
 	})
 	t.Run("self-legacy", func(t *testing.T) {
-		testVMessSelf(t, security, user, 1, false, false, false)
+		testVMessSelf(t, security, 1, false, false, false)
 	})
 	t.Run("self-legacy-padding", func(t *testing.T) {
-		testVMessSelf(t, security, user, 1, true, false, false)
+		testVMessSelf(t, security, 1, true, false, false)
 	})
 	t.Run("packetaddr", func(t *testing.T) {
-		testVMessSelf(t, security, user, 0, false, false, true)
+		testVMessSelf(t, security, 0, false, false, true)
 	})
 	t.Run("inbound", func(t *testing.T) {
-		testVMessInboundWithV2Ray(t, security, user, 0, false)
+		testVMessInboundWithV2Ray(t, security, 0, false)
 	})
 	t.Run("inbound-authid", func(t *testing.T) {
-		testVMessInboundWithV2Ray(t, security, user, 0, true)
+		testVMessInboundWithV2Ray(t, security, 0, true)
 	})
 	t.Run("inbound-legacy", func(t *testing.T) {
-		testVMessInboundWithV2Ray(t, security, user, 64, false)
+		testVMessInboundWithV2Ray(t, security, 64, false)
 	})
 	t.Run("outbound", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 0)
+		testVMessOutboundWithV2Ray(t, security, false, false, 0)
 	})
 	t.Run("outbound-padding", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, true, false, 0)
+		testVMessOutboundWithV2Ray(t, security, true, false, 0)
 	})
 	t.Run("outbound-authid", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, true, 0)
+		testVMessOutboundWithV2Ray(t, security, false, true, 0)
 	})
 	t.Run("outbound-padding-authid", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, true, true, 0)
+		testVMessOutboundWithV2Ray(t, security, true, true, 0)
 	})
 	t.Run("outbound-legacy", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, false, false, 1)
+		testVMessOutboundWithV2Ray(t, security, false, false, 1)
 	})
 	t.Run("outbound-legacy-padding", func(t *testing.T) {
-		testVMessOutboundWithV2Ray(t, security, user, true, false, 1)
+		testVMessOutboundWithV2Ray(t, security, true, false, 1)
 	})
 }
 
-func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, alterId int, authenticatedLength bool) {
+func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authenticatedLength bool) {
+	userId := newUUID()
 	content, err := os.ReadFile("config/vmess-client.json")
 	require.NoError(t, err)
 	config, err := ajson.Unmarshal(content)
@@ -161,7 +159,7 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, al
 	outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0)
 	outbound.MustKey("port").SetNumeric(float64(serverPort))
 	user := outbound.MustKey("users").MustIndex(0)
-	user.MustKey("id").SetString(uuid.String())
+	user.MustKey("id").SetString(userId.String())
 	user.MustKey("alterId").SetNumeric(float64(alterId))
 	user.MustKey("security").SetString(security)
 	var experiments string
@@ -193,7 +191,7 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, al
 					Users: []option.VMessUser{
 						{
 							Name:    "sekai",
-							UUID:    uuid.String(),
+							UUID:    userId.String(),
 							AlterId: alterId,
 						},
 					},
@@ -205,7 +203,8 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, al
 	testSuitSimple(t, clientPort, testPort)
 }
 
-func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, globalPadding bool, authenticatedLength bool, alterId int) {
+func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding bool, authenticatedLength bool, alterId int) {
+	user := newUUID()
 	content, err := os.ReadFile("config/vmess-server.json")
 	require.NoError(t, err)
 	config, err := ajson.Unmarshal(content)
@@ -213,7 +212,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
 
 	inbound := config.MustKey("inbounds").MustIndex(0)
 	inbound.MustKey("port").SetNumeric(float64(serverPort))
-	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(uuid.String())
+	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String())
 	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("alterId").SetNumeric(float64(alterId))
 
 	content, err = ajson.Marshal(config)
@@ -248,7 +247,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
 						ServerPort: serverPort,
 					},
 					Security:            security,
-					UUID:                uuid.String(),
+					UUID:                user.String(),
 					GlobalPadding:       globalPadding,
 					AuthenticatedLength: authenticatedLength,
 					AlterId:             alterId,
@@ -259,7 +258,8 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
 	testSuitSimple(t, clientPort, testPort)
 }
 
-func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) {
+func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) {
+	user := newUUID()
 	startInstance(t, option.Options{
 		Inbounds: []option.Inbound{
 			{
@@ -282,7 +282,7 @@ func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, alterId int, g
 					Users: []option.VMessUser{
 						{
 							Name:    "sekai",
-							UUID:    uuid.String(),
+							UUID:    user.String(),
 							AlterId: alterId,
 						},
 					},
@@ -302,7 +302,7 @@ func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, alterId int, g
 						ServerPort: serverPort,
 					},
 					Security:            security,
-					UUID:                uuid.String(),
+					UUID:                user.String(),
 					AlterId:             alterId,
 					GlobalPadding:       globalPadding,
 					AuthenticatedLength: authenticatedLength,

+ 144 - 0
transport/vless/client.go

@@ -0,0 +1,144 @@
+package vless
+
+import (
+	"encoding/binary"
+	"io"
+	"net"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	M "github.com/sagernet/sing/common/metadata"
+
+	"github.com/gofrs/uuid"
+)
+
+type Client struct {
+	key []byte
+}
+
+func NewClient(userId string) (*Client, error) {
+	user := uuid.FromStringOrNil(userId)
+	if user == uuid.Nil {
+		user = uuid.NewV5(user, userId)
+	}
+	return &Client{key: user.Bytes()}, nil
+}
+
+func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) *Conn {
+	return &Conn{Conn: conn, key: c.key, destination: destination}
+}
+
+func (c *Client) DialPacketConn(conn net.Conn, destination M.Socksaddr) *PacketConn {
+	return &PacketConn{Conn: conn, key: c.key, destination: destination}
+}
+
+func (c *Client) DialXUDPPacketConn(conn net.Conn, destination M.Socksaddr) *XUDPConn {
+	return &XUDPConn{Conn: conn, key: c.key, destination: destination}
+}
+
+type Conn struct {
+	net.Conn
+	key            []byte
+	destination    M.Socksaddr
+	requestWritten bool
+	responseRead   bool
+}
+
+func (c *Conn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = ReadResponse(c.Conn)
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	return c.Conn.Read(b)
+}
+
+func (c *Conn) Write(b []byte) (n int, err error) {
+	if !c.requestWritten {
+		err = WriteRequest(c.Conn, Request{c.key, CommandTCP, c.destination}, b)
+		if err == nil {
+			n = len(b)
+		}
+		c.requestWritten = true
+		return
+	}
+	return c.Conn.Write(b)
+}
+
+func (c *Conn) Upstream() any {
+	return c.Conn
+}
+
+type PacketConn struct {
+	net.Conn
+	key            []byte
+	destination    M.Socksaddr
+	requestWritten bool
+	responseRead   bool
+}
+
+func (c *PacketConn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = ReadResponse(c.Conn)
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	var length uint16
+	err = binary.Read(c.Conn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	if cap(b) < int(length) {
+		return 0, io.ErrShortBuffer
+	}
+	return io.ReadFull(c.Conn, b[:length])
+}
+
+func (c *PacketConn) Write(b []byte) (n int, err error) {
+	if !c.requestWritten {
+		err = WritePacketRequest(c.Conn, Request{c.key, CommandUDP, c.destination}, b)
+		if err == nil {
+			n = len(b)
+		}
+		c.requestWritten = true
+		return
+	}
+	err = binary.Write(c.Conn, binary.BigEndian, uint16(len(b)))
+	if err != nil {
+		return
+	}
+	return c.Conn.Write(b)
+}
+
+func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	defer buffer.Release()
+	dataLen := buffer.Len()
+	binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(dataLen))
+	if !c.requestWritten {
+		err := WritePacketRequest(c.Conn, Request{c.key, CommandUDP, c.destination}, buffer.Bytes())
+		c.requestWritten = true
+		return err
+	}
+	return common.Error(c.Conn.Write(buffer.Bytes()))
+}
+
+func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	n, err = c.Read(p)
+	return
+}
+
+func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	return c.Write(p)
+}
+
+func (c *PacketConn) FrontHeadroom() int {
+	return 2
+}
+
+func (c *PacketConn) Upstream() any {
+	return c.Conn
+}

+ 104 - 0
transport/vless/protocol.go

@@ -0,0 +1,104 @@
+package vless
+
+import (
+	"encoding/binary"
+	"io"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/common/rw"
+)
+
+const (
+	Version    = 0
+	CommandTCP = 1
+	CommandUDP = 2
+	CommandMux = 3
+	NetworkUDP = 2
+)
+
+var AddressSerializer = M.NewSerializer(
+	M.AddressFamilyByte(0x01, M.AddressFamilyIPv4),
+	M.AddressFamilyByte(0x02, M.AddressFamilyFqdn),
+	M.AddressFamilyByte(0x03, M.AddressFamilyIPv6),
+	M.PortThenAddress(),
+)
+
+type Request struct {
+	UUID        []byte
+	Command     byte
+	Destination M.Socksaddr
+}
+
+func WriteRequest(writer io.Writer, request Request, payload []byte) error {
+	var requestLen int
+	requestLen += 1  // version
+	requestLen += 16 // uuid
+	requestLen += 1  // protobuf length
+	requestLen += 1  // command
+	requestLen += AddressSerializer.AddrPortLen(request.Destination)
+	requestLen += len(payload)
+	_buffer := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(Version),
+		common.Error(buffer.Write(request.UUID)),
+		buffer.WriteByte(0),
+		buffer.WriteByte(CommandTCP),
+		AddressSerializer.WriteAddrPort(buffer, request.Destination),
+		common.Error(buffer.Write(payload)),
+	)
+	return common.Error(writer.Write(buffer.Bytes()))
+}
+
+func WritePacketRequest(writer io.Writer, request Request, payload []byte) error {
+	var requestLen int
+	requestLen += 1  // version
+	requestLen += 16 // uuid
+	requestLen += 1  // protobuf length
+	requestLen += 1  // command
+	requestLen += AddressSerializer.AddrPortLen(request.Destination)
+	if len(payload) > 0 {
+		requestLen += 2
+		requestLen += len(payload)
+	}
+	_buffer := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(Version),
+		common.Error(buffer.Write(request.UUID)),
+		buffer.WriteByte(0),
+		buffer.WriteByte(CommandUDP),
+		AddressSerializer.WriteAddrPort(buffer, request.Destination),
+		binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
+		common.Error(buffer.Write(payload)),
+	)
+	return common.Error(writer.Write(buffer.Bytes()))
+}
+
+func ReadResponse(reader io.Reader) error {
+	version, err := rw.ReadByte(reader)
+	if err != nil {
+		return err
+	}
+	if version != Version {
+		return E.New("unknown version: ", version)
+	}
+	protobufLength, err := rw.ReadByte(reader)
+	if err != nil {
+		return err
+	}
+	if protobufLength > 0 {
+		err = rw.SkipN(reader, int(protobufLength))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 174 - 0
transport/vless/xudp.go

@@ -0,0 +1,174 @@
+package vless
+
+import (
+	"encoding/binary"
+	"io"
+	"net"
+	"os"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+type XUDPConn struct {
+	net.Conn
+	key            []byte
+	destination    M.Socksaddr
+	requestWritten bool
+	responseRead   bool
+}
+
+func (c *XUDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	return 0, nil, os.ErrInvalid
+}
+
+func (c *XUDPConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	start := buffer.Start()
+	if !c.responseRead {
+		err = ReadResponse(c.Conn)
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+		buffer.FullReset()
+	}
+	_, err = buffer.ReadFullFrom(c.Conn, 6)
+	if err != nil {
+		return
+	}
+	var length uint16
+	err = binary.Read(buffer, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	header, err := buffer.ReadBytes(4)
+	if err != nil {
+		return
+	}
+	switch header[2] {
+	case 1:
+		// frame new
+		return M.Socksaddr{}, E.New("unexpected frame new")
+	case 2:
+		// frame keep
+		if length != 4 {
+			_, err = buffer.ReadFullFrom(c.Conn, int(length)-2)
+			if err != nil {
+				return
+			}
+			buffer.Advance(1)
+			destination, err = AddressSerializer.ReadAddrPort(buffer)
+			if err != nil {
+				return
+			}
+		} else {
+			_, err = buffer.ReadFullFrom(c.Conn, 2)
+			if err != nil {
+				return
+			}
+			destination = c.destination
+		}
+	case 3:
+		// frame end
+		return M.Socksaddr{}, io.EOF
+	case 4:
+		// frame keep alive
+	default:
+		return M.Socksaddr{}, E.New("unexpected frame: ", buffer.Byte(2))
+	}
+	// option error
+	if header[3]&2 == 2 {
+		return M.Socksaddr{}, E.Cause(net.ErrClosed, "remote closed")
+	}
+	// option data
+	if header[3]&1 != 1 {
+		buffer.Resize(start, 0)
+		return c.ReadPacket(buffer)
+	} else {
+		err = binary.Read(buffer, binary.BigEndian, &length)
+		if err != nil {
+			return
+		}
+		buffer.Resize(start, 0)
+		_, err = buffer.ReadFullFrom(c.Conn, int(length))
+		return
+	}
+}
+
+func (c *XUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	destination := M.SocksaddrFromNet(addr)
+	headerLen := c.frontHeadroom(AddressSerializer.AddrPortLen(destination))
+	buffer := buf.NewSize(headerLen + len(p))
+	buffer.Advance(headerLen)
+	common.Must1(buffer.Write(p))
+	err = c.WritePacket(buffer, destination)
+	if err == nil {
+		n = len(p)
+	}
+	return
+}
+
+func (c *XUDPConn) frontHeadroom(addrLen int) int {
+	if !c.requestWritten {
+		var headerLen int
+		headerLen += 1  // version
+		headerLen += 16 // uuid
+		headerLen += 1  // protobuf length
+		headerLen += 1  // command
+		headerLen += 2  // frame len
+		headerLen += 5  // frame header
+		headerLen += addrLen
+		headerLen += 2 // payload len
+		return headerLen
+	} else {
+		return 7 + addrLen + 2
+	}
+}
+
+func (c *XUDPConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	defer buffer.Release()
+	dataLen := buffer.Len()
+	addrLen := M.SocksaddrSerializer.AddrPortLen(destination)
+	if !c.requestWritten {
+		header := buf.With(buffer.ExtendHeader(c.frontHeadroom(addrLen)))
+		common.Must(
+			header.WriteByte(Version),
+			common.Error(header.Write(c.key)),
+			header.WriteByte(0),
+			header.WriteByte(CommandMux),
+			binary.Write(header, binary.BigEndian, uint16(5+addrLen)),
+			header.WriteByte(0),
+			header.WriteByte(0),
+			header.WriteByte(1), // frame type new
+			header.WriteByte(1), // option data
+			header.WriteByte(NetworkUDP),
+			AddressSerializer.WriteAddrPort(header, destination),
+			binary.Write(header, binary.BigEndian, uint16(dataLen)),
+		)
+		c.requestWritten = true
+	} else {
+		header := buffer.ExtendHeader(c.frontHeadroom(addrLen))
+		binary.BigEndian.PutUint16(header, uint16(5+addrLen))
+		header[2] = 0
+		header[3] = 0
+		header[4] = 2 // frame keep
+		header[5] = 1 // option data
+		header[6] = NetworkUDP
+		err := AddressSerializer.WriteAddrPort(buf.With(header[7:]), destination)
+		if err != nil {
+			return err
+		}
+		binary.BigEndian.PutUint16(header[7+addrLen:], uint16(dataLen))
+	}
+	return common.Error(c.Conn.Write(buffer.Bytes()))
+}
+
+func (c *XUDPConn) FrontHeadroom() int {
+	return c.frontHeadroom(M.MaxSocksaddrLength)
+}
+
+func (c *XUDPConn) Upstream() any {
+	return c.Conn
+}