From 38088f28b0c03ef413efcb145784fbbaede6bec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 12 Sep 2022 21:59:27 +0800 Subject: [PATCH] Add vless outbound and xudp --- constant/proxy.go | 1 + option/outbound.go | 5 + option/vless.go | 11 +++ outbound/builder.go | 2 + outbound/vless.go | 146 ++++++++++++++++++++++++++++ test/clash_test.go | 4 +- test/config/vless-server.json | 25 +++++ test/vless_test.go | 120 +++++++++++++++++++++++ test/vmess_test.go | 96 +++++++++---------- transport/vless/client.go | 144 ++++++++++++++++++++++++++++ transport/vless/protocol.go | 104 ++++++++++++++++++++ transport/vless/xudp.go | 174 ++++++++++++++++++++++++++++++++++ 12 files changed, 783 insertions(+), 49 deletions(-) create mode 100644 option/vless.go create mode 100644 outbound/vless.go create mode 100644 test/config/vless-server.json create mode 100644 test/vless_test.go create mode 100644 transport/vless/client.go create mode 100644 transport/vless/protocol.go create mode 100644 transport/vless/xudp.go diff --git a/constant/proxy.go b/constant/proxy.go index 2b09d8f3..be9df47a 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -20,6 +20,7 @@ const ( TypeSSH = "ssh" TypeShadowTLS = "shadowtls" TypeShadowsocksR = "shadowsocksr" + TypeVLESS = "vless" ) const ( diff --git a/option/outbound.go b/option/outbound.go index e7a4727d..c8793ca2 100644 --- a/option/outbound.go +++ b/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: diff --git a/option/vless.go b/option/vless.go new file mode 100644 index 00000000..9c6cf8f6 --- /dev/null +++ b/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"` +} diff --git a/outbound/builder.go b/outbound/builder.go index 7206546a..3e49c1a9 100644 --- a/outbound/builder.go +++ b/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: diff --git a/outbound/vless.go b/outbound/vless.go new file mode 100644 index 00000000..389df1fe --- /dev/null +++ b/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) +} diff --git a/test/clash_test.go b/test/clash_test.go index fa258681..ae9c321e 100644 --- a/test/clash_test.go +++ b/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() diff --git a/test/config/vless-server.json b/test/config/vless-server.json new file mode 100644 index 00000000..b5a7bfd0 --- /dev/null +++ b/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" + } + ] +} \ No newline at end of file diff --git a/test/vless_test.go b/test/vless_test.go new file mode 100644 index 00000000..0a128c5f --- /dev/null +++ b/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) +} diff --git a/test/vmess_test.go b/test/vmess_test.go index dd89b2ab..513977eb 100644 --- a/test/vmess_test.go +++ b/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, diff --git a/transport/vless/client.go b/transport/vless/client.go new file mode 100644 index 00000000..825fd6e7 --- /dev/null +++ b/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 +} diff --git a/transport/vless/protocol.go b/transport/vless/protocol.go new file mode 100644 index 00000000..357b672b --- /dev/null +++ b/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 +} diff --git a/transport/vless/xudp.go b/transport/vless/xudp.go new file mode 100644 index 00000000..3ded8319 --- /dev/null +++ b/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 +}