Browse Source

Add custom TLS server support for http based v2ray transports

世界 2 years ago
parent
commit
ed50257735

+ 2 - 3
common/tls/reality_server.go

@@ -8,7 +8,6 @@ import (
 	"encoding/base64"
 	"encoding/hex"
 	"net"
-	"os"
 	"time"
 
 	"github.com/sagernet/reality"
@@ -135,7 +134,7 @@ func (c *RealityServerConfig) Config() (*tls.Config, error) {
 }
 
 func (c *RealityServerConfig) Client(conn net.Conn) (Conn, error) {
-	return nil, os.ErrInvalid
+	return ClientHandshake(context.Background(), conn, c)
 }
 
 func (c *RealityServerConfig) Start() error {
@@ -147,7 +146,7 @@ func (c *RealityServerConfig) Close() error {
 }
 
 func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) {
-	return nil, os.ErrInvalid
+	return ServerHandshake(context.Background(), conn, c)
 }
 
 func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) {

+ 4 - 2
go.mod

@@ -20,11 +20,13 @@ require (
 	github.com/miekg/dns v1.1.50
 	github.com/oschwald/maxminddb-golang v1.10.0
 	github.com/pires/go-proxyproto v0.6.2
+	github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd
+	github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d
 	github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
 	github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
 	github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
-	github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5
-	github.com/sagernet/sing v0.1.8-0.20230228031050-b60f6390dfe8
+	github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1
+	github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c
 	github.com/sagernet/sing-dns v0.1.4
 	github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
 	github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587

+ 8 - 4
go.sum

@@ -115,6 +115,10 @@ github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05
 github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
 github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd h1:nv3WtVfPGX+i2Ip/TR+Yd3LO1xFSpKUgWmYsXxKJ6vM=
+github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd/go.mod h1:geEm+9ZyRMZ8THRH0XSexeStaMDtkFBf4J1nMK92mAY=
+github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d h1:RmBTGU4SvqxX57SDvpQtrkiQDaCnr4J/DMYMrUBL7OQ=
+github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d/go.mod h1:Ag8QdZjLwuy3V2pyOcqlKz4Cdh0wKEOFlYgR3wPUGkI=
 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
@@ -125,12 +129,12 @@ github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6E
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 h1:tztuJB+giOWNRKQEBVY2oI3PsheTooMdh+/yxemYQYY=
 github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32/go.mod h1:QMCkxXAC3CvBgDZVIJp43NWTuwGBScCzMLVLynjERL8=
-github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5 h1:yDic66vLGsY3zqEyOyRj5tyGfHevLeNv/tXjHUWVzkE=
-github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
+github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1 h1:8mSzchN6DkM26JKLalPwj2KLMIsEjzlp/pYgznlKE2Q=
+github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
-github.com/sagernet/sing v0.1.8-0.20230228031050-b60f6390dfe8 h1:ZBb6CW6bFovBoW950v0eiitQKYEkB2GGot8tkVfu0gM=
-github.com/sagernet/sing v0.1.8-0.20230228031050-b60f6390dfe8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
+github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c h1:+YUwfoIkKlMi3Y1QrOy+OlIELC9KWV0+/5F3NX72q8U=
+github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
 github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN+0=
 github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=

+ 4 - 2
test/go.mod

@@ -10,7 +10,7 @@ require (
 	github.com/docker/docker v20.10.18+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid v4.4.0+incompatible
-	github.com/sagernet/sing v0.1.8-0.20230221060643-3401d210384b
+	github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c
 	github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
 	github.com/spyzhov/ajson v0.7.1
 	github.com/stretchr/testify v1.8.1
@@ -63,11 +63,13 @@ require (
 	github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
 	github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
 	github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
+	github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd // indirect
+	github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d // indirect
 	github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 // indirect
 	github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 // indirect
-	github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5 // indirect
+	github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1 // indirect
 	github.com/sagernet/sing-dns v0.1.4 // indirect
 	github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587 // indirect
 	github.com/sagernet/sing-tun v0.1.2-0.20230226091124-0cdb0eed74d9 // indirect

+ 8 - 4
test/go.sum

@@ -130,6 +130,10 @@ github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4
 github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
 github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
 github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
+github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd h1:nv3WtVfPGX+i2Ip/TR+Yd3LO1xFSpKUgWmYsXxKJ6vM=
+github.com/sagernet/badhttp v0.0.0-20230228035330-e77eb9a689fd/go.mod h1:geEm+9ZyRMZ8THRH0XSexeStaMDtkFBf4J1nMK92mAY=
+github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d h1:RmBTGU4SvqxX57SDvpQtrkiQDaCnr4J/DMYMrUBL7OQ=
+github.com/sagernet/badhttp2 v0.0.0-20230228040529-408b0b8e774d/go.mod h1:Ag8QdZjLwuy3V2pyOcqlKz4Cdh0wKEOFlYgR3wPUGkI=
 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
@@ -138,12 +142,12 @@ github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6E
 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 h1:tztuJB+giOWNRKQEBVY2oI3PsheTooMdh+/yxemYQYY=
 github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32/go.mod h1:QMCkxXAC3CvBgDZVIJp43NWTuwGBScCzMLVLynjERL8=
-github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5 h1:yDic66vLGsY3zqEyOyRj5tyGfHevLeNv/tXjHUWVzkE=
-github.com/sagernet/reality v0.0.0-20230226124550-f98d51fa21b5/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
+github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1 h1:8mSzchN6DkM26JKLalPwj2KLMIsEjzlp/pYgznlKE2Q=
+github.com/sagernet/reality v0.0.0-20230228045158-d3e085a8e5d1/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
-github.com/sagernet/sing v0.1.8-0.20230221060643-3401d210384b h1:Ji2AfGlc4j9AitobOx4k3BCj7eS5nSxL1cgaL81zvlo=
-github.com/sagernet/sing v0.1.8-0.20230221060643-3401d210384b/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
+github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c h1:+YUwfoIkKlMi3Y1QrOy+OlIELC9KWV0+/5F3NX72q8U=
+github.com/sagernet/sing v0.1.8-0.20230228034829-bb617490652c/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
 github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN+0=
 github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=

+ 291 - 0
test/reality_test.go

@@ -0,0 +1,291 @@
+package main
+
+import (
+	"net/netip"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/vless"
+)
+
+func TestVLESSVisionReality(t *testing.T) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	userUUID := newUUID()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{
+						{
+							Name: "sekai",
+							UUID: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.InboundRealityOptions{
+							Enabled: true,
+							Handshake: option.InboundRealityHandshakeOptions{
+								ServerOptions: option.ServerOptions{
+									Server:     "google.com",
+									ServerPort: 443,
+								},
+							},
+							ShortID:    []string{"0123456789abcdef"},
+							PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+						},
+					},
+				},
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan",
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Users: []option.TrojanUser{
+						{
+							Name:     "sekai",
+							Password: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan-out",
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: otherPort,
+					},
+					Password: userUUID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					DialerOptions: option.DialerOptions{
+						Detour: "vless-out",
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "vless-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: userUUID.String(),
+					Flow: vless.FlowVision,
+					TLS: &option.OutboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.OutboundRealityOptions{
+							Enabled:   true,
+							ShortID:   "0123456789abcdef",
+							PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+						},
+						UTLS: &option.OutboundUTLSOptions{
+							Enabled: true,
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "trojan-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestVLESSRealityTransport(t *testing.T) {
+	t.Run("grpc", func(t *testing.T) {
+		testVLESSRealityTransport(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeGRPC,
+		})
+	})
+	t.Run("websocket", func(t *testing.T) {
+		testVLESSRealityTransport(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeWebsocket,
+		})
+	})
+	t.Run("h2", func(t *testing.T) {
+		testVLESSRealityTransport(t, &option.V2RayTransportOptions{
+			Type: C.V2RayTransportTypeHTTP,
+		})
+	})
+}
+
+func testVLESSRealityTransport(t *testing.T, transport *option.V2RayTransportOptions) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	userUUID := newUUID()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{
+						{
+							Name: "sekai",
+							UUID: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.InboundRealityOptions{
+							Enabled: true,
+							Handshake: option.InboundRealityHandshakeOptions{
+								ServerOptions: option.ServerOptions{
+									Server:     "google.com",
+									ServerPort: 443,
+								},
+							},
+							ShortID:    []string{"0123456789abcdef"},
+							PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+						},
+					},
+					Transport: transport,
+				},
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan",
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Users: []option.TrojanUser{
+						{
+							Name:     "sekai",
+							Password: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan-out",
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: otherPort,
+					},
+					Password: userUUID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					DialerOptions: option.DialerOptions{
+						Detour: "vless-out",
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "vless-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: userUUID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.OutboundRealityOptions{
+							Enabled:   true,
+							ShortID:   "0123456789abcdef",
+							PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+						},
+						UTLS: &option.OutboundUTLSOptions{
+							Enabled: true,
+						},
+					},
+					Transport: transport,
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "trojan-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}

+ 0 - 131
test/vless_test.go

@@ -395,134 +395,3 @@ func testVLESSSelfTLS(t *testing.T, flow string) {
 	})
 	testSuit(t, clientPort, testPort)
 }
-
-func TestVLESSVisionReality(t *testing.T) {
-	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
-
-	userUUID := newUUID()
-	startInstance(t, option.Options{
-		Inbounds: []option.Inbound{
-			{
-				Type: C.TypeMixed,
-				Tag:  "mixed-in",
-				MixedOptions: option.HTTPMixedInboundOptions{
-					ListenOptions: option.ListenOptions{
-						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
-						ListenPort: clientPort,
-					},
-				},
-			},
-			{
-				Type: C.TypeVLESS,
-				VLESSOptions: option.VLESSInboundOptions{
-					ListenOptions: option.ListenOptions{
-						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
-						ListenPort: serverPort,
-					},
-					Users: []option.VLESSUser{
-						{
-							Name: "sekai",
-							UUID: userUUID.String(),
-						},
-					},
-					TLS: &option.InboundTLSOptions{
-						Enabled:    true,
-						ServerName: "google.com",
-						Reality: &option.InboundRealityOptions{
-							Enabled: true,
-							Handshake: option.InboundRealityHandshakeOptions{
-								ServerOptions: option.ServerOptions{
-									Server:     "google.com",
-									ServerPort: 443,
-								},
-							},
-							ShortID:    []string{"0123456789abcdef"},
-							PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
-						},
-					},
-				},
-			},
-			{
-				Type: C.TypeTrojan,
-				Tag:  "trojan",
-				TrojanOptions: option.TrojanInboundOptions{
-					ListenOptions: option.ListenOptions{
-						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
-						ListenPort: otherPort,
-					},
-					Users: []option.TrojanUser{
-						{
-							Name:     "sekai",
-							Password: userUUID.String(),
-						},
-					},
-					TLS: &option.InboundTLSOptions{
-						Enabled:         true,
-						ServerName:      "example.org",
-						CertificatePath: certPem,
-						KeyPath:         keyPem,
-					},
-				},
-			},
-		},
-		Outbounds: []option.Outbound{
-			{
-				Type: C.TypeDirect,
-			},
-			{
-				Type: C.TypeTrojan,
-				Tag:  "trojan-out",
-				TrojanOptions: option.TrojanOutboundOptions{
-					ServerOptions: option.ServerOptions{
-						Server:     "127.0.0.1",
-						ServerPort: otherPort,
-					},
-					Password: userUUID.String(),
-					TLS: &option.OutboundTLSOptions{
-						Enabled:         true,
-						ServerName:      "example.org",
-						CertificatePath: certPem,
-					},
-					DialerOptions: option.DialerOptions{
-						Detour: "vless-out",
-					},
-				},
-			},
-			{
-				Type: C.TypeVLESS,
-				Tag:  "vless-out",
-				VLESSOptions: option.VLESSOutboundOptions{
-					ServerOptions: option.ServerOptions{
-						Server:     "127.0.0.1",
-						ServerPort: serverPort,
-					},
-					UUID: userUUID.String(),
-					Flow: vless.FlowVision,
-					TLS: &option.OutboundTLSOptions{
-						Enabled:    true,
-						ServerName: "google.com",
-						Reality: &option.OutboundRealityOptions{
-							Enabled:   true,
-							ShortID:   "0123456789abcdef",
-							PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
-						},
-						UTLS: &option.OutboundUTLSOptions{
-							Enabled: true,
-						},
-					},
-				},
-			},
-		},
-		Route: &option.RouteOptions{
-			Rules: []option.Rule{
-				{
-					DefaultOptions: option.DefaultRule{
-						Inbound:  []string{"mixed-in"},
-						Outbound: "trojan-out",
-					},
-				},
-			},
-		},
-	})
-	testSuit(t, clientPort, testPort)
-}

+ 2 - 0
transport/v2raygrpclite/server.go

@@ -1,3 +1,5 @@
+//go:build !go1.20
+
 package v2raygrpclite
 
 import (

+ 122 - 0
transport/v2raygrpclite/server_badhttp.go

@@ -0,0 +1,122 @@
+//go:build go1.20 && !go1.21
+
+package v2raygrpclite
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/sagernet/badhttp"
+	"github.com/sagernet/badhttp2"
+	"github.com/sagernet/badhttp2/h2c"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2rayhttp"
+	"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"
+	sHttp "github.com/sagernet/sing/protocol/http"
+)
+
+var _ adapter.V2RayServerTransport = (*Server)(nil)
+
+type Server struct {
+	handler      adapter.V2RayServerTransportHandler
+	errorHandler E.Handler
+	httpServer   *http.Server
+	h2Server     *http2.Server
+	h2cHandler   http.Handler
+	path         string
+}
+
+func (s *Server) Network() []string {
+	return []string{N.NetworkTCP}
+}
+
+func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
+	server := &Server{
+		handler:  handler,
+		path:     fmt.Sprintf("/%s/Tun", url.QueryEscape(options.ServiceName)),
+		h2Server: new(http2.Server),
+	}
+	server.httpServer = &http.Server{
+		Handler: server,
+	}
+	server.h2cHandler = h2c.NewHandler(server, server.h2Server)
+	if tlsConfig != nil {
+		if len(tlsConfig.NextProtos()) == 0 {
+			tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
+		}
+		server.httpServer.TLSConfig = tlsConfig
+	}
+	return server, nil
+}
+
+func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" {
+		s.h2cHandler.ServeHTTP(writer, request)
+		return
+	}
+	if request.URL.Path != s.path {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
+		return
+	}
+	if request.Method != http.MethodPost {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad method: ", request.Method))
+		return
+	}
+	if ct := request.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/grpc") {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad content type: ", ct))
+		return
+	}
+	writer.Header().Set("Content-Type", "application/grpc")
+	writer.Header().Set("TE", "trailers")
+	writer.WriteHeader(http.StatusOK)
+	var metadata M.Metadata
+	metadata.Source = sHttp.SourceAddress(v2rayhttp.BadRequest(request))
+	conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher)))
+	s.handler.NewConnection(request.Context(), conn, metadata)
+	conn.CloseWrapper()
+}
+
+func (s *Server) fallbackRequest(ctx context.Context, writer http.ResponseWriter, request *http.Request, statusCode int, err error) {
+	conn := v2rayhttp.NewHTTPConn(request.Body, writer)
+	fErr := s.handler.FallbackConnection(ctx, &conn, M.Metadata{})
+	if fErr == nil {
+		return
+	} else if fErr == os.ErrInvalid {
+		fErr = nil
+	}
+	writer.WriteHeader(statusCode)
+	s.handler.NewError(request.Context(), E.Cause(E.Errors(err, E.Cause(fErr, "fallback connection")), "process connection from ", request.RemoteAddr))
+}
+
+func (s *Server) Serve(listener net.Listener) error {
+	fixTLSConfig := s.httpServer.TLSConfig == nil
+	err := http2.ConfigureServer(s.httpServer, s.h2Server)
+	if err != nil {
+		return err
+	}
+	if fixTLSConfig {
+		s.httpServer.TLSConfig = nil
+	}
+	if s.httpServer.TLSConfig == nil {
+		return s.httpServer.Serve(listener)
+	} else {
+		return s.httpServer.ServeTLS(listener, "", "")
+	}
+}
+
+func (s *Server) ServePacket(listener net.PacketConn) error {
+	return os.ErrInvalid
+}
+
+func (s *Server) Close() error {
+	return common.Close(common.PtrOrNil(s.httpServer))
+}

+ 2 - 0
transport/v2rayhttp/server.go

@@ -1,3 +1,5 @@
+//go:build !go1.20
+
 package v2rayhttp
 
 import (

+ 182 - 0
transport/v2rayhttp/server_badhttp.go

@@ -0,0 +1,182 @@
+//go:build go1.20 && !go1.21
+
+package v2rayhttp
+
+import (
+	std_bufio "bufio"
+	"context"
+	"net"
+	stdHTTP "net/http"
+	"os"
+	"strings"
+	"unsafe"
+
+	"github.com/sagernet/badhttp"
+	"github.com/sagernet/badhttp2"
+	"github.com/sagernet/badhttp2/h2c"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"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"
+	sHttp "github.com/sagernet/sing/protocol/http"
+)
+
+var _ adapter.V2RayServerTransport = (*Server)(nil)
+
+type Server struct {
+	ctx        context.Context
+	handler    adapter.V2RayServerTransportHandler
+	httpServer *http.Server
+	h2Server   *http2.Server
+	h2cHandler http.Handler
+	host       []string
+	path       string
+	method     string
+	headers    http.Header
+}
+
+func (s *Server) Network() []string {
+	return []string{N.NetworkTCP}
+}
+
+func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
+	server := &Server{
+		ctx:      ctx,
+		handler:  handler,
+		h2Server: new(http2.Server),
+		host:     options.Host,
+		path:     options.Path,
+		method:   options.Method,
+		headers:  make(http.Header),
+	}
+	if server.method == "" {
+		server.method = "PUT"
+	}
+	if !strings.HasPrefix(server.path, "/") {
+		server.path = "/" + server.path
+	}
+	for key, value := range options.Headers {
+		server.headers.Set(key, value)
+	}
+	server.httpServer = &http.Server{
+		Handler:           server,
+		ReadHeaderTimeout: C.TCPTimeout,
+		MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
+		TLSConfig:         tlsConfig,
+	}
+	server.h2cHandler = h2c.NewHandler(server, server.h2Server)
+	return server, nil
+}
+
+func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" {
+		s.h2cHandler.ServeHTTP(writer, request)
+		return
+	}
+	host := request.Host
+	if len(s.host) > 0 && !common.Contains(s.host, host) {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusBadRequest, E.New("bad host: ", host))
+		return
+	}
+	if !strings.HasPrefix(request.URL.Path, s.path) {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
+		return
+	}
+	if request.Method != s.method {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad method: ", request.Method))
+		return
+	}
+
+	writer.Header().Set("Cache-Control", "no-store")
+
+	for key, values := range s.headers {
+		for _, value := range values {
+			writer.Header().Set(key, value)
+		}
+	}
+
+	writer.WriteHeader(http.StatusOK)
+	writer.(http.Flusher).Flush()
+
+	var metadata M.Metadata
+	metadata.Source = sHttp.SourceAddress(BadRequest(request))
+	if h, ok := writer.(http.Hijacker); ok {
+		conn, _, err := h.Hijack()
+		if err != nil {
+			s.fallbackRequest(request.Context(), writer, request, http.StatusInternalServerError, E.Cause(err, "hijack conn"))
+			return
+		}
+		s.handler.NewConnection(request.Context(), conn, metadata)
+	} else {
+		conn := NewHTTP2Wrapper(&ServerHTTPConn{
+			NewHTTPConn(request.Body, writer),
+			writer.(http.Flusher),
+		})
+		s.handler.NewConnection(request.Context(), conn, metadata)
+		conn.CloseWrapper()
+	}
+}
+
+func (s *Server) fallbackRequest(ctx context.Context, writer http.ResponseWriter, request *http.Request, statusCode int, err error) {
+	conn := NewHTTPConn(request.Body, writer)
+	fErr := s.handler.FallbackConnection(ctx, &conn, M.Metadata{})
+	if fErr == nil {
+		return
+	} else if fErr == os.ErrInvalid {
+		fErr = nil
+	}
+	writer.WriteHeader(statusCode)
+	s.handler.NewError(request.Context(), E.Cause(E.Errors(err, E.Cause(fErr, "fallback connection")), "process connection from ", request.RemoteAddr))
+}
+
+func (s *Server) Serve(listener net.Listener) error {
+	fixTLSConfig := s.httpServer.TLSConfig == nil
+	err := http2.ConfigureServer(s.httpServer, s.h2Server)
+	if err != nil {
+		return err
+	}
+	if fixTLSConfig {
+		s.httpServer.TLSConfig = nil
+	}
+	if s.httpServer.TLSConfig == nil {
+		return s.httpServer.Serve(listener)
+	} else {
+		return s.httpServer.ServeTLS(listener, "", "")
+	}
+}
+
+func (s *Server) ServePacket(listener net.PacketConn) error {
+	return os.ErrInvalid
+}
+
+func (s *Server) Close() error {
+	return common.Close(common.PtrOrNil(s.httpServer))
+}
+
+var (
+	_ stdHTTP.ResponseWriter = (*BadResponseWriter)(nil)
+	_ stdHTTP.Hijacker       = (*BadResponseWriter)(nil)
+)
+
+type BadResponseWriter struct {
+	http.ResponseWriter
+}
+
+func (w *BadResponseWriter) Header() stdHTTP.Header {
+	return stdHTTP.Header(w.ResponseWriter.Header())
+}
+
+func (w *BadResponseWriter) Hijack() (net.Conn, *std_bufio.ReadWriter, error) {
+	if hijacker, loaded := common.Cast[http.Hijacker](w.ResponseWriter); loaded {
+		return hijacker.Hijack()
+	}
+	return nil, nil, os.ErrInvalid
+}
+
+func BadRequest(r *http.Request) *stdHTTP.Request {
+	return (*stdHTTP.Request)(unsafe.Pointer(r))
+}

+ 2 - 0
transport/v2raywebsocket/server.go

@@ -1,3 +1,5 @@
+//go:build !go1.20
+
 package v2raywebsocket
 
 import (

+ 141 - 0
transport/v2raywebsocket/server_badhttp.go

@@ -0,0 +1,141 @@
+//go:build go1.20 && !go1.21
+
+package v2raywebsocket
+
+import (
+	"context"
+	"encoding/base64"
+	"net"
+	stdHTTP "net/http"
+	"os"
+	"strings"
+
+	"github.com/sagernet/badhttp"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2rayhttp"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	sHttp "github.com/sagernet/sing/protocol/http"
+	"github.com/sagernet/websocket"
+)
+
+var _ adapter.V2RayServerTransport = (*Server)(nil)
+
+type Server struct {
+	ctx                 context.Context
+	handler             adapter.V2RayServerTransportHandler
+	httpServer          *http.Server
+	path                string
+	maxEarlyData        uint32
+	earlyDataHeaderName string
+}
+
+func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
+	server := &Server{
+		ctx:                 ctx,
+		handler:             handler,
+		path:                options.Path,
+		maxEarlyData:        options.MaxEarlyData,
+		earlyDataHeaderName: options.EarlyDataHeaderName,
+	}
+	if !strings.HasPrefix(server.path, "/") {
+		server.path = "/" + server.path
+	}
+	server.httpServer = &http.Server{
+		Handler:           server,
+		ReadHeaderTimeout: C.TCPTimeout,
+		MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
+		TLSConfig:         tlsConfig,
+	}
+	return server, nil
+}
+
+var upgrader = websocket.Upgrader{
+	HandshakeTimeout: C.TCPTimeout,
+	CheckOrigin: func(r *stdHTTP.Request) bool {
+		return true
+	},
+}
+
+func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	if s.maxEarlyData == 0 || s.earlyDataHeaderName != "" {
+		if request.URL.Path != s.path {
+			s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
+			return
+		}
+	}
+	var (
+		earlyData []byte
+		err       error
+		conn      net.Conn
+	)
+	if s.earlyDataHeaderName == "" {
+		if strings.HasPrefix(request.URL.RequestURI(), s.path) {
+			earlyDataStr := request.URL.RequestURI()[len(s.path):]
+			earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr)
+		} else {
+			s.fallbackRequest(request.Context(), writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
+			return
+		}
+	} else {
+		earlyDataStr := request.Header.Get(s.earlyDataHeaderName)
+		if earlyDataStr != "" {
+			earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr)
+		}
+	}
+	if err != nil {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusBadRequest, E.Cause(err, "decode early data"))
+		return
+	}
+	wsConn, err := upgrader.Upgrade(&v2rayhttp.BadResponseWriter{ResponseWriter: writer}, v2rayhttp.BadRequest(request), nil)
+	if err != nil {
+		s.fallbackRequest(request.Context(), writer, request, http.StatusBadRequest, E.Cause(err, "upgrade websocket connection"))
+		return
+	}
+	var metadata M.Metadata
+	metadata.Source = sHttp.SourceAddress(v2rayhttp.BadRequest(request))
+	conn = NewServerConn(wsConn, metadata.Source.TCPAddr())
+	if len(earlyData) > 0 {
+		conn = bufio.NewCachedConn(conn, buf.As(earlyData))
+	}
+	s.handler.NewConnection(request.Context(), conn, metadata)
+}
+
+func (s *Server) fallbackRequest(ctx context.Context, writer http.ResponseWriter, request *http.Request, statusCode int, err error) {
+	conn := v2rayhttp.NewHTTPConn(request.Body, writer)
+	fErr := s.handler.FallbackConnection(ctx, &conn, M.Metadata{})
+	if fErr == nil {
+		return
+	} else if fErr == os.ErrInvalid {
+		fErr = nil
+	}
+	writer.WriteHeader(statusCode)
+	s.handler.NewError(request.Context(), E.Cause(E.Errors(err, E.Cause(fErr, "fallback connection")), "process connection from ", request.RemoteAddr))
+}
+
+func (s *Server) Network() []string {
+	return []string{N.NetworkTCP}
+}
+
+func (s *Server) Serve(listener net.Listener) error {
+	if s.httpServer.TLSConfig == nil {
+		return s.httpServer.Serve(listener)
+	} else {
+		return s.httpServer.ServeTLS(listener, "", "")
+	}
+}
+
+func (s *Server) ServePacket(listener net.PacketConn) error {
+	return os.ErrInvalid
+}
+
+func (s *Server) Close() error {
+	return common.Close(common.PtrOrNil(s.httpServer))
+}