Skip to content

Commit b914489

Browse files
neildgopherbot
authored andcommitted
internal/http3: refactor in prep for sharing transport/server code
Pull out various elements of the HTTP/3 client that can be reused in the server. Move tests which can apply to client or server connections into conn_test.go. For golang/go#70914 Change-Id: I72b5eab55ba27df980ab2079120613f175b05927 Reviewed-on: https://go-review.googlesource.com/c/net/+/646616 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Auto-Submit: Damien Neil <dneil@google.com>
1 parent ebd23f8 commit b914489

File tree

6 files changed

+358
-308
lines changed

6 files changed

+358
-308
lines changed

internal/http3/conn.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http3
8+
9+
import (
10+
"context"
11+
"io"
12+
"sync"
13+
14+
"golang.org/x/net/quic"
15+
)
16+
17+
type streamHandler interface {
18+
handleControlStream(*stream) error
19+
handlePushStream(*stream) error
20+
handleEncoderStream(*stream) error
21+
handleDecoderStream(*stream) error
22+
handleRequestStream(*stream)
23+
abort(error)
24+
}
25+
26+
type genericConn struct {
27+
mu sync.Mutex
28+
29+
// The peer may create exactly one control, encoder, and decoder stream.
30+
// streamsCreated is a bitset of streams created so far.
31+
// Bits are 1 << streamType.
32+
streamsCreated uint8
33+
}
34+
35+
func (c *genericConn) acceptStreams(qconn *quic.Conn, h streamHandler) {
36+
for {
37+
// Use context.Background: This blocks until a stream is accepted
38+
// or the connection closes.
39+
st, err := qconn.AcceptStream(context.Background())
40+
if err != nil {
41+
return // connection closed
42+
}
43+
if st.IsReadOnly() {
44+
go c.handleUnidirectionalStream(newStream(st), h)
45+
} else {
46+
go h.handleRequestStream(newStream(st))
47+
}
48+
}
49+
}
50+
51+
func (c *genericConn) handleUnidirectionalStream(st *stream, h streamHandler) {
52+
// Unidirectional stream header: One varint with the stream type.
53+
v, err := st.readVarint()
54+
if err != nil {
55+
h.abort(&connectionError{
56+
code: errH3StreamCreationError,
57+
message: "error reading unidirectional stream header",
58+
})
59+
return
60+
}
61+
stype := streamType(v)
62+
if err := c.checkStreamCreation(stype); err != nil {
63+
h.abort(err)
64+
return
65+
}
66+
switch stype {
67+
case streamTypeControl:
68+
err = h.handleControlStream(st)
69+
case streamTypePush:
70+
err = h.handlePushStream(st)
71+
case streamTypeEncoder:
72+
err = h.handleEncoderStream(st)
73+
case streamTypeDecoder:
74+
err = h.handleDecoderStream(st)
75+
default:
76+
// "Recipients of unknown stream types MUST either abort reading
77+
// of the stream or discard incoming data without further processing."
78+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-7
79+
//
80+
// We should send the H3_STREAM_CREATION_ERROR error code,
81+
// but the quic package currently doesn't allow setting error codes
82+
// for STOP_SENDING frames.
83+
// TODO: Should CloseRead take an error code?
84+
st.stream.CloseRead()
85+
err = nil
86+
}
87+
if err == io.EOF {
88+
err = &connectionError{
89+
code: errH3ClosedCriticalStream,
90+
message: streamType(stype).String() + " stream closed",
91+
}
92+
}
93+
if err != nil {
94+
h.abort(err)
95+
}
96+
}
97+
98+
func (c *genericConn) checkStreamCreation(stype streamType) error {
99+
switch stype {
100+
case streamTypeControl, streamTypeEncoder, streamTypeDecoder:
101+
// The peer may create exactly one control, encoder, and decoder stream.
102+
default:
103+
return nil
104+
}
105+
c.mu.Lock()
106+
defer c.mu.Unlock()
107+
bit := uint8(1) << stype
108+
if c.streamsCreated&bit != 0 {
109+
return &connectionError{
110+
code: errH3StreamCreationError,
111+
message: "multiple " + stype.String() + " streams created",
112+
}
113+
}
114+
c.streamsCreated |= bit
115+
return nil
116+
}

internal/http3/conn_test.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24 && goexperiment.synctest
6+
7+
package http3
8+
9+
import (
10+
"testing"
11+
"testing/synctest"
12+
)
13+
14+
// Tests which apply to both client and server connections.
15+
16+
func TestConnCreatesControlStream(t *testing.T) {
17+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
18+
controlStream := tc.wantStream(streamTypeControl)
19+
controlStream.wantFrameHeader(
20+
"server sends SETTINGS frame on control stream",
21+
frameTypeSettings)
22+
controlStream.discardFrame()
23+
})
24+
}
25+
26+
func TestConnUnknownUnidirectionalStream(t *testing.T) {
27+
// "Recipients of unknown stream types MUST either abort reading of the stream
28+
// or discard incoming data without further processing."
29+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-7
30+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
31+
st := tc.newStream(0x21) // reserved stream type
32+
33+
// The endpoint should send a STOP_SENDING for this stream,
34+
// but it should not close the connection.
35+
synctest.Wait()
36+
if _, err := st.Write([]byte("hello")); err == nil {
37+
t.Fatalf("write to send-only stream with an unknown type succeeded; want error")
38+
}
39+
tc.wantNotClosed("after receiving unknown unidirectional stream type")
40+
})
41+
}
42+
43+
func TestConnUnknownSettings(t *testing.T) {
44+
// "An implementation MUST ignore any [settings] parameter with
45+
// an identifier it does not understand."
46+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-9
47+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
48+
controlStream := tc.newStream(streamTypeControl)
49+
controlStream.writeSettings(0x1f+0x21, 0) // reserved settings type
50+
controlStream.Flush()
51+
tc.wantNotClosed("after receiving unknown settings")
52+
})
53+
}
54+
55+
func TestConnInvalidSettings(t *testing.T) {
56+
// "These reserved settings MUST NOT be sent, and their receipt MUST
57+
// be treated as a connection error of type H3_SETTINGS_ERROR."
58+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-5
59+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
60+
controlStream := tc.newStream(streamTypeControl)
61+
controlStream.writeSettings(0x02, 0) // HTTP/2 SETTINGS_ENABLE_PUSH
62+
controlStream.Flush()
63+
tc.wantClosed("invalid setting", errH3SettingsError)
64+
})
65+
}
66+
67+
func TestConnDuplicateStream(t *testing.T) {
68+
for _, stype := range []streamType{
69+
streamTypeControl,
70+
streamTypeEncoder,
71+
streamTypeDecoder,
72+
} {
73+
t.Run(stype.String(), func(t *testing.T) {
74+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
75+
_ = tc.newStream(stype)
76+
tc.wantNotClosed("after creating one " + stype.String() + " stream")
77+
78+
// Opening a second control, encoder, or decoder stream
79+
// is a protocol violation.
80+
_ = tc.newStream(stype)
81+
tc.wantClosed("duplicate stream", errH3StreamCreationError)
82+
})
83+
})
84+
}
85+
}
86+
87+
func TestConnUnknownFrames(t *testing.T) {
88+
for _, stype := range []streamType{
89+
streamTypeControl,
90+
} {
91+
t.Run(stype.String(), func(t *testing.T) {
92+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
93+
st := tc.newStream(stype)
94+
95+
if stype == streamTypeControl {
96+
// First frame on the control stream must be settings.
97+
st.writeVarint(int64(frameTypeSettings))
98+
st.writeVarint(0) // size
99+
}
100+
101+
data := "frame content"
102+
st.writeVarint(0x1f + 0x21) // reserved frame type
103+
st.writeVarint(int64(len(data))) // size
104+
st.Write([]byte(data))
105+
st.Flush()
106+
107+
tc.wantNotClosed("after writing unknown frame")
108+
})
109+
})
110+
}
111+
}
112+
113+
func TestConnInvalidFrames(t *testing.T) {
114+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
115+
control := tc.newStream(streamTypeControl)
116+
117+
// SETTINGS frame.
118+
control.writeVarint(int64(frameTypeSettings))
119+
control.writeVarint(0) // size
120+
121+
// DATA frame (invalid on the control stream).
122+
control.writeVarint(int64(frameTypeData))
123+
control.writeVarint(0) // size
124+
control.Flush()
125+
tc.wantClosed("after writing DATA frame to control stream", errH3FrameUnexpected)
126+
})
127+
}
128+
129+
func TestConnPeerCreatesBadUnidirectionalStream(t *testing.T) {
130+
runConnTest(t, func(t testing.TB, tc *testQUICConn) {
131+
// Create and close a stream without sending the unidirectional stream header.
132+
qs, err := tc.qconn.NewSendOnlyStream(canceledCtx)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
st := newTestQUICStream(tc.t, newStream(qs))
137+
st.stream.stream.Close()
138+
139+
tc.wantClosed("after peer creates and closes uni stream", errH3StreamCreationError)
140+
})
141+
}
142+
143+
func runConnTest(t *testing.T, f func(testing.TB, *testQUICConn)) {
144+
t.Helper()
145+
runSynctestSubtest(t, "client", func(t testing.TB) {
146+
tc := newTestClientConn(t)
147+
f(t, tc.testQUICConn)
148+
})
149+
}

internal/http3/quic.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http3
8+
9+
import (
10+
"crypto/tls"
11+
12+
"golang.org/x/net/quic"
13+
)
14+
15+
func initConfig(config *quic.Config) *quic.Config {
16+
if config == nil {
17+
config = &quic.Config{}
18+
}
19+
20+
// maybeCloneTLSConfig clones the user-provided tls.Config (but only once)
21+
// prior to us modifying it.
22+
needCloneTLSConfig := true
23+
maybeCloneTLSConfig := func() *tls.Config {
24+
if needCloneTLSConfig {
25+
config.TLSConfig = config.TLSConfig.Clone()
26+
needCloneTLSConfig = false
27+
}
28+
return config.TLSConfig
29+
}
30+
31+
if config.TLSConfig == nil {
32+
config.TLSConfig = &tls.Config{}
33+
needCloneTLSConfig = false
34+
}
35+
if config.TLSConfig.MinVersion == 0 {
36+
maybeCloneTLSConfig().MinVersion = tls.VersionTLS13
37+
}
38+
if config.TLSConfig.NextProtos == nil {
39+
maybeCloneTLSConfig().NextProtos = []string{"h3"}
40+
}
41+
return config
42+
}

internal/http3/quic_test.go

+15-16
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,8 @@ func newQUICEndpointPair(t testing.TB) (e1, e2 *quic.Endpoint) {
4848
TLSConfig: testTLSConfig,
4949
}
5050
tn := &testNet{}
51-
pc1 := tn.newPacketConn()
52-
e1, err := quic.NewEndpoint(pc1, config)
53-
if err != nil {
54-
t.Fatal(err)
55-
}
56-
t.Cleanup(func() {
57-
e1.Close(t.Context())
58-
})
59-
pc2 := tn.newPacketConn()
60-
e2, err = quic.NewEndpoint(pc2, config)
61-
if err != nil {
62-
t.Fatal(err)
63-
}
64-
t.Cleanup(func() {
65-
e2.Close(t.Context())
66-
})
51+
e1 = tn.newQUICEndpoint(t, config)
52+
e2 = tn.newQUICEndpoint(t, config)
6753
return e1, e2
6854
}
6955

@@ -121,6 +107,19 @@ func (tn *testNet) newPacketConn() *testPacketConn {
121107
return tc
122108
}
123109

110+
func (tn *testNet) newQUICEndpoint(t testing.TB, config *quic.Config) *quic.Endpoint {
111+
t.Helper()
112+
pc := tn.newPacketConn()
113+
e, err := quic.NewEndpoint(pc, config)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
t.Cleanup(func() {
118+
e.Close(t.Context())
119+
})
120+
return e
121+
}
122+
124123
// connForAddr returns the conn with the given source address.
125124
func (tn *testNet) connForAddr(srcAddr netip.AddrPort) *testPacketConn {
126125
tn.mu.Lock()

0 commit comments

Comments
 (0)