diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 849f0e38ee1d..b37c84f2910f 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -243,6 +243,7 @@ type SplitHTTPConfig struct { ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"` ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"` + AllowH2C bool `json:"allowH2C"` Xmux XmuxConfig `json:"xmux"` DownloadSettings *StreamConfig `json:"downloadSettings"` Extra json.RawMessage `json:"extra"` @@ -426,6 +427,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { ScMaxBufferedPosts: c.ScMaxBufferedPosts, ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, + AllowH2C: c.AllowH2C, Xmux: &splithttp.XmuxConfig{ MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConnections: newRangeConfig(c.Xmux.MaxConnections), diff --git a/testing/scenarios/vless_test.go b/testing/scenarios/vless_test.go index cdc75c59b04d..81e6a0413fef 100644 --- a/testing/scenarios/vless_test.go +++ b/testing/scenarios/vless_test.go @@ -25,6 +25,7 @@ import ( "github.com/xtls/xray-core/testing/servers/tcp" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/splithttp" transtcp "github.com/xtls/xray-core/transport/internet/tcp" "github.com/xtls/xray-core/transport/internet/tls" "golang.org/x/sync/errgroup" @@ -647,3 +648,121 @@ func TestVlessRealityFingerprints(t *testing.T) { } wg.Wait() } + +func TestVlessXHTTPH2C(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + + xhttpSettings := serial.ToTypedMessage(&splithttp.Config{ + Path: "/h2c", + AllowH2C: true, + }) + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "splithttp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "splithttp", + Settings: xhttpSettings, + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "splithttp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "splithttp", + Settings: xhttpSettings, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/transport/internet/splithttp/config.pb.go b/transport/internet/splithttp/config.pb.go index 4e99d8a8c42a..06c5a67c6072 100644 --- a/transport/internet/splithttp/config.pb.go +++ b/transport/internet/splithttp/config.pb.go @@ -187,6 +187,7 @@ type Config struct { UplinkDataKey string `protobuf:"bytes,25,opt,name=uplinkDataKey,proto3" json:"uplinkDataKey,omitempty"` UplinkChunkSize *RangeConfig `protobuf:"bytes,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"` ServerMaxHeaderBytes int32 `protobuf:"varint,27,opt,name=serverMaxHeaderBytes,proto3" json:"serverMaxHeaderBytes,omitempty"` + AllowH2C bool `protobuf:"varint,28,opt,name=allow_h2c,json=allowH2c,proto3" json:"allow_h2c,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -410,6 +411,13 @@ func (x *Config) GetServerMaxHeaderBytes() int32 { return 0 } +func (x *Config) GetAllowH2C() bool { + if x != nil { + return x.AllowH2C + } + return false +} + var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor const file_transport_internet_splithttp_config_proto_rawDesc = "" + @@ -425,7 +433,7 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" + "\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" + "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" + - "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" + + "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xdf\v\n" + "\x06Config\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + @@ -456,7 +464,8 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" + "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" + "\x0fuplinkChunkSize\x18\x1a \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fuplinkChunkSize\x122\n" + - "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x1a:\n" + + "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x12\x1b\n" + + "\tallow_h2c\x18\x1c \x01(\bR\ballowH2c\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" + diff --git a/transport/internet/splithttp/config.proto b/transport/internet/splithttp/config.proto index 4c303d293222..39a398bdbe7b 100644 --- a/transport/internet/splithttp/config.proto +++ b/transport/internet/splithttp/config.proto @@ -50,4 +50,5 @@ message Config { string uplinkDataKey = 25; RangeConfig uplinkChunkSize = 26; int32 serverMaxHeaderBytes = 27; + bool allow_h2c = 28; } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 0c351a5ae76e..0f9246a4cdb5 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -79,11 +79,14 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in return xmuxClient.XmuxConn.(DialerClient), xmuxClient } -func decideHTTPVersion(tlsConfig *tls.Config, realityConfig *reality.Config) string { +func decideHTTPVersion(tlsConfig *tls.Config, realityConfig *reality.Config, allowH2C bool) string { if realityConfig != nil { return "2" } if tlsConfig == nil { + if allowH2C { + return "2" + } return "1.1" } if len(tlsConfig.NextProtocol) != 1 { @@ -101,8 +104,9 @@ func decideHTTPVersion(tlsConfig *tls.Config, realityConfig *reality.Config) str func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStreamConfig) DialerClient { tlsConfig := tls.ConfigFromStreamSettings(streamSettings) realityConfig := reality.ConfigFromStreamSettings(streamSettings) + transportConfig := streamSettings.ProtocolSettings.(*Config) - httpVersion := decideHTTPVersion(tlsConfig, realityConfig) + httpVersion := decideHTTPVersion(tlsConfig, realityConfig, transportConfig.AllowH2C) if httpVersion == "3" { dest.Network = net.Network_UDP // better to keep this line } @@ -113,8 +117,6 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(dest)) } - transportConfig := streamSettings.ProtocolSettings.(*Config) - dialContext := func(ctxInner context.Context) (net.Conn, error) { conn, err := internet.DialSystem(ctxInner, dest, streamSettings.SocketSettings) if err != nil { @@ -312,6 +314,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea }, IdleConnTimeout: net.ConnIdleTimeout, ReadIdleTimeout: keepAlivePeriod, + AllowHTTP: transportConfig.AllowH2C, } } else { httpDialContext := func(ctxInner context.Context, network string, addr string) (net.Conn, error) { @@ -348,13 +351,12 @@ func init() { func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { tlsConfig := tls.ConfigFromStreamSettings(streamSettings) realityConfig := reality.ConfigFromStreamSettings(streamSettings) + transportConfiguration := streamSettings.ProtocolSettings.(*Config) - httpVersion := decideHTTPVersion(tlsConfig, realityConfig) + httpVersion := decideHTTPVersion(tlsConfig, realityConfig, transportConfiguration.AllowH2C) if httpVersion == "3" { dest.Network = net.Network_UDP } - - transportConfiguration := streamSettings.ProtocolSettings.(*Config) var requestURL url.URL if tlsConfig != nil || realityConfig != nil { @@ -413,7 +415,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me dest2 := *memory2.Destination // just panic tlsConfig2 := tls.ConfigFromStreamSettings(memory2) realityConfig2 := reality.ConfigFromStreamSettings(memory2) - httpVersion2 := decideHTTPVersion(tlsConfig2, realityConfig2) + config2 := memory2.ProtocolSettings.(*Config) + httpVersion2 := decideHTTPVersion(tlsConfig2, realityConfig2, config2.AllowH2C) if httpVersion2 == "3" { dest2.Network = net.Network_UDP } @@ -422,7 +425,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me } else { requestURL2.Scheme = "http" } - config2 := memory2.ProtocolSettings.(*Config) requestURL2.Host = config2.Host if requestURL2.Host == "" && tlsConfig2 != nil { requestURL2.Host = tlsConfig2.ServerName diff --git a/transport/internet/splithttp/splithttp_test.go b/transport/internet/splithttp/splithttp_test.go index ab02b61930f8..b4dc20aa57eb 100644 --- a/transport/internet/splithttp/splithttp_test.go +++ b/transport/internet/splithttp/splithttp_test.go @@ -6,7 +6,6 @@ import ( "crypto/rand" "fmt" "io" - "net/http" "runtime" "testing" "time" @@ -186,39 +185,60 @@ func Test_ListenXHAndDial_H2C(t *testing.T) { } listenPort := tcp.PickPort() - - streamSettings := &internet.MemoryStreamConfig{ + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ ProtocolName: "splithttp", ProtocolSettings: &Config{ - Path: "shs", + Path: "/sh", + AllowH2C: true, }, - } - listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { - go func() { - _ = conn.Close() - }() + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) }) common.Must(err) - defer listen.Close() - - protocols := new(http.Protocols) - protocols.SetUnencryptedHTTP2(true) - client := http.Client{ - Transport: &http.Transport{ - Protocols: protocols, - }, + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{Path: "sh", AllowH2C: true}, } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) - resp, err := client.Get("http://" + net.LocalHostIP.String() + ":" + listenPort.String()) + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) common.Must(err) - if resp.StatusCode != 404 { - t.Error("Expected 404 but got:", resp.StatusCode) + var b [1024]byte + fmt.Println("test2") + n, _ := io.ReadFull(conn, b[:]) + fmt.Println("string is", n) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) } - if resp.ProtoMajor != 2 { - t.Error("Expected h2 but got:", resp.ProtoMajor) + common.Must(conn.Close()) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, _ = io.ReadFull(conn, b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) } + common.Must(conn.Close()) + + common.Must(listen.Close()) } func Test_ListenXHAndDial_QUIC(t *testing.T) {