Skip to content

Commit ba8089a

Browse files
yuhan6665dragonbreath2000
authored andcommitted
Transport: Add HTTP3 to HTTP (XTLS#3819)
1 parent eda761b commit ba8089a

5 files changed

Lines changed: 316 additions & 128 deletions

File tree

infra/conf/transport_internet.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ func (p TransportProtocol) Build() (string, error) {
661661
return "mkcp", nil
662662
case "ws", "websocket":
663663
return "websocket", nil
664-
case "h2", "http":
664+
case "h2", "h3", "http":
665665
return "http", nil
666666
case "grpc", "gun":
667667
return "grpc", nil

transport/internet/http/dialer.go

Lines changed: 119 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"sync"
1010
"time"
1111

12+
"github.com/quic-go/quic-go"
13+
"github.com/quic-go/quic-go/http3"
1214
"github.com/xtls/xray-core/common"
1315
"github.com/xtls/xray-core/common/buf"
1416
c "github.com/xtls/xray-core/common/ctx"
@@ -24,6 +26,13 @@ import (
2426
"golang.org/x/net/http2"
2527
)
2628

29+
// defines the maximum time an idle TCP session can survive in the tunnel, so
30+
// it should be consistent across HTTP versions and with other transports.
31+
const connIdleTimeout = 300 * time.Second
32+
33+
// consistent with quic-go
34+
const h3KeepalivePeriod = 10 * time.Second
35+
2736
type dialerConf struct {
2837
net.Destination
2938
*internet.MemoryStreamConfig
@@ -48,72 +57,129 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
4857
if tlsConfigs == nil && realityConfigs == nil {
4958
return nil, errors.New("TLS or REALITY must be enabled for http transport.").AtWarning()
5059
}
60+
isH3 := tlsConfigs != nil && (len(tlsConfigs.NextProtocol) == 1 && tlsConfigs.NextProtocol[0] == "h3")
61+
if isH3 {
62+
dest.Network = net.Network_UDP
63+
}
5164
sockopt := streamSettings.SocketSettings
5265

5366
if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found {
5467
return client, nil
5568
}
5669

57-
transport := &http2.Transport{
58-
DialTLSContext: func(hctx context.Context, string, addr string, tlsConfig *gotls.Config) (net.Conn, error) {
59-
rawHost, rawPort, err := net.SplitHostPort(addr)
60-
if err != nil {
61-
return nil, err
62-
}
63-
if len(rawPort) == 0 {
64-
rawPort = "443"
65-
}
66-
port, err := net.PortFromString(rawPort)
67-
if err != nil {
68-
return nil, err
69-
}
70-
address := net.ParseAddress(rawHost)
70+
var transport http.RoundTripper
71+
if isH3 {
72+
quicConfig := &quic.Config{
73+
MaxIdleTimeout: connIdleTimeout,
7174

72-
hctx = c.ContextWithID(hctx, c.IDFromContext(ctx))
73-
hctx = session.ContextWithOutbounds(hctx, session.OutboundsFromContext(ctx))
74-
hctx = session.ContextWithTimeoutOnly(hctx, true)
75+
// these two are defaults of quic-go/http3. the default of quic-go (no
76+
// http3) is different, so it is hardcoded here for clarity.
77+
// https://github.com/quic-go/quic-go/blob/b8ea5c798155950fb5bbfdd06cad1939c9355878/http3/client.go#L36-L39
78+
MaxIncomingStreams: -1,
79+
KeepAlivePeriod: h3KeepalivePeriod,
80+
}
81+
roundTripper := &http3.RoundTripper{
82+
QUICConfig: quicConfig,
83+
TLSClientConfig: tlsConfigs.GetTLSConfig(tls.WithDestination(dest)),
84+
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
85+
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
86+
if err != nil {
87+
return nil, err
88+
}
7589

76-
pconn, err := internet.DialSystem(hctx, net.TCPDestination(address, port), sockopt)
77-
if err != nil {
78-
errors.LogErrorInner(ctx, err, "failed to dial to " + addr)
79-
return nil, err
80-
}
90+
var udpConn net.PacketConn
91+
var udpAddr *net.UDPAddr
8192

82-
if realityConfigs != nil {
83-
return reality.UClient(pconn, realityConfigs, hctx, dest)
84-
}
93+
switch c := conn.(type) {
94+
case *internet.PacketConnWrapper:
95+
var ok bool
96+
udpConn, ok = c.Conn.(*net.UDPConn)
97+
if !ok {
98+
return nil, errors.New("PacketConnWrapper does not contain a UDP connection")
99+
}
100+
udpAddr, err = net.ResolveUDPAddr("udp", c.Dest.String())
101+
if err != nil {
102+
return nil, err
103+
}
104+
case *net.UDPConn:
105+
udpConn = c
106+
udpAddr, err = net.ResolveUDPAddr("udp", c.RemoteAddr().String())
107+
if err != nil {
108+
return nil, err
109+
}
110+
default:
111+
udpConn = &internet.FakePacketConn{c}
112+
udpAddr, err = net.ResolveUDPAddr("udp", c.RemoteAddr().String())
113+
if err != nil {
114+
return nil, err
115+
}
116+
}
85117

86-
var cn tls.Interface
87-
if fingerprint := tls.GetFingerprint(tlsConfigs.Fingerprint); fingerprint != nil {
88-
cn = tls.UClient(pconn, tlsConfig, fingerprint).(*tls.UConn)
89-
} else {
90-
cn = tls.Client(pconn, tlsConfig).(*tls.Conn)
91-
}
92-
if err := cn.HandshakeContext(ctx); err != nil {
93-
errors.LogErrorInner(ctx, err, "failed to dial to " + addr)
94-
return nil, err
95-
}
96-
if !tlsConfig.InsecureSkipVerify {
97-
if err := cn.VerifyHostname(tlsConfig.ServerName); err != nil {
118+
return quic.DialEarly(ctx, udpConn, udpAddr, tlsCfg, cfg)
119+
},
120+
}
121+
transport = roundTripper
122+
} else {
123+
transportH2 := &http2.Transport{
124+
DialTLSContext: func(hctx context.Context, string, addr string, tlsConfig *gotls.Config) (net.Conn, error) {
125+
rawHost, rawPort, err := net.SplitHostPort(addr)
126+
if err != nil {
127+
return nil, err
128+
}
129+
if len(rawPort) == 0 {
130+
rawPort = "443"
131+
}
132+
port, err := net.PortFromString(rawPort)
133+
if err != nil {
134+
return nil, err
135+
}
136+
address := net.ParseAddress(rawHost)
137+
138+
hctx = c.ContextWithID(hctx, c.IDFromContext(ctx))
139+
hctx = session.ContextWithOutbounds(hctx, session.OutboundsFromContext(ctx))
140+
hctx = session.ContextWithTimeoutOnly(hctx, true)
141+
142+
pconn, err := internet.DialSystem(hctx, net.TCPDestination(address, port), sockopt)
143+
if err != nil {
98144
errors.LogErrorInner(ctx, err, "failed to dial to " + addr)
99145
return nil, err
100146
}
101-
}
102-
negotiatedProtocol := cn.NegotiatedProtocol()
103-
if negotiatedProtocol != http2.NextProtoTLS {
104-
return nil, errors.New("http2: unexpected ALPN protocol " + negotiatedProtocol + "; want q" + http2.NextProtoTLS).AtError()
105-
}
106-
return cn, nil
107-
},
108-
}
109-
110-
if tlsConfigs != nil {
111-
transport.TLSClientConfig = tlsConfigs.GetTLSConfig(tls.WithDestination(dest))
112-
}
113-
114-
if httpSettings.IdleTimeout > 0 || httpSettings.HealthCheckTimeout > 0 {
115-
transport.ReadIdleTimeout = time.Second * time.Duration(httpSettings.IdleTimeout)
116-
transport.PingTimeout = time.Second * time.Duration(httpSettings.HealthCheckTimeout)
147+
148+
if realityConfigs != nil {
149+
return reality.UClient(pconn, realityConfigs, hctx, dest)
150+
}
151+
152+
var cn tls.Interface
153+
if fingerprint := tls.GetFingerprint(tlsConfigs.Fingerprint); fingerprint != nil {
154+
cn = tls.UClient(pconn, tlsConfig, fingerprint).(*tls.UConn)
155+
} else {
156+
cn = tls.Client(pconn, tlsConfig).(*tls.Conn)
157+
}
158+
if err := cn.HandshakeContext(ctx); err != nil {
159+
errors.LogErrorInner(ctx, err, "failed to dial to " + addr)
160+
return nil, err
161+
}
162+
if !tlsConfig.InsecureSkipVerify {
163+
if err := cn.VerifyHostname(tlsConfig.ServerName); err != nil {
164+
errors.LogErrorInner(ctx, err, "failed to dial to " + addr)
165+
return nil, err
166+
}
167+
}
168+
negotiatedProtocol := cn.NegotiatedProtocol()
169+
if negotiatedProtocol != http2.NextProtoTLS {
170+
return nil, errors.New("http2: unexpected ALPN protocol " + negotiatedProtocol + "; want q" + http2.NextProtoTLS).AtError()
171+
}
172+
return cn, nil
173+
},
174+
}
175+
if tlsConfigs != nil {
176+
transportH2.TLSClientConfig = tlsConfigs.GetTLSConfig(tls.WithDestination(dest))
177+
}
178+
if httpSettings.IdleTimeout > 0 || httpSettings.HealthCheckTimeout > 0 {
179+
transportH2.ReadIdleTimeout = time.Second * time.Duration(httpSettings.IdleTimeout)
180+
transportH2.PingTimeout = time.Second * time.Duration(httpSettings.HealthCheckTimeout)
181+
}
182+
transport = transportH2
117183
}
118184

119185
client := &http.Client{
@@ -158,9 +224,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
158224
Host: dest.NetAddr(),
159225
Path: httpSettings.getNormalizedPath(),
160226
},
161-
Proto: "HTTP/2",
162-
ProtoMajor: 2,
163-
ProtoMinor: 0,
164227
Header: httpHeaders,
165228
}
166229
// Disable any compression method from server.

transport/internet/http/http_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/xtls/xray-core/common/net"
1313
"github.com/xtls/xray-core/common/protocol/tls/cert"
1414
"github.com/xtls/xray-core/testing/servers/tcp"
15+
"github.com/xtls/xray-core/testing/servers/udp"
1516
"github.com/xtls/xray-core/transport/internet"
1617
. "github.com/xtls/xray-core/transport/internet/http"
1718
"github.com/xtls/xray-core/transport/internet/stat"
@@ -92,3 +93,80 @@ func TestHTTPConnection(t *testing.T) {
9293
t.Error(r)
9394
}
9495
}
96+
97+
func TestH3Connection(t *testing.T) {
98+
port := udp.PickPort()
99+
100+
listener, err := Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{
101+
ProtocolName: "http",
102+
ProtocolSettings: &Config{},
103+
SecurityType: "tls",
104+
SecuritySettings: &tls.Config{
105+
NextProtocol: []string{"h3"},
106+
Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("www.example.com")))},
107+
},
108+
}, func(conn stat.Connection) {
109+
go func() {
110+
defer conn.Close()
111+
112+
b := buf.New()
113+
defer b.Release()
114+
115+
for {
116+
if _, err := b.ReadFrom(conn); err != nil {
117+
return
118+
}
119+
_, err := conn.Write(b.Bytes())
120+
common.Must(err)
121+
}
122+
}()
123+
})
124+
common.Must(err)
125+
126+
defer listener.Close()
127+
128+
time.Sleep(time.Second)
129+
130+
dctx := context.Background()
131+
conn, err := Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{
132+
ProtocolName: "http",
133+
ProtocolSettings: &Config{},
134+
SecurityType: "tls",
135+
SecuritySettings: &tls.Config{
136+
NextProtocol: []string{"h3"},
137+
ServerName: "www.example.com",
138+
AllowInsecure: true,
139+
},
140+
})
141+
common.Must(err)
142+
defer conn.Close()
143+
144+
const N = 1024
145+
b1 := make([]byte, N)
146+
common.Must2(rand.Read(b1))
147+
b2 := buf.New()
148+
149+
nBytes, err := conn.Write(b1)
150+
common.Must(err)
151+
if nBytes != N {
152+
t.Error("write: ", nBytes)
153+
}
154+
155+
b2.Clear()
156+
common.Must2(b2.ReadFullFrom(conn, N))
157+
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
158+
t.Error(r)
159+
}
160+
161+
nBytes, err = conn.Write(b1)
162+
common.Must(err)
163+
if nBytes != N {
164+
t.Error("write: ", nBytes)
165+
}
166+
167+
b2.Clear()
168+
common.Must2(b2.ReadFullFrom(conn, N))
169+
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
170+
t.Error(r)
171+
}
172+
}

0 commit comments

Comments
 (0)