This commit is contained in:
Fangliding
2026-04-01 00:09:59 +08:00
parent d2758a023c
commit b53ae0a183
6 changed files with 186 additions and 33 deletions

View File

@@ -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),

View File

@@ -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)
}
}

View File

@@ -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" +

View File

@@ -50,4 +50,5 @@ message Config {
string uplinkDataKey = 25;
RangeConfig uplinkChunkSize = 26;
int32 serverMaxHeaderBytes = 27;
bool allow_h2c = 28;
}

View File

@@ -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

View File

@@ -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) {