diff --git a/context/context_test.go b/context/context_test.go index e7bf0acc2..2cb54edb8 100644 --- a/context/context_test.go +++ b/context/context_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.7 -// +build !go1.7 package context diff --git a/context/ctxhttp/ctxhttp_test.go b/context/ctxhttp/ctxhttp_test.go index 21f7599cc..d585f117f 100644 --- a/context/ctxhttp/ctxhttp_test.go +++ b/context/ctxhttp/ctxhttp_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !plan9 -// +build !plan9 package ctxhttp diff --git a/context/go17.go b/context/go17.go index 2cb9c408f..0c1b86793 100644 --- a/context/go17.go +++ b/context/go17.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.7 -// +build go1.7 package context diff --git a/context/go19.go b/context/go19.go index 64d31ecc3..e31e35a90 100644 --- a/context/go19.go +++ b/context/go19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package context diff --git a/context/pre_go17.go b/context/pre_go17.go index 7b6b68511..065ff3dfa 100644 --- a/context/pre_go17.go +++ b/context/pre_go17.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.7 -// +build !go1.7 package context diff --git a/context/pre_go19.go b/context/pre_go19.go index 1f9715341..ec5a63803 100644 --- a/context/pre_go19.go +++ b/context/pre_go19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.9 -// +build !go1.9 package context diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index b6b4f9c19..42987ab7c 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -751,6 +751,9 @@ func (p *Parser) AllAnswers() ([]Resource, error) { } // SkipAnswer skips a single Answer Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AnswerHeader] would actually return an error. func (p *Parser) SkipAnswer() error { return p.skipResource(sectionAnswers) } @@ -801,6 +804,9 @@ func (p *Parser) AllAuthorities() ([]Resource, error) { } // SkipAuthority skips a single Authority Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AuthorityHeader] would actually return an error. func (p *Parser) SkipAuthority() error { return p.skipResource(sectionAuthorities) } @@ -851,6 +857,9 @@ func (p *Parser) AllAdditionals() ([]Resource, error) { } // SkipAdditional skips a single Additional Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AdditionalHeader] would actually return an error. func (p *Parser) SkipAdditional() error { return p.skipResource(sectionAdditionals) } diff --git a/go.mod b/go.mod index 38ac82b44..21deffd4b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module golang.org/x/net -go 1.17 +go 1.18 require ( - golang.org/x/crypto v0.14.0 - golang.org/x/sys v0.13.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/crypto v0.15.0 + golang.org/x/sys v0.14.0 + golang.org/x/term v0.14.0 + golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index dc4dc125c..54759e489 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,8 @@ -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/html/atom/gen.go b/html/atom/gen.go index 5b0aaf737..5d85c604d 100644 --- a/html/atom/gen.go +++ b/html/atom/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go //go:generate go run gen.go -test diff --git a/http/httpproxy/go19_test.go b/http/httpproxy/go19_test.go index 5f6e3d7ff..5fca5ac45 100644 --- a/http/httpproxy/go19_test.go +++ b/http/httpproxy/go19_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package httpproxy_test diff --git a/http2/databuffer.go b/http2/databuffer.go index a3067f8de..e6f55cbd1 100644 --- a/http2/databuffer.go +++ b/http2/databuffer.go @@ -20,41 +20,44 @@ import ( // TODO: Benchmark to determine if the pools are necessary. The GC may have // improved enough that we can instead allocate chunks like this: // make([]byte, max(16<<10, expectedBytesRemaining)) -var ( - dataChunkSizeClasses = []int{ - 1 << 10, - 2 << 10, - 4 << 10, - 8 << 10, - 16 << 10, - } - dataChunkPools = [...]sync.Pool{ - {New: func() interface{} { return make([]byte, 1<<10) }}, - {New: func() interface{} { return make([]byte, 2<<10) }}, - {New: func() interface{} { return make([]byte, 4<<10) }}, - {New: func() interface{} { return make([]byte, 8<<10) }}, - {New: func() interface{} { return make([]byte, 16<<10) }}, - } -) +var dataChunkPools = [...]sync.Pool{ + {New: func() interface{} { return new([1 << 10]byte) }}, + {New: func() interface{} { return new([2 << 10]byte) }}, + {New: func() interface{} { return new([4 << 10]byte) }}, + {New: func() interface{} { return new([8 << 10]byte) }}, + {New: func() interface{} { return new([16 << 10]byte) }}, +} func getDataBufferChunk(size int64) []byte { - i := 0 - for ; i < len(dataChunkSizeClasses)-1; i++ { - if size <= int64(dataChunkSizeClasses[i]) { - break - } + switch { + case size <= 1<<10: + return dataChunkPools[0].Get().(*[1 << 10]byte)[:] + case size <= 2<<10: + return dataChunkPools[1].Get().(*[2 << 10]byte)[:] + case size <= 4<<10: + return dataChunkPools[2].Get().(*[4 << 10]byte)[:] + case size <= 8<<10: + return dataChunkPools[3].Get().(*[8 << 10]byte)[:] + default: + return dataChunkPools[4].Get().(*[16 << 10]byte)[:] } - return dataChunkPools[i].Get().([]byte) } func putDataBufferChunk(p []byte) { - for i, n := range dataChunkSizeClasses { - if len(p) == n { - dataChunkPools[i].Put(p) - return - } + switch len(p) { + case 1 << 10: + dataChunkPools[0].Put((*[1 << 10]byte)(p)) + case 2 << 10: + dataChunkPools[1].Put((*[2 << 10]byte)(p)) + case 4 << 10: + dataChunkPools[2].Put((*[4 << 10]byte)(p)) + case 8 << 10: + dataChunkPools[3].Put((*[8 << 10]byte)(p)) + case 16 << 10: + dataChunkPools[4].Put((*[16 << 10]byte)(p)) + default: + panic(fmt.Sprintf("unexpected buffer len=%v", len(p))) } - panic(fmt.Sprintf("unexpected buffer len=%v", len(p))) } // dataBuffer is an io.ReadWriter backed by a list of data chunks. diff --git a/http2/go111.go b/http2/go111.go deleted file mode 100644 index 5bf62b032..000000000 --- a/http2/go111.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.11 -// +build go1.11 - -package http2 - -import ( - "net/http/httptrace" - "net/textproto" -) - -func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { - return trace != nil && trace.WroteHeaderField != nil -} - -func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { - if trace != nil && trace.WroteHeaderField != nil { - trace.WroteHeaderField(k, []string{v}) - } -} - -func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { - if trace != nil { - return trace.Got1xxResponse - } - return nil -} diff --git a/http2/go115.go b/http2/go115.go deleted file mode 100644 index 908af1ab9..000000000 --- a/http2/go115.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.15 -// +build go1.15 - -package http2 - -import ( - "context" - "crypto/tls" -) - -// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS -// connection. -func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { - dialer := &tls.Dialer{ - Config: cfg, - } - cn, err := dialer.DialContext(ctx, network, addr) - if err != nil { - return nil, err - } - tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed - return tlsCn, nil -} diff --git a/http2/go118.go b/http2/go118.go deleted file mode 100644 index aca4b2b31..000000000 --- a/http2/go118.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return tc.NetConn() -} diff --git a/http2/h2i/h2i.go b/http2/h2i/h2i.go index 901f6ca79..ee7020dd9 100644 --- a/http2/h2i/h2i.go +++ b/http2/h2i/h2i.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows /* The h2i command is an interactive HTTP/2 console. diff --git a/http2/hpack/gen.go b/http2/hpack/gen.go index de14ab0ec..21a4198b3 100644 --- a/http2/hpack/gen.go +++ b/http2/hpack/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package main diff --git a/http2/not_go111.go b/http2/not_go111.go deleted file mode 100644 index cc0baa819..000000000 --- a/http2/not_go111.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.11 -// +build !go1.11 - -package http2 - -import ( - "net/http/httptrace" - "net/textproto" -) - -func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { return false } - -func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) {} - -func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { - return nil -} diff --git a/http2/not_go115.go b/http2/not_go115.go deleted file mode 100644 index e6c04cf7a..000000000 --- a/http2/not_go115.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.15 -// +build !go1.15 - -package http2 - -import ( - "context" - "crypto/tls" -) - -// dialTLSWithContext opens a TLS connection. -func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { - cn, err := tls.Dial(network, addr, cfg) - if err != nil { - return nil, err - } - if err := cn.Handshake(); err != nil { - return nil, err - } - if cfg.InsecureSkipVerify { - return cn, nil - } - if err := cn.VerifyHostname(cfg.ServerName); err != nil { - return nil, err - } - return cn, nil -} diff --git a/http2/not_go118.go b/http2/not_go118.go deleted file mode 100644 index eab532c96..000000000 --- a/http2/not_go118.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.18 -// +build !go1.18 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return nil -} diff --git a/http2/server.go b/http2/server.go index 02c88b6b3..ae94c6408 100644 --- a/http2/server.go +++ b/http2/server.go @@ -2549,7 +2549,6 @@ type responseWriterState struct { wroteHeader bool // WriteHeader called (explicitly or implicitly). Not necessarily sent to user yet. sentHeader bool // have we sent the header frame? handlerDone bool // handler has finished - dirty bool // a Write failed; don't reuse this responseWriterState sentContentLen int64 // non-zero if handler set a Content-Length header wroteBytes int64 @@ -2669,7 +2668,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { date: date, }) if err != nil { - rws.dirty = true return 0, err } if endStream { @@ -2690,7 +2688,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { if len(p) > 0 || endStream { // only send a 0 byte DATA frame if we're ending the stream. if err := rws.conn.writeDataFromHandler(rws.stream, p, endStream); err != nil { - rws.dirty = true return 0, err } } @@ -2702,9 +2699,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { trailers: rws.trailers, endStream: true, }) - if err != nil { - rws.dirty = true - } return len(p), err } return len(p), nil @@ -2920,14 +2914,12 @@ func (rws *responseWriterState) writeHeader(code int) { h.Del("Transfer-Encoding") } - if rws.conn.writeHeaders(rws.stream, &writeResHeaders{ + rws.conn.writeHeaders(rws.stream, &writeResHeaders{ streamID: rws.stream.id, httpResCode: code, h: h, endStream: rws.handlerDone && !rws.hasTrailers(), - }) != nil { - rws.dirty = true - } + }) return } @@ -2992,19 +2984,10 @@ func (w *responseWriter) write(lenData int, dataB []byte, dataS string) (n int, func (w *responseWriter) handlerDone() { rws := w.rws - dirty := rws.dirty rws.handlerDone = true w.Flush() w.rws = nil - if !dirty { - // Only recycle the pool if all prior Write calls to - // the serverConn goroutine completed successfully. If - // they returned earlier due to resets from the peer - // there might still be write goroutines outstanding - // from the serverConn referencing the rws memory. See - // issue 20704. - responseWriterStatePool.Put(rws) - } + responseWriterStatePool.Put(rws) } // Push errors. @@ -3187,6 +3170,7 @@ func (sc *serverConn) startPush(msg *startPushRequest) { panic(fmt.Sprintf("newWriterAndRequestNoBody(%+v): %v", msg.url, err)) } + sc.curHandlers++ go sc.runHandler(rw, req, sc.handler.ServeHTTP) return promisedID, nil } diff --git a/http2/server_push_test.go b/http2/server_push_test.go index 6e57de0b7..9882d9ef7 100644 --- a/http2/server_push_test.go +++ b/http2/server_push_test.go @@ -517,3 +517,55 @@ func TestServer_Push_RejectAfterGoAway(t *testing.T) { t.Error(err) } } + +func TestServer_Push_Underflow(t *testing.T) { + // Test for #63511: Send several requests which generate PUSH_PROMISE responses, + // verify they all complete successfully. + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.RequestURI() { + case "/": + opt := &http.PushOptions{ + Header: http.Header{"User-Agent": {"testagent"}}, + } + if err := w.(http.Pusher).Push("/pushed", opt); err != nil { + t.Errorf("error pushing: %v", err) + } + w.WriteHeader(200) + case "/pushed": + r.Header.Set("User-Agent", "newagent") + r.Header.Set("Cookie", "cookie") + w.WriteHeader(200) + default: + t.Errorf("unknown RequestURL %q", r.URL.RequestURI()) + } + }) + // Send several requests. + st.greet() + const numRequests = 4 + for i := 0; i < numRequests; i++ { + st.writeHeaders(HeadersFrameParam{ + StreamID: uint32(1 + i*2), // clients send odd numbers + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + } + // Each request should result in one PUSH_PROMISE and two responses. + numPushPromises := 0 + numHeaders := 0 + for numHeaders < numRequests*2 || numPushPromises < numRequests { + f, err := st.readFrame() + if err != nil { + st.t.Fatal(err) + } + switch f := f.(type) { + case *HeadersFrame: + if !f.Flags.Has(FlagHeadersEndStream) { + t.Fatalf("got HEADERS frame with no END_STREAM, expected END_STREAM: %v", f) + } + numHeaders++ + case *PushPromiseFrame: + numPushPromises++ + } + } +} diff --git a/http2/transport.go b/http2/transport.go index 4515b22c4..df578b86c 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -1018,7 +1018,7 @@ func (cc *ClientConn) forceCloseConn() { if !ok { return } - if nc := tlsUnderlyingConn(tc); nc != nil { + if nc := tc.NetConn(); nc != nil { nc.Close() } } @@ -3201,3 +3201,34 @@ func traceFirstResponseByte(trace *httptrace.ClientTrace) { trace.GotFirstResponseByte() } } + +func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { + return trace != nil && trace.WroteHeaderField != nil +} + +func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { + if trace != nil && trace.WroteHeaderField != nil { + trace.WroteHeaderField(k, []string{v}) + } +} + +func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { + if trace != nil { + return trace.Got1xxResponse + } + return nil +} + +// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS +// connection. +func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { + dialer := &tls.Dialer{ + Config: cfg, + } + cn, err := dialer.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed + return tlsCn, nil +} diff --git a/http2/transport_go117_test.go b/http2/transport_go117_test.go deleted file mode 100644 index f5d4e0c1a..000000000 --- a/http2/transport_go117_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.17 -// +build go1.17 - -package http2 - -import ( - "context" - "crypto/tls" - "errors" - "net/http" - "net/http/httptest" - - "testing" -) - -func TestTransportDialTLSContext(t *testing.T) { - blockCh := make(chan struct{}) - serverTLSConfigFunc := func(ts *httptest.Server) { - ts.Config.TLSConfig = &tls.Config{ - // Triggers the server to request the clients certificate - // during TLS handshake. - ClientAuth: tls.RequestClientCert, - } - } - ts := newServerTester(t, - func(w http.ResponseWriter, r *http.Request) {}, - optOnlyServer, - serverTLSConfigFunc, - ) - defer ts.Close() - tr := &Transport{ - TLSClientConfig: &tls.Config{ - GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { - // Tests that the context provided to `req` is - // passed into this function. - close(blockCh) - <-cri.Context().Done() - return nil, cri.Context().Err() - }, - InsecureSkipVerify: true, - }, - } - defer tr.CloseIdleConnections() - req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) - if err != nil { - t.Fatal(err) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - req = req.WithContext(ctx) - errCh := make(chan error) - go func() { - defer close(errCh) - res, err := tr.RoundTrip(req) - if err != nil { - errCh <- err - return - } - res.Body.Close() - }() - // Wait for GetClientCertificate handler to be called - <-blockCh - // Cancel the context - cancel() - // Expect the cancellation error here - err = <-errCh - if err == nil { - t.Fatal("cancelling context during client certificate fetch did not error as expected") - return - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("unexpected error returned after cancellation: %v", err) - } -} - -// TestDialRaceResumesDial tests that, given two concurrent requests -// to the same address, when the first Dial is interrupted because -// the first request's context is cancelled, the second request -// resumes the dial automatically. -func TestDialRaceResumesDial(t *testing.T) { - blockCh := make(chan struct{}) - serverTLSConfigFunc := func(ts *httptest.Server) { - ts.Config.TLSConfig = &tls.Config{ - // Triggers the server to request the clients certificate - // during TLS handshake. - ClientAuth: tls.RequestClientCert, - } - } - ts := newServerTester(t, - func(w http.ResponseWriter, r *http.Request) {}, - optOnlyServer, - serverTLSConfigFunc, - ) - defer ts.Close() - tr := &Transport{ - TLSClientConfig: &tls.Config{ - GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { - select { - case <-blockCh: - // If we already errored, return without error. - return &tls.Certificate{}, nil - default: - } - close(blockCh) - <-cri.Context().Done() - return nil, cri.Context().Err() - }, - InsecureSkipVerify: true, - }, - } - defer tr.CloseIdleConnections() - req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) - if err != nil { - t.Fatal(err) - } - // Create two requests with independent cancellation. - ctx1, cancel1 := context.WithCancel(context.Background()) - defer cancel1() - req1 := req.WithContext(ctx1) - ctx2, cancel2 := context.WithCancel(context.Background()) - defer cancel2() - req2 := req.WithContext(ctx2) - errCh := make(chan error) - go func() { - res, err := tr.RoundTrip(req1) - if err != nil { - errCh <- err - return - } - res.Body.Close() - }() - successCh := make(chan struct{}) - go func() { - // Don't start request until first request - // has initiated the handshake. - <-blockCh - res, err := tr.RoundTrip(req2) - if err != nil { - errCh <- err - return - } - res.Body.Close() - // Close successCh to indicate that the second request - // made it to the server successfully. - close(successCh) - }() - // Wait for GetClientCertificate handler to be called - <-blockCh - // Cancel the context first - cancel1() - // Expect the cancellation error here - err = <-errCh - if err == nil { - t.Fatal("cancelling context during client certificate fetch did not error as expected") - return - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("unexpected error returned after cancellation: %v", err) - } - select { - case err := <-errCh: - t.Fatalf("unexpected second error: %v", err) - case <-successCh: - } -} diff --git a/http2/transport_test.go b/http2/transport_test.go index 99848485b..a81131f29 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -6369,3 +6369,154 @@ func TestTransportSlowClose(t *testing.T) { } res.Body.Close() } + +func TestTransportDialTLSContext(t *testing.T) { + blockCh := make(chan struct{}) + serverTLSConfigFunc := func(ts *httptest.Server) { + ts.Config.TLSConfig = &tls.Config{ + // Triggers the server to request the clients certificate + // during TLS handshake. + ClientAuth: tls.RequestClientCert, + } + } + ts := newServerTester(t, + func(w http.ResponseWriter, r *http.Request) {}, + optOnlyServer, + serverTLSConfigFunc, + ) + defer ts.Close() + tr := &Transport{ + TLSClientConfig: &tls.Config{ + GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + // Tests that the context provided to `req` is + // passed into this function. + close(blockCh) + <-cri.Context().Done() + return nil, cri.Context().Err() + }, + InsecureSkipVerify: true, + }, + } + defer tr.CloseIdleConnections() + req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + req = req.WithContext(ctx) + errCh := make(chan error) + go func() { + defer close(errCh) + res, err := tr.RoundTrip(req) + if err != nil { + errCh <- err + return + } + res.Body.Close() + }() + // Wait for GetClientCertificate handler to be called + <-blockCh + // Cancel the context + cancel() + // Expect the cancellation error here + err = <-errCh + if err == nil { + t.Fatal("cancelling context during client certificate fetch did not error as expected") + return + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("unexpected error returned after cancellation: %v", err) + } +} + +// TestDialRaceResumesDial tests that, given two concurrent requests +// to the same address, when the first Dial is interrupted because +// the first request's context is cancelled, the second request +// resumes the dial automatically. +func TestDialRaceResumesDial(t *testing.T) { + blockCh := make(chan struct{}) + serverTLSConfigFunc := func(ts *httptest.Server) { + ts.Config.TLSConfig = &tls.Config{ + // Triggers the server to request the clients certificate + // during TLS handshake. + ClientAuth: tls.RequestClientCert, + } + } + ts := newServerTester(t, + func(w http.ResponseWriter, r *http.Request) {}, + optOnlyServer, + serverTLSConfigFunc, + ) + defer ts.Close() + tr := &Transport{ + TLSClientConfig: &tls.Config{ + GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + select { + case <-blockCh: + // If we already errored, return without error. + return &tls.Certificate{}, nil + default: + } + close(blockCh) + <-cri.Context().Done() + return nil, cri.Context().Err() + }, + InsecureSkipVerify: true, + }, + } + defer tr.CloseIdleConnections() + req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) + if err != nil { + t.Fatal(err) + } + // Create two requests with independent cancellation. + ctx1, cancel1 := context.WithCancel(context.Background()) + defer cancel1() + req1 := req.WithContext(ctx1) + ctx2, cancel2 := context.WithCancel(context.Background()) + defer cancel2() + req2 := req.WithContext(ctx2) + errCh := make(chan error) + go func() { + res, err := tr.RoundTrip(req1) + if err != nil { + errCh <- err + return + } + res.Body.Close() + }() + successCh := make(chan struct{}) + go func() { + // Don't start request until first request + // has initiated the handshake. + <-blockCh + res, err := tr.RoundTrip(req2) + if err != nil { + errCh <- err + return + } + res.Body.Close() + // Close successCh to indicate that the second request + // made it to the server successfully. + close(successCh) + }() + // Wait for GetClientCertificate handler to be called + <-blockCh + // Cancel the context first + cancel1() + // Expect the cancellation error here + err = <-errCh + if err == nil { + t.Fatal("cancelling context during client certificate fetch did not error as expected") + return + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("unexpected error returned after cancellation: %v", err) + } + select { + case err := <-errCh: + t.Fatalf("unexpected second error: %v", err) + case <-successCh: + } +} diff --git a/icmp/helper_posix.go b/icmp/helper_posix.go index 6c3ebfaed..f625483f0 100644 --- a/icmp/helper_posix.go +++ b/icmp/helper_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package icmp diff --git a/icmp/listen_posix.go b/icmp/listen_posix.go index 6aea80478..b7cb15b7d 100644 --- a/icmp/listen_posix.go +++ b/icmp/listen_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package icmp diff --git a/icmp/listen_stub.go b/icmp/listen_stub.go index 1acfb74b6..7b76be1cb 100644 --- a/icmp/listen_stub.go +++ b/icmp/listen_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows package icmp diff --git a/idna/go118.go b/idna/go118.go index c5c4338db..712f1ad83 100644 --- a/idna/go118.go +++ b/idna/go118.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.18 -// +build go1.18 package idna diff --git a/idna/idna10.0.0.go b/idna/idna10.0.0.go index 64ccf85fe..7b3717884 100644 --- a/idna/idna10.0.0.go +++ b/idna/idna10.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.10 -// +build go1.10 // Package idna implements IDNA2008 using the compatibility processing // defined by UTS (Unicode Technical Standard) #46, which defines a standard to diff --git a/idna/idna9.0.0.go b/idna/idna9.0.0.go index ee1698cef..cc6a892a4 100644 --- a/idna/idna9.0.0.go +++ b/idna/idna9.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.10 -// +build !go1.10 // Package idna implements IDNA2008 using the compatibility processing // defined by UTS (Unicode Technical Standard) #46, which defines a standard to diff --git a/idna/pre_go118.go b/idna/pre_go118.go index 3aaccab1c..40e74bb3d 100644 --- a/idna/pre_go118.go +++ b/idna/pre_go118.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.18 -// +build !go1.18 package idna diff --git a/idna/tables10.0.0.go b/idna/tables10.0.0.go index d1d62ef45..c6c2bf10a 100644 --- a/idna/tables10.0.0.go +++ b/idna/tables10.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.10 && !go1.13 -// +build go1.10,!go1.13 package idna diff --git a/idna/tables11.0.0.go b/idna/tables11.0.0.go index 167efba71..76789393c 100644 --- a/idna/tables11.0.0.go +++ b/idna/tables11.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.13 && !go1.14 -// +build go1.13,!go1.14 package idna diff --git a/idna/tables12.0.0.go b/idna/tables12.0.0.go index ab40f7bcc..0600cd2ae 100644 --- a/idna/tables12.0.0.go +++ b/idna/tables12.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.14 && !go1.16 -// +build go1.14,!go1.16 package idna diff --git a/idna/tables13.0.0.go b/idna/tables13.0.0.go index 66701eadf..2fb768ef6 100644 --- a/idna/tables13.0.0.go +++ b/idna/tables13.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.16 && !go1.21 -// +build go1.16,!go1.21 package idna diff --git a/idna/tables15.0.0.go b/idna/tables15.0.0.go index 40033778f..5ff05fe1a 100644 --- a/idna/tables15.0.0.go +++ b/idna/tables15.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.21 -// +build go1.21 package idna diff --git a/idna/tables9.0.0.go b/idna/tables9.0.0.go index 4074b5332..0f25e84ca 100644 --- a/idna/tables9.0.0.go +++ b/idna/tables9.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build !go1.10 -// +build !go1.10 package idna diff --git a/idna/trie12.0.0.go b/idna/trie12.0.0.go index bb63f904b..8a75b9667 100644 --- a/idna/trie12.0.0.go +++ b/idna/trie12.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.16 -// +build !go1.16 package idna diff --git a/idna/trie13.0.0.go b/idna/trie13.0.0.go index 7d68a8dc1..fa45bb907 100644 --- a/idna/trie13.0.0.go +++ b/idna/trie13.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.16 -// +build go1.16 package idna diff --git a/internal/iana/gen.go b/internal/iana/gen.go index 34f0f7eee..0fe65d899 100644 --- a/internal/iana/gen.go +++ b/internal/iana/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/internal/quic/cmd/interop/Dockerfile b/internal/quic/cmd/interop/Dockerfile new file mode 100644 index 000000000..4b52e5356 --- /dev/null +++ b/internal/quic/cmd/interop/Dockerfile @@ -0,0 +1,32 @@ +FROM martenseemann/quic-network-simulator-endpoint:latest AS builder + +ARG TARGETPLATFORM +RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}" + +RUN apt-get update && apt-get install -y wget tar git + +ENV GOVERSION=1.21.1 + +RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \ + filename="go${GOVERSION}.${platform}.tar.gz" && \ + wget https://dl.google.com/go/${filename} && \ + tar xfz ${filename} && \ + rm ${filename} + +ENV PATH="/go/bin:${PATH}" + +RUN git clone https://go.googlesource.com/net + +WORKDIR /net +RUN go build -o /interop ./internal/quic/cmd/interop + +FROM martenseemann/quic-network-simulator-endpoint:latest + +WORKDIR /go-x-net + +COPY --from=builder /interop ./ + +# copy run script and run it +COPY run_endpoint.sh . +RUN chmod +x run_endpoint.sh +ENTRYPOINT [ "./run_endpoint.sh" ] diff --git a/internal/quic/cmd/interop/README.md b/internal/quic/cmd/interop/README.md new file mode 100644 index 000000000..aca0571b9 --- /dev/null +++ b/internal/quic/cmd/interop/README.md @@ -0,0 +1,7 @@ +This directory contains configuration and programs used to +integrate with the QUIC Interop Test Runner. + +The QUIC Interop Test Runner executes a variety of test cases +against a matrix of clients and servers. + +https://github.com/marten-seemann/quic-interop-runner diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go new file mode 100644 index 000000000..cc5292e9e --- /dev/null +++ b/internal/quic/cmd/interop/main.go @@ -0,0 +1,262 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +// The interop command is the client and server used by QUIC interoperability tests. +// +// https://github.com/marten-seemann/quic-interop-runner +package main + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "net/url" + "os" + "path/filepath" + "sync" + + "golang.org/x/net/internal/quic" +) + +var ( + listen = flag.String("listen", "", "listen address") + cert = flag.String("cert", "", "certificate") + pkey = flag.String("key", "", "private key") + root = flag.String("root", "", "serve files from this root") + output = flag.String("output", "", "directory to write files to") +) + +func main() { + ctx := context.Background() + flag.Parse() + urls := flag.Args() + + config := &quic.Config{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS13, + NextProtos: []string{"hq-interop"}, + }, + MaxBidiRemoteStreams: -1, + MaxUniRemoteStreams: -1, + } + if *cert != "" { + c, err := tls.LoadX509KeyPair(*cert, *pkey) + if err != nil { + log.Fatal(err) + } + config.TLSConfig.Certificates = []tls.Certificate{c} + } + if *root != "" { + config.MaxBidiRemoteStreams = 100 + } + if keylog := os.Getenv("SSLKEYLOGFILE"); keylog != "" { + f, err := os.Create(keylog) + if err != nil { + log.Fatal(err) + } + defer f.Close() + config.TLSConfig.KeyLogWriter = f + } + + testcase := os.Getenv("TESTCASE") + switch testcase { + case "handshake", "keyupdate": + basicTest(ctx, config, urls) + return + case "chacha20": + // "[...] offer only ChaCha20 as a ciphersuite." + // + // crypto/tls does not support configuring TLS 1.3 ciphersuites, + // so we can't support this test. + case "transfer": + // "The client should use small initial flow control windows + // for both stream- and connection-level flow control + // such that the during the transfer of files on the order of 1 MB + // the flow control window needs to be increased." + config.MaxStreamReadBufferSize = 64 << 10 + config.MaxConnReadBufferSize = 64 << 10 + basicTest(ctx, config, urls) + return + case "http3": + // TODO + case "multiconnect": + // TODO + case "resumption": + // TODO + case "retry": + // TODO + case "versionnegotiation": + // "The client should start a connection using + // an unsupported version number [...]" + // + // We don't support setting the client's version, + // so only run this test as a server. + if *listen != "" && len(urls) == 0 { + basicTest(ctx, config, urls) + return + } + case "v2": + // We do not support QUIC v2. + case "zerortt": + // TODO + } + fmt.Printf("unsupported test case %q\n", testcase) + os.Exit(127) +} + +// basicTest runs the standard test setup. +// +// As a server, it serves the contents of the -root directory. +// As a client, it downloads all the provided URLs in parallel, +// making one connection to each destination server. +func basicTest(ctx context.Context, config *quic.Config, urls []string) { + l, err := quic.Listen("udp", *listen, config) + if err != nil { + log.Fatal(err) + } + log.Printf("listening on %v", l.LocalAddr()) + + byAuthority := map[string][]*url.URL{} + for _, s := range urls { + u, addr, err := parseURL(s) + if err != nil { + log.Fatal(err) + } + byAuthority[addr] = append(byAuthority[addr], u) + } + var g sync.WaitGroup + defer g.Wait() + for addr, u := range byAuthority { + addr, u := addr, u + g.Add(1) + go func() { + defer g.Done() + fetchFrom(ctx, l, addr, u) + }() + } + + if config.MaxBidiRemoteStreams >= 0 { + serve(ctx, l) + } +} + +func serve(ctx context.Context, l *quic.Listener) error { + for { + c, err := l.Accept(ctx) + if err != nil { + return err + } + go serveConn(ctx, c) + } +} + +func serveConn(ctx context.Context, c *quic.Conn) { + for { + s, err := c.AcceptStream(ctx) + if err != nil { + return + } + go func() { + if err := serveReq(ctx, s); err != nil { + log.Print("serveReq:", err) + } + }() + } +} + +func serveReq(ctx context.Context, s *quic.Stream) error { + defer s.Close() + req, err := io.ReadAll(s) + if err != nil { + return err + } + if !bytes.HasSuffix(req, []byte("\r\n")) { + return errors.New("invalid request") + } + req = bytes.TrimSuffix(req, []byte("\r\n")) + if !bytes.HasPrefix(req, []byte("GET /")) { + return errors.New("invalid request") + } + req = bytes.TrimPrefix(req, []byte("GET /")) + if !filepath.IsLocal(string(req)) { + return errors.New("invalid request") + } + f, err := os.Open(filepath.Join(*root, string(req))) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(s, f) + return err +} + +func parseURL(s string) (u *url.URL, authority string, err error) { + u, err = url.Parse(s) + if err != nil { + return nil, "", err + } + host := u.Hostname() + port := u.Port() + if port == "" { + port = "443" + } + authority = net.JoinHostPort(host, port) + return u, authority, nil +} + +func fetchFrom(ctx context.Context, l *quic.Listener, addr string, urls []*url.URL) { + conn, err := l.Dial(ctx, "udp", addr) + if err != nil { + log.Printf("%v: %v", addr, err) + return + } + log.Printf("connected to %v", addr) + defer conn.Close() + var g sync.WaitGroup + for _, u := range urls { + u := u + g.Add(1) + go func() { + defer g.Done() + if err := fetchOne(ctx, conn, u); err != nil { + log.Printf("fetch %v: %v", u, err) + } else { + log.Printf("fetched %v", u) + } + }() + } + g.Wait() +} + +func fetchOne(ctx context.Context, conn *quic.Conn, u *url.URL) error { + if len(u.Path) == 0 || u.Path[0] != '/' || !filepath.IsLocal(u.Path[1:]) { + return errors.New("invalid path") + } + file, err := os.Create(filepath.Join(*output, u.Path[1:])) + if err != nil { + return err + } + s, err := conn.NewStream(ctx) + if err != nil { + return err + } + defer s.Close() + if _, err := s.Write([]byte("GET " + u.Path + "\r\n")); err != nil { + return err + } + s.CloseWrite() + if _, err := io.Copy(file, s); err != nil { + return err + } + return nil +} diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go new file mode 100644 index 000000000..4119740e6 --- /dev/null +++ b/internal/quic/cmd/interop/main_test.go @@ -0,0 +1,174 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package main + +import ( + "bufio" + "bytes" + "context" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" +) + +func init() { + // We reexec the test binary with CMD_INTEROP_MAIN=1 to run main. + if os.Getenv("CMD_INTEROP_MAIN") == "1" { + main() + os.Exit(0) + } +} + +var ( + tryExecOnce sync.Once + tryExecErr error +) + +// needsExec skips the test if we can't use exec.Command. +func needsExec(t *testing.T) { + tryExecOnce.Do(func() { + cmd := exec.Command(os.Args[0], "-test.list=^$") + cmd.Env = []string{} + tryExecErr = cmd.Run() + }) + if tryExecErr != nil { + t.Skipf("skipping test: cannot exec subprocess: %v", tryExecErr) + } +} + +type interopTest struct { + donec chan struct{} + addr string + cmd *exec.Cmd +} + +func run(ctx context.Context, t *testing.T, name, testcase string, args []string) *interopTest { + needsExec(t) + ctx, cancel := context.WithCancel(ctx) + cmd := exec.CommandContext(ctx, os.Args[0], args...) + out, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + cmd.Stdout = cmd.Stderr + cmd.Env = []string{ + "CMD_INTEROP_MAIN=1", + "TESTCASE=" + testcase, + } + t.Logf("run %v: %v", name, args) + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + + addrc := make(chan string, 1) + donec := make(chan struct{}) + go func() { + defer close(addrc) + defer close(donec) + defer t.Logf("%v done", name) + s := bufio.NewScanner(out) + for s.Scan() { + line := s.Text() + t.Logf("%v: %v", name, line) + _, addr, ok := strings.Cut(line, "listening on ") + if ok { + select { + case addrc <- addr: + default: + } + } + } + }() + + t.Cleanup(func() { + cancel() + <-donec + }) + + addr, ok := <-addrc + if !ok { + t.Fatal(cmd.Wait()) + } + _, port, _ := net.SplitHostPort(addr) + addr = net.JoinHostPort("localhost", port) + + iop := &interopTest{ + cmd: cmd, + donec: donec, + addr: addr, + } + return iop +} + +func (iop *interopTest) wait() { + <-iop.donec +} + +func TestTransfer(t *testing.T) { + ctx := context.Background() + src := t.TempDir() + dst := t.TempDir() + certs := t.TempDir() + certFile := filepath.Join(certs, "cert.pem") + keyFile := filepath.Join(certs, "key.pem") + sourceName := "source" + content := []byte("hello, world\n") + + os.WriteFile(certFile, localhostCert, 0600) + os.WriteFile(keyFile, localhostKey, 0600) + os.WriteFile(filepath.Join(src, sourceName), content, 0600) + + srv := run(ctx, t, "server", "transfer", []string{ + "-listen", "localhost:0", + "-cert", filepath.Join(certs, "cert.pem"), + "-key", filepath.Join(certs, "key.pem"), + "-root", src, + }) + cli := run(ctx, t, "client", "transfer", []string{ + "-output", dst, "https://" + srv.addr + "/" + sourceName, + }) + cli.wait() + + got, err := os.ReadFile(filepath.Join(dst, "source")) + if err != nil { + t.Fatalf("reading downloaded file: %v", err) + } + if !bytes.Equal(got, content) { + t.Fatalf("got downloaded file: %q, want %q", string(got), string(content)) + } +} + +// localhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --ecdsa-curve P256 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIBrDCCAVKgAwIBAgIPCvPhO+Hfv+NW76kWxULUMAoGCCqGSM49BAMCMBIxEDAO +BgNVBAoTB0FjbWUgQ28wIBcNNzAwMTAxMDAwMDAwWhgPMjA4NDAxMjkxNjAwMDBa +MBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARh +WRF8p8X9scgW7JjqAwI9nYV8jtkdhqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGms +PyfMPe5Jrha/LmjgR1G9o4GIMIGFMA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSOJri/wLQxq6oC +Y6ZImms/STbTljAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAA +AAAAAAAAAAAAATAKBggqhkjOPQQDAgNIADBFAiBUguxsW6TGhixBAdORmVNnkx40 +HjkKwncMSDbUaeL9jQIhAJwQ8zV9JpQvYpsiDuMmqCuW35XXil3cQ6Drz82c+fvE +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(`-----BEGIN TESTING KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgY1B1eL/Bbwf/MDcs +rnvvWhFNr1aGmJJR59PdCN9lVVqhRANCAARhWRF8p8X9scgW7JjqAwI9nYV8jtkd +hqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGmsPyfMPe5Jrha/LmjgR1G9 +-----END TESTING KEY-----`)) + +// testingKey helps keep security scanners from getting excited about a private key in this file. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/internal/quic/cmd/interop/run_endpoint.sh b/internal/quic/cmd/interop/run_endpoint.sh new file mode 100644 index 000000000..d72335d8e --- /dev/null +++ b/internal/quic/cmd/interop/run_endpoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Set up the routing needed for the simulation +/setup.sh + +# The following variables are available for use: +# - ROLE contains the role of this execution context, client or server +# - SERVER_PARAMS contains user-supplied command line parameters +# - CLIENT_PARAMS contains user-supplied command line parameters + +if [ "$ROLE" == "client" ]; then + # Wait for the simulator to start up. + /wait-for-it.sh sim:57832 -s -t 30 + ./interop -output=/downloads $CLIENT_PARAMS $REQUESTS +elif [ "$ROLE" == "server" ]; then + ./interop -cert=/certs/cert.pem -key=/certs/priv.key -listen=:443 -root=/www "$@" $SERVER_PARAMS +fi diff --git a/internal/quic/config.go b/internal/quic/config.go index b390d6911..6278bf89c 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -47,6 +47,31 @@ type Config struct { // If zero, the default value of 1MiB is used. // If negative, the limit is zero. MaxConnReadBufferSize int64 + + // RequireAddressValidation may be set to true to enable address validation + // of client connections prior to starting the handshake. + // + // Enabling this setting reduces the amount of work packets with spoofed + // source address information can cause a server to perform, + // at the cost of increased handshake latency. + RequireAddressValidation bool + + // StatelessResetKey is used to provide stateless reset of connections. + // A restart may leave an endpoint without access to the state of + // existing connections. Stateless reset permits an endpoint to respond + // to a packet for a connection it does not recognize. + // + // This field should be filled with random bytes. + // The contents should remain stable across restarts, + // to permit an endpoint to send a reset for + // connections created before a restart. + // + // The contents of the StatelessResetKey should not be exposed. + // An attacker can use knowledge of this field's value to + // reset existing connections. + // + // If this field is left as zero, stateless reset is disabled. + StatelessResetKey [32]byte } func configDefault(v, def, limit int64) int64 { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 9db00fe09..1292f2b20 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -48,6 +48,9 @@ type Conn struct { crypto [numberSpaceCount]cryptoStream tls *tls.QUICConn + // retryToken is the token provided by the peer in a Retry packet. + retryToken []byte + // handshakeConfirmed is set when the handshake is confirmed. // For server connections, it tracks sending HANDSHAKE_DONE. handshakeConfirmed sentVal @@ -61,14 +64,37 @@ type Conn struct { // connTestHooks override conn behavior in tests. type connTestHooks interface { + // init is called after a conn is created. + init() + + // nextMessage is called to request the next event from msgc. + // Used to give tests control of the connection event loop. nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) + + // handleTLSEvent is called with each TLS event. handleTLSEvent(tls.QUICEvent) + + // newConnID is called to generate a new connection ID. + // Permits tests to generate consistent connection IDs rather than random ones. newConnID(seq int64) ([]byte, error) + + // waitUntil blocks until the until func returns true or the context is done. + // Used to synchronize asynchronous blocking operations in tests. waitUntil(ctx context.Context, until func() bool) error + + // timeNow returns the current time. timeNow() time.Time } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener, hooks connTestHooks) (*Conn, error) { +// newServerConnIDs is connection IDs associated with a new server connection. +type newServerConnIDs struct { + srcConnID []byte // source from client's current Initial + dstConnID []byte // destination from client's current Initial + originalDstConnID []byte // destination from client's first Initial + retrySrcConnID []byte // source from server's Retry +} + +func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -76,7 +102,6 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), - testHooks: hooks, maxIdleTimeout: defaultMaxIdleTimeout, idleTimeout: now.Add(defaultMaxIdleTimeout), peerAckDelayExponent: -1, @@ -86,17 +111,25 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // non-blocking operation. c.msgc = make(chan any, 1) - var originalDstConnID []byte + if l.testHooks != nil { + l.testHooks.newConn(c) + } + + // initialConnID is the connection ID used to generate Initial packet protection keys. + var initialConnID []byte if c.side == clientSide { if err := c.connIDState.initClient(c); err != nil { return nil, err } initialConnID, _ = c.connIDState.dstConnID() } else { - if err := c.connIDState.initServer(c, initialConnID); err != nil { + initialConnID = cids.originalDstConnID + if cids.retrySrcConnID != nil { + initialConnID = cids.retrySrcConnID + } + if err := c.connIDState.initServer(c, cids); err != nil { return nil, err } - originalDstConnID = initialConnID } // The smallest allowed maximum QUIC datagram size is 1200 bytes. @@ -107,10 +140,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.streamsInit() c.lifetimeInit() - // TODO: retry_source_connection_id if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), - originalDstConnID: originalDstConnID, + originalDstConnID: cids.originalDstConnID, + retrySrcConnID: cids.retrySrcConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, @@ -126,6 +159,9 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. return nil, err } + if c.testHooks != nil { + c.testHooks.init() + } go c.loop(now) return c, nil } @@ -174,7 +210,8 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { - if err := c.connIDState.validateTransportParameters(c.side, p); err != nil { + isRetry := c.retryToken != nil + if err := c.connIDState.validateTransportParameters(c, isRetry, p); err != nil { return err } c.streams.outflow.setMaxData(p.initialMaxData) @@ -195,13 +232,15 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { resetToken [16]byte ) copy(resetToken[:], p.preferredAddrResetToken) - if err := c.connIDState.handleNewConnID(seq, retirePriorTo, p.preferredAddrConnID, resetToken); err != nil { + if err := c.connIDState.handleNewConnID(c, seq, retirePriorTo, p.preferredAddrConnID, resetToken); err != nil { return err } } - - // TODO: Many more transport parameters to come. - + // TODO: max_idle_timeout + // TODO: stateless_reset_token + // TODO: max_udp_payload_size + // TODO: disable_active_migration + // TODO: preferred_address return nil } diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index b8b86fd6f..a9ef0db5e 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -62,7 +62,7 @@ func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { c.lifetime.drainEndTime = time.Time{} if c.lifetime.finalErr == nil { // The peer never responded to our CONNECTION_CLOSE. - c.enterDraining(errNoPeerResponse) + c.enterDraining(now, errNoPeerResponse) } return true } @@ -152,11 +152,17 @@ func (c *Conn) sendOK(now time.Time) bool { } // enterDraining enters the draining state. -func (c *Conn) enterDraining(err error) { +func (c *Conn) enterDraining(now time.Time, err error) { if c.isDraining() { return } - if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != errNo { + if err == errStatelessReset { + // If we've received a stateless reset, then we must not send a CONNECTION_CLOSE. + // Setting connCloseSentTime here prevents us from doing so. + c.lifetime.finalErr = errStatelessReset + c.lifetime.localErr = errStatelessReset + c.lifetime.connCloseSentTime = now + } else if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo { // If we've terminated the connection due to a peer protocol violation, // record the final error on the connection as our reason for termination. c.lifetime.finalErr = c.lifetime.localErr @@ -220,7 +226,7 @@ func (c *Conn) Wait(ctx context.Context) error { // Otherwise, Abort sends a transport error of APPLICATION_ERROR with the error's text. func (c *Conn) Abort(err error) { if err == nil { - err = localTransportError(errNo) + err = localTransportError{code: errNo} } c.sendMsg(func(now time.Time, c *Conn) { c.abort(now, err) @@ -239,14 +245,14 @@ func (c *Conn) abort(now time.Time, err error) { // The connection does not send a CONNECTION_CLOSE, and skips the draining period. func (c *Conn) abortImmediately(now time.Time, err error) { c.abort(now, err) - c.enterDraining(err) + c.enterDraining(now, err) c.exited = true } // exit fully terminates a connection immediately. func (c *Conn) exit() { c.sendMsg(func(now time.Time, c *Conn) { - c.enterDraining(errors.New("connection closed")) + c.enterDraining(now, errors.New("connection closed")) c.exited = true }) } diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 20c00e754..d583ae92a 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -15,7 +15,9 @@ import ( ) func TestConnCloseResponseBackoff(t *testing.T) { - tc := newTestConn(t, clientSide) + tc := newTestConn(t, clientSide, func(c *Config) { + clear(c.StatelessResetKey[:]) + }) tc.handshake() tc.conn.Abort(nil) @@ -184,3 +186,15 @@ func TestConnCloseReceiveInHandshake(t *testing.T) { }) tc.wantIdle("no more frames to send") } + +func TestConnCloseClosedByListener(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, clientSide) + tc.handshake() + + tc.listener.l.Close(ctx) + tc.wantFrame("listener closes connection before exiting", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) +} diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index 4f1ab6eaf..8b69ef7db 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -90,7 +90,10 @@ func (c *Conn) shouldUpdateFlowControl(credit int64) bool { func (c *Conn) handleStreamBytesReceived(n int64) error { c.streams.inflow.usedLimit += n if c.streams.inflow.usedLimit > c.streams.inflow.sentLimit { - return localTransportError(errFlowControl) + return localTransportError{ + code: errFlowControl, + reason: "stream exceeded flow control limit", + } } return nil } diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 045e646ac..439c22123 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -22,12 +22,15 @@ type connIDState struct { // // These are []connID rather than []*connID to minimize allocations. local []connID - remote []connID + remote []remoteConnID nextLocalSeq int64 retireRemotePriorTo int64 // largest Retire Prior To value sent by the peer peerActiveConnIDLimit int64 // peer's active_connection_id_limit transport parameter + originalDstConnID []byte // expected original_destination_connection_id param + retrySrcConnID []byte // expected retry_source_connection_id param + needSend bool } @@ -55,6 +58,12 @@ type connID struct { send sentVal } +// A remoteConnID is a connection ID and stateless reset token. +type remoteConnID struct { + connID + resetToken statelessResetToken +} + func (s *connIDState) initClient(c *Conn) error { // Client chooses its initial connection ID, and sends it // in the Source Connection ID field of the first Initial packet. @@ -67,6 +76,9 @@ func (s *connIDState) initClient(c *Conn) error { cid: locid, }) s.nextLocalSeq = 1 + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addConnID(c, locid) + }) // Client chooses an initial, transient connection ID for the server, // and sends it in the Destination Connection ID field of the first Initial packet. @@ -74,22 +86,24 @@ func (s *connIDState) initClient(c *Conn) error { if err != nil { return err } - s.remote = append(s.remote, connID{ - seq: -1, - cid: remid, + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: -1, + cid: remid, + }, }) - const retired = false - c.listener.connIDsChanged(c, retired, s.local[:]) + s.originalDstConnID = remid return nil } -func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { +func (s *connIDState) initServer(c *Conn, cids newServerConnIDs) error { + dstConnID := cloneBytes(cids.dstConnID) // Client-chosen, transient connection ID received in the first Initial packet. // The server will not use this as the Source Connection ID of packets it sends, // but remembers it because it may receive packets sent to this destination. s.local = append(s.local, connID{ seq: -1, - cid: cloneBytes(dstConnID), + cid: dstConnID, }) // Server chooses a connection ID, and sends it in the Source Connection ID of @@ -103,8 +117,18 @@ func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { cid: locid, }) s.nextLocalSeq = 1 - const retired = false - c.listener.connIDsChanged(c, retired, s.local[:]) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addConnID(c, dstConnID) + conns.addConnID(c, locid) + }) + + // Client chose its own connection ID. + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(cids.srcConnID), + }, + }) return nil } @@ -127,6 +151,19 @@ func (s *connIDState) dstConnID() (cid []byte, ok bool) { return nil, false } +// isValidStatelessResetToken reports whether the given reset token is +// associated with a non-retired connection ID which we have used. +func (s *connIDState) isValidStatelessResetToken(resetToken statelessResetToken) bool { + for i := range s.remote { + // We currently only use the first available remote connection ID, + // so any other reset token is not valid. + if !s.remote[i].retired { + return s.remote[i].resetToken == resetToken + } + } + return false +} + // setPeerActiveConnIDLimit sets the active_connection_id_limit // transport parameter received from the peer. func (s *connIDState) setPeerActiveConnIDLimit(c *Conn, lim int64) error { @@ -141,12 +178,13 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { toIssue-- } } - prev := len(s.local) + var newIDs [][]byte for toIssue > 0 { cid, err := c.newConnID(s.nextLocalSeq) if err != nil { return err } + newIDs = append(newIDs, cid) s.local = append(s.local, connID{ seq: s.nextLocalSeq, cid: cid, @@ -156,40 +194,62 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { s.needSend = true toIssue-- } - const retired = false - c.listener.connIDsChanged(c, retired, s.local[prev:]) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + for _, cid := range newIDs { + conns.addConnID(c, cid) + } + }) return nil } // validateTransportParameters verifies the original_destination_connection_id and // initial_source_connection_id transport parameters match the expected values. -func (s *connIDState) validateTransportParameters(side connSide, p transportParameters) error { +func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p transportParameters) error { // TODO: Consider returning more detailed errors, for debugging. - switch side { - case clientSide: - // Verify original_destination_connection_id matches - // the transient remote connection ID we chose. - if len(s.remote) == 0 || s.remote[0].seq != -1 { - return localTransportError(errInternal) - } - if !bytes.Equal(s.remote[0].cid, p.originalDstConnID) { - return localTransportError(errTransportParameter) + // Verify original_destination_connection_id matches + // the transient remote connection ID we chose (client) + // or is empty (server). + if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) { + return localTransportError{ + code: errTransportParameter, + reason: "original_destination_connection_id mismatch", } - // Remove the transient remote connection ID. - // We have no further need for it. - s.remote = append(s.remote[:0], s.remote[1:]...) - case serverSide: - if p.originalDstConnID != nil { - // Clients do not send original_destination_connection_id. - return localTransportError(errTransportParameter) + } + s.originalDstConnID = nil // we have no further need for this + // Verify retry_source_connection_id matches the value from + // the server's Retry packet (when one was sent), or is empty. + if !bytes.Equal(p.retrySrcConnID, s.retrySrcConnID) { + return localTransportError{ + code: errTransportParameter, + reason: "retry_source_connection_id mismatch", } } + s.retrySrcConnID = nil // we have no further need for this // Verify initial_source_connection_id matches the first remote connection ID. if len(s.remote) == 0 || s.remote[0].seq != 0 { - return localTransportError(errInternal) + return localTransportError{ + code: errInternal, + reason: "remote connection id missing", + } } if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "initial_source_connection_id mismatch", + } + } + if len(p.statelessResetToken) > 0 { + if c.side == serverSide { + return localTransportError{ + code: errTransportParameter, + reason: "client sent stateless_reset_token", + } + } + token := statelessResetToken(p.statelessResetToken) + s.remote[0].resetToken = token + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addResetToken(c, token) + }) } return nil } @@ -203,42 +263,45 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // We're a client connection processing the first Initial packet // from the server. Replace the transient remote connection ID // with the Source Connection ID from the packet. - // Leave the transient ID the list for now, since we'll need it when - // processing the transport parameters. - s.remote[0].retired = true - s.remote = append(s.remote, connID{ - seq: 0, - cid: cloneBytes(srcConnID), - }) - } - case ptype == packetTypeInitial && c.side == serverSide: - if len(s.remote) == 0 { - // We're a server connection processing the first Initial packet - // from the client. Set the client's connection ID. - s.remote = append(s.remote, connID{ - seq: 0, - cid: cloneBytes(srcConnID), - }) + s.remote[0] = remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(srcConnID), + }, + } } case ptype == packetTypeHandshake && c.side == serverSide: if len(s.local) > 0 && s.local[0].seq == -1 && !s.local[0].retired { // We're a server connection processing the first Handshake packet from // the client. Discard the transient, client-chosen connection ID used // for Initial packets; the client will never send it again. - const retired = true - c.listener.connIDsChanged(c, retired, s.local[0:1]) + cid := s.local[0].cid + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireConnID(c, cid) + }) s.local = append(s.local[:0], s.local[1:]...) } } } -func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error { +func (s *connIDState) handleRetryPacket(srcConnID []byte) { + if len(s.remote) != 1 || s.remote[0].seq != -1 { + panic("BUG: handling retry with non-transient remote conn id") + } + s.retrySrcConnID = cloneBytes(srcConnID) + s.remote[0].cid = s.retrySrcConnID +} + +func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, resetToken statelessResetToken) error { if len(s.remote[0].cid) == 0 { // "An endpoint that is sending packets with a zero-length // Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID // frame as a connection error of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.15-6 - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "NEW_CONNECTION_ID from peer with zero-length DCID", + } } if retire > s.retireRemotePriorTo { @@ -251,13 +314,19 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken rcid := &s.remote[i] if !rcid.retired && rcid.seq >= 0 && rcid.seq < s.retireRemotePriorTo { s.retireRemote(rcid) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireResetToken(c, rcid.resetToken) + }) } if !rcid.retired { active++ } if rcid.seq == seq { if !bytes.Equal(rcid.cid, cid) { - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "NEW_CONNECTION_ID does not match prior id", + } } have = true // yes, we've seen this sequence number } @@ -269,15 +338,21 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken // We could take steps to keep the list of remote connection IDs // sorted by sequence number, but there's no particular need // so we don't bother. - s.remote = append(s.remote, connID{ - seq: seq, - cid: cloneBytes(cid), + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: seq, + cid: cloneBytes(cid), + }, + resetToken: resetToken, }) if seq < s.retireRemotePriorTo { // This ID was already retired by a previous NEW_CONNECTION_ID frame. s.retireRemote(&s.remote[len(s.remote)-1]) } else { active++ + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addResetToken(c, resetToken) + }) } } @@ -285,7 +360,10 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken // Retired connection IDs (including newly-retired ones) do not count // against the limit. // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.1.1-5 - return localTransportError(errConnectionIDLimit) + return localTransportError{ + code: errConnectionIDLimit, + reason: "active_connection_id_limit exceeded", + } } // "An endpoint SHOULD limit the number of connection IDs it has retired locally @@ -295,14 +373,17 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken // Set a limit of four times the active_connection_id_limit for // the total number of remote connection IDs we keep state for locally. if len(s.remote) > 4*activeConnIDLimit { - return localTransportError(errConnectionIDLimit) + return localTransportError{ + code: errConnectionIDLimit, + reason: "too many unacknowledged RETIRE_CONNECTION_ID frames", + } } return nil } // retireRemote marks a remote connection ID as retired. -func (s *connIDState) retireRemote(rcid *connID) { +func (s *connIDState) retireRemote(rcid *remoteConnID) { rcid.retired = true rcid.send.setUnsent() s.needSend = true @@ -310,12 +391,17 @@ func (s *connIDState) retireRemote(rcid *connID) { func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { if seq >= s.nextLocalSeq { - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "RETIRE_CONNECTION_ID for unissued sequence number", + } } for i := range s.local { if s.local[i].seq == seq { - const retired = true - c.listener.connIDsChanged(c, retired, s.local[i:i+1]) + cid := s.local[i].cid + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireConnID(c, cid) + }) s.local = append(s.local[:i], s.local[i+1:]...) break } @@ -360,7 +446,7 @@ func (s *connIDState) ackOrLossRetireConnectionID(pnum packetNumber, seq int64, // // It returns true if no more frames need appending, // false if not everything fit in the current packet. -func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) bool { +func (s *connIDState) appendFrames(c *Conn, pnum packetNumber, pto bool) bool { if !s.needSend && !pto { // Fast path: We don't need to send anything. return true @@ -373,11 +459,11 @@ func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) if !s.local[i].send.shouldSendPTO(pto) { continue } - if !w.appendNewConnectionIDFrame( + if !c.w.appendNewConnectionIDFrame( s.local[i].seq, retireBefore, s.local[i].cid, - [16]byte{}, // TODO: stateless reset token + c.listener.resetGen.tokenForConnID(s.local[i].cid), ) { return false } @@ -387,7 +473,7 @@ func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) if !s.remote[i].send.shouldSendPTO(pto) { continue } - if !w.appendRetireConnectionIDFrame(s.remote[i].seq) { + if !c.w.appendRetireConnectionIDFrame(s.remote[i].seq) { return false } s.remote[i].send.setSent(pnum) diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 44755ecf4..314a6b384 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -47,15 +47,14 @@ func TestConnIDClientHandshake(t *testing.T) { if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } - wantRemote := []connID{{ - cid: testLocalConnID(-1), - seq: -1, - }, { - cid: testPeerConnID(0), - seq: 0, + wantRemote := []remoteConnID{{ + connID: connID{ + cid: testPeerConnID(0), + seq: 0, + }, }} - if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { - t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) + if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } } @@ -96,12 +95,14 @@ func TestConnIDServerHandshake(t *testing.T) { if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } - wantRemote := []connID{{ - cid: testPeerConnID(0), - seq: 0, + wantRemote := []remoteConnID{{ + connID: connID{ + cid: testPeerConnID(0), + seq: 0, + }, }} - if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { - t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) + if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } // The client's first Handshake packet permits the server to discard the @@ -137,6 +138,24 @@ func connIDListEqual(a, b []connID) bool { return true } +func remoteConnIDListEqual(a, b []remoteConnID) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].seq != b[i].seq { + return false + } + if !bytes.Equal(a[i].cid, b[i].cid) { + return false + } + if a[i].resetToken != b[i].resetToken { + return false + } + } + return true +} + func fmtConnIDList(s []connID) string { var strs []string for _, cid := range s { @@ -145,6 +164,14 @@ func fmtConnIDList(s []connID) string { return "{" + strings.Join(strs, " ") + "}" } +func fmtRemoteConnIDList(s []remoteConnID) string { + var strs []string + for _, cid := range s { + strs = append(strs, fmt.Sprintf("[seq:%v cid:{%x} token:{%x}]", cid.seq, cid.cid, cid.resetToken)) + } + return "{" + strings.Join(strs, " ") + "}" +} + func TestNewRandomConnID(t *testing.T) { cid, err := newRandomConnID(0) if len(cid) != connIDLen || err != nil { @@ -177,16 +204,19 @@ func TestConnIDPeerRequestsManyIDs(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.wantFrame("provide additional connection ID 2", packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.wantFrame("provide additional connection ID 3", packetType1RTT, debugFrameNewConnectionID{ seq: 3, connID: testLocalConnID(3), + token: testLocalStatelessResetToken(3), }) tc.wantIdle("connection ID limit reached, no more to provide") } @@ -258,6 +288,7 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { seq: 2, retirePriorTo: 1, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) }) } @@ -458,6 +489,7 @@ func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { retirePriorTo: 1, seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.wantIdle("repeated RETIRE_CONNECTION_ID frames are not an error") } @@ -546,8 +578,11 @@ func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = testPeerConnID(1) p.preferredAddrResetToken = make([]byte, 16) + }, func(cids *newServerConnIDs) { + cids.srcConnID = []byte{} + }, func(tc *testConn) { + tc.peerConnID = []byte{} }) - tc.peerConnID = []byte{} tc.writeFrames(packetTypeInitial, debugFrameCrypto{ @@ -586,3 +621,46 @@ func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { }) }) } + +func TestConnIDsCleanedUpAfterClose(t *testing.T) { + testSides(t, "", func(t *testing.T, side connSide) { + tc := newTestConn(t, side, func(p *transportParameters) { + if side == clientSide { + token := testPeerStatelessResetToken(0) + p.statelessResetToken = token[:] + } + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + token: testPeerStatelessResetToken(0), + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{}) + tc.conn.Abort(nil) + tc.wantFrame("CONN_CLOSE sent after user closes connection", + packetType1RTT, debugFrameConnectionCloseTransport{}) + + // Wait for the conn to drain. + // Then wait for the conn loop to exit, + // and force an immediate sync of the connsMap updates + // (normally only done by the listener read loop). + tc.advanceToTimer() + <-tc.conn.donec + tc.listener.l.connsMap.applyUpdates() + + if got := len(tc.listener.l.connsMap.byConnID); got != 0 { + t.Errorf("%v conn ids in listener map after closing, want 0", got) + } + if got := len(tc.listener.l.connsMap.byResetToken); got != 0 { + t.Errorf("%v reset tokens in listener map after closing, want 0", got) + } + }) +} diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 9b8846251..5144be6ac 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -160,6 +160,7 @@ func TestLostCryptoFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.triggerLossOrPTO(packetTypeHandshake, pto) tc.wantFrame("client resends Handshake CRYPTO frame", @@ -607,6 +608,7 @@ func TestLostNewConnectionIDFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.triggerLossOrPTO(packetType1RTT, pto) @@ -614,6 +616,7 @@ func TestLostNewConnectionIDFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) }) } @@ -669,6 +672,7 @@ func TestLostHandshakeDoneFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 9b1ba1ae1..896c6d74e 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -24,7 +24,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { ptype := getPacketType(buf) switch ptype { case packetTypeInitial: - if c.side == serverSide && len(dgram.b) < minimumClientInitialDatagramSize { + if c.side == serverSide && len(dgram.b) < paddedInitialDatagramSize { // Discard client-sent Initial packets in too-short datagrams. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 return @@ -34,13 +34,30 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf) case packetType1RTT: n = c.handle1RTT(now, buf) + case packetTypeRetry: + c.handleRetry(now, buf) + return case packetTypeVersionNegotiation: c.handleVersionNegotiation(now, buf) return default: - return + n = -1 } if n <= 0 { + // We don't expect to get a stateless reset with a valid + // destination connection ID, since the sender of a stateless + // reset doesn't know what the connection ID is. + // + // We're required to perform this check anyway. + // + // "[...] the comparison MUST be performed when the first packet + // in an incoming datagram [...] cannot be decrypted." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-2 + if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen { + var token statelessResetToken + copy(token[:], buf[len(buf)-len(token):]) + c.handleStatelessReset(now, token) + } // Invalid data at the end of a datagram is ignored. break } @@ -62,12 +79,18 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if buf[0]&reservedLongBits != 0 { // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "reserved header bits are not zero", + }) return -1 } if p.version != quicVersion1 { // The peer has changed versions on us mid-handshake? - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "protocol version changed during handshake", + }) return -1 } @@ -112,7 +135,10 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { if buf[0]&reserved1RTTBits != 0 { // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "reserved header bits are not zero", + }) return -1 } @@ -128,6 +154,42 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { return len(buf) } +func (c *Conn) handleRetry(now time.Time, pkt []byte) { + if c.side != clientSide { + return // clients don't send Retry packets + } + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + if !c.keysInitial.canRead() { + return // discarded Initial keys, connection is already established + } + if c.acks[initialSpace].seen.numRanges() != 0 { + return // processed at least one packet + } + if c.retryToken != nil { + return // received a Retry already + } + // "Clients MUST discard Retry packets that have a Retry Integrity Tag + // that cannot be validated." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + p, ok := parseRetryPacket(pkt, c.connIDState.originalDstConnID) + if !ok { + return + } + // "A client MUST discard a Retry packet with a zero-length Retry Token field." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + if len(p.token) == 0 { + return + } + c.retryToken = cloneBytes(p.token) + c.connIDState.handleRetryPacket(p.srcConnID) + // We need to resend any data we've already sent in Initial packets. + // We must not reuse already sent packet numbers. + c.loss.discardPackets(initialSpace, c.handleAckOrLoss) + // TODO: Discard 0-RTT packets as well, once we support 0-RTT. +} + var errVersionNegotiation = errors.New("server does not support QUIC version 1") func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) { @@ -169,7 +231,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, // "An endpoint MUST treat receipt of a packet containing no frames // as a connection error of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "packet contains no frames", + }) return false } // frameOK verifies that ptype is one of the packets in mask. @@ -179,7 +244,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, // that is not permitted as a connection error of type // PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "frame not allowed in packet", + }) return false } return true @@ -294,7 +362,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, n = c.handleHandshakeDoneFrame(now, space, payload) } if n < 0 { - c.abort(now, localTransportError(errFrameEncoding)) + c.abort(now, localTransportError{ + code: errFrameEncoding, + reason: "frame encoding error", + }) return false } payload = payload[n:] @@ -307,7 +378,10 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) largest, ackDelay, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) { if end > c.loss.nextNumber(space) { // Acknowledgement of a packet we never sent. - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "acknowledgement for unsent packet", + }) return } c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss) @@ -429,7 +503,7 @@ func (c *Conn) handleNewConnectionIDFrame(now time.Time, space numberSpace, payl if n < 0 { return -1 } - if err := c.connIDState.handleNewConnID(seq, retire, connID, resetToken); err != nil { + if err := c.connIDState.handleNewConnID(c, seq, retire, connID, resetToken); err != nil { c.abort(now, err) } return n @@ -451,7 +525,7 @@ func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte if n < 0 { return -1 } - c.enterDraining(peerTransportError{code: code, reason: reason}) + c.enterDraining(now, peerTransportError{code: code, reason: reason}) return n } @@ -460,7 +534,7 @@ func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []by if n < 0 { return -1 } - c.enterDraining(&ApplicationError{Code: code, Reason: reason}) + c.enterDraining(now, &ApplicationError{Code: code, Reason: reason}) return n } @@ -468,7 +542,10 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa if c.side == serverSide { // Clients should never send HANDSHAKE_DONE. // https://www.rfc-editor.org/rfc/rfc9000#section-19.20-4 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "client sent HANDSHAKE_DONE", + }) return -1 } if !c.isClosingOrDraining() { @@ -476,3 +553,12 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa } return 1 } + +var errStatelessReset = errors.New("received stateless reset") + +func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) { + if !c.connIDState.isValidStatelessResetToken(resetToken) { + return + } + c.enterDraining(now, errStatelessReset) +} diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 00b02c2a3..22e780479 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -68,6 +68,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { num: pnum, dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), + extra: c.retryToken, } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) c.appendFrames(now, initialSpace, pnum, limit) @@ -76,10 +77,11 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { - // Client initial packets need to be sent in a datagram padded to - // at least 1200 bytes. We can't add the padding yet, however, - // since we may want to coalesce additional packets with this one. - if c.side == clientSide { + // Client initial packets and ack-eliciting server initial packaets + // need to be sent in a datagram padded to at least 1200 bytes. + // We can't add the padding yet, however, since we may want to + // coalesce additional packets with this one. + if c.side == clientSide || sentInitial.ackEliciting { pad = true } } @@ -122,7 +124,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // 1-RTT packets have no length field and extend to the end // of the datagram, so if we're sending a datagram that needs // padding we need to add it inside the 1-RTT packet. - c.w.appendPaddingTo(minimumClientInitialDatagramSize) + c.w.appendPaddingTo(paddedInitialDatagramSize) pad = false } if logPackets { @@ -148,7 +150,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Pad out the datagram with zeros, coalescing the Initial // packet with invalid packets that will be ignored by the peer. // https://www.rfc-editor.org/rfc/rfc9000.html#section-14.1-1 - for len(buf) < minimumClientInitialDatagramSize { + for len(buf) < paddedInitialDatagramSize { buf = append(buf, 0) // Technically this padding isn't in any packet, but // account it to the Initial packet in this datagram @@ -249,7 +251,7 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, } // NEW_CONNECTION_ID, RETIRE_CONNECTION_ID - if !c.connIDState.appendFrames(&c.w, pnum, pto) { + if !c.connIDState.appendFrames(c, pnum, pto) { return } @@ -326,7 +328,7 @@ func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err c.lifetime.connCloseSentTime = now switch e := err.(type) { case localTransportError: - c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "") + c.w.appendConnectionCloseTransportFrame(e.code, 0, e.reason) case *ApplicationError: if space != appDataSpace { // "CONNECTION_CLOSE frames signaling application errors (type 0x1d) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index a0793297e..83ab5554c 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -127,7 +127,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) if (id.initiator() == c.side) != (ftype == sendStream) { // Received an invalid frame for unidirectional stream. // For example, a RESET_STREAM frame for a send-only stream. - c.abort(now, localTransportError(errStreamState)) + c.abort(now, localTransportError{ + code: errStreamState, + reason: "invalid frame for unidirectional stream", + }) return nil } } @@ -148,7 +151,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) } // Received a frame for a stream that should be originated by us, // but which we never created. - c.abort(now, localTransportError(errStreamState)) + c.abort(now, localTransportError{ + code: errStreamState, + reason: "received frame for unknown stream", + }) return nil } else { // if isOpen, this is a stream that was implicitly opened by a diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 6a359e89a..c70c58ef0 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -33,12 +33,12 @@ func TestConnTestConn(t *testing.T) { tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now }) - if !ranAt.Equal(tc.now) { - t.Errorf("func ran on loop at %v, want %v", ranAt, tc.now) + if !ranAt.Equal(tc.listener.now) { + t.Errorf("func ran on loop at %v, want %v", ranAt, tc.listener.now) } tc.wait() - nextTime := tc.now.Add(defaultMaxIdleTimeout / 2) + nextTime := tc.listener.now.Add(defaultMaxIdleTimeout / 2) tc.advanceTo(nextTime) tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now @@ -57,6 +57,7 @@ func TestConnTestConn(t *testing.T) { type testDatagram struct { packets []*testPacket paddedSize int + addr netip.AddrPort } func (d testDatagram) String() string { @@ -74,14 +75,16 @@ func (d testDatagram) String() string { } type testPacket struct { - ptype packetType - version uint32 - num packetNumber - keyPhaseBit bool - keyNumber int - dstConnID []byte - srcConnID []byte - frames []debugFrame + ptype packetType + version uint32 + num packetNumber + keyPhaseBit bool + keyNumber int + dstConnID []byte + srcConnID []byte + token []byte + originalDstConnID []byte // used for encoding Retry packets + frames []debugFrame } func (p testPacket) String() string { @@ -96,6 +99,9 @@ func (p testPacket) String() string { if p.dstConnID != nil { fmt.Fprintf(&b, " dst={%x}", p.dstConnID) } + if p.token != nil { + fmt.Fprintf(&b, " token={%x}", p.token) + } for _, f := range p.frames { fmt.Fprintf(&b, "\n %v", f) } @@ -111,7 +117,6 @@ type testConn struct { t *testing.T conn *Conn listener *testListener - now time.Time timer time.Time timerLastFired time.Time idlec chan struct{} // only accessed on the conn's loop @@ -183,10 +188,58 @@ type keySecret struct { // allowing test code to access Conn state directly // by first ensuring the loop goroutine is idle. func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { + t.Helper() + config := &Config{ + TLSConfig: newTestTLSConfig(side), + StatelessResetKey: testStatelessResetKey, + } + var cids newServerConnIDs + if side == serverSide { + // The initial connection ID for the server is chosen by the client. + cids.srcConnID = testPeerConnID(0) + cids.dstConnID = testPeerConnID(-1) + } + var configTransportParams []func(*transportParameters) + var configTestConn []func(*testConn) + for _, o := range opts { + switch o := o.(type) { + case func(*Config): + o(config) + case func(*tls.Config): + o(config.TLSConfig) + case func(cids *newServerConnIDs): + o(&cids) + case func(p *transportParameters): + configTransportParams = append(configTransportParams, o) + case func(p *testConn): + configTestConn = append(configTestConn, o) + default: + t.Fatalf("unknown newTestConn option %T", o) + } + } + + listener := newTestListener(t, config) + listener.configTransportParams = configTransportParams + listener.configTestConn = configTestConn + conn, err := listener.l.newConn( + listener.now, + side, + cids, + netip.MustParseAddrPort("127.0.0.1:443")) + if err != nil { + t.Fatal(err) + } + tc := listener.conns[conn] + tc.wait() + return tc +} + +func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testConn { t.Helper() tc := &testConn{ t: t, - now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + listener: listener, + conn: conn, peerConnID: testPeerConnID(0), ignoreFrames: map[byte]bool{ frameTypePadding: true, // ignore PADDING by default @@ -196,36 +249,28 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { recvDatagram: make(chan *datagram), } t.Cleanup(tc.cleanup) + for _, f := range listener.configTestConn { + f(tc) + } + conn.testHooks = (*testConnHooks)(tc) - config := &Config{ - TLSConfig: newTestTLSConfig(side), + if listener.peerTLSConn != nil { + tc.peerTLSConn = listener.peerTLSConn + listener.peerTLSConn = nil + return tc } + peerProvidedParams := defaultTransportParameters() peerProvidedParams.initialSrcConnID = testPeerConnID(0) - if side == clientSide { + if conn.side == clientSide { peerProvidedParams.originalDstConnID = testLocalConnID(-1) } - for _, o := range opts { - switch o := o.(type) { - case func(*Config): - o(config) - case func(*tls.Config): - o(config.TLSConfig) - case func(p *transportParameters): - o(&peerProvidedParams) - default: - t.Fatalf("unknown newTestConn option %T", o) - } + for _, f := range listener.configTransportParams { + f(&peerProvidedParams) } - var initialConnID []byte - if side == serverSide { - // The initial connection ID for the server is chosen by the client. - initialConnID = testPeerConnID(-1) - } - - peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(side.peer())} - if side == clientSide { + peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(conn.side.peer())} + if conn.side == clientSide { tc.peerTLSConn = tls.QUICServer(peerQUICConfig) } else { tc.peerTLSConn = tls.QUICClient(peerQUICConfig) @@ -233,43 +278,19 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) tc.peerTLSConn.Start(context.Background()) - tc.listener = newTestListener(t, config, (*testConnHooks)(tc)) - conn, err := tc.listener.l.newConn( - tc.now, - side, - initialConnID, - netip.MustParseAddrPort("127.0.0.1:443")) - if err != nil { - tc.t.Fatal(err) - } - tc.conn = conn - - conn.keysAppData.updateAfter = maxPacketNumber // disable key updates - tc.keysInitial.r = conn.keysInitial.w - tc.keysInitial.w = conn.keysInitial.r - - tc.wait() return tc } // advance causes time to pass. func (tc *testConn) advance(d time.Duration) { tc.t.Helper() - tc.advanceTo(tc.now.Add(d)) + tc.listener.advance(d) } // advanceTo sets the current time. func (tc *testConn) advanceTo(now time.Time) { tc.t.Helper() - if tc.now.After(now) { - tc.t.Fatalf("time moved backwards: %v -> %v", tc.now, now) - } - tc.now = now - if tc.timer.After(tc.now) { - return - } - tc.conn.sendMsg(timerEvent{}) - tc.wait() + tc.listener.advanceTo(now) } // advanceToTimer sets the current time to the time of the Conn's next timer event. @@ -284,10 +305,10 @@ func (tc *testConn) timerDelay() time.Duration { if tc.timer.IsZero() { return math.MaxInt64 // infinite } - if tc.timer.Before(tc.now) { + if tc.timer.Before(tc.listener.now) { return 0 } - return tc.timer.Sub(tc.now) + return tc.timer.Sub(tc.listener.now) } const infiniteDuration = time.Duration(math.MaxInt64) @@ -297,10 +318,10 @@ func (tc *testConn) timeUntilEvent() time.Duration { if tc.timer.IsZero() { return infiniteDuration } - if tc.timer.Before(tc.now) { + if tc.timer.Before(tc.listener.now) { return 0 } - return tc.timer.Sub(tc.now) + return tc.timer.Sub(tc.listener.now) } // wait blocks until the conn becomes idle. @@ -340,8 +361,8 @@ func (tc *testConn) cleanup() { <-tc.conn.donec } -func (tc *testConn) logDatagram(text string, d *testDatagram) { - tc.t.Helper() +func logDatagram(t *testing.T, text string, d *testDatagram) { + t.Helper() if !*testVV { return } @@ -349,7 +370,7 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { if d.paddedSize > 0 { pad = fmt.Sprintf(" (padded to %v)", d.paddedSize) } - tc.t.Logf("%v datagram%v", text, pad) + t.Logf("%v datagram%v", text, pad) for _, p := range d.packets { var s string switch p.ptype { @@ -358,15 +379,18 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { default: s = fmt.Sprintf(" %v pnum=%v ver=%v dst={%x} src={%x}", p.ptype, p.num, p.version, p.dstConnID, p.srcConnID) } + if p.token != nil { + s += fmt.Sprintf(" token={%x}", p.token) + } if p.keyPhaseBit { s += fmt.Sprintf(" KeyPhase") } if p.keyNumber != 0 { s += fmt.Sprintf(" keynum=%v", p.keyNumber) } - tc.t.Log(s) + t.Log(s) for _, f := range p.frames { - tc.t.Logf(" %v", f) + t.Logf(" %v", f) } } } @@ -374,27 +398,7 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { // write sends the Conn a datagram. func (tc *testConn) write(d *testDatagram) { tc.t.Helper() - var buf []byte - tc.logDatagram("<- conn under test receives", d) - for _, p := range d.packets { - space := spaceForPacketType(p.ptype) - if p.num >= tc.peerNextPacketNum[space] { - tc.peerNextPacketNum[space] = p.num + 1 - } - pad := 0 - if p.ptype == packetType1RTT { - pad = d.paddedSize - } - buf = append(buf, tc.encodeTestPacket(p, pad)...) - } - for len(buf) < d.paddedSize { - buf = append(buf, 0) - } - // TODO: This should use tc.listener.write. - tc.conn.sendMsg(&datagram{ - b: buf, - }) - tc.wait() + tc.listener.writeDatagram(d) } // writeFrame sends the Conn a datagram containing the given frames. @@ -464,10 +468,10 @@ func (tc *testConn) readDatagram() *testDatagram { if buf == nil { return nil } - d := tc.parseTestDatagram(buf) + d := parseTestDatagram(tc.t, tc.listener, tc, buf) // Log the datagram before removing ignored frames. // When things go wrong, it's useful to see all the frames. - tc.logDatagram("-> conn under test sends", d) + logDatagram(tc.t, "-> conn under test sends", d) typeForFrame := func(f debugFrame) byte { // This is very clunky, and points at a problem // in how we specify what frames to ignore in tests. @@ -551,7 +555,13 @@ func (tc *testConn) readPacket() *testPacket { if d == nil { return nil } - tc.sentPackets = d.packets + for _, p := range d.packets { + if len(p.frames) == 0 { + tc.lastPacket = p + continue + } + tc.sentPackets = append(tc.sentPackets, p) + } } p := tc.sentPackets[0] tc.sentPackets = tc.sentPackets[1:] @@ -584,6 +594,20 @@ func (tc *testConn) wantDatagram(expectation string, want *testDatagram) { } } +func datagramEqual(a, b *testDatagram) bool { + if a.paddedSize != b.paddedSize || + a.addr != b.addr || + len(a.packets) != len(b.packets) { + return false + } + for i := range a.packets { + if !packetEqual(a.packets[i], b.packets[i]) { + return false + } + } + return true +} + // wantPacket indicates that we expect the Conn to send a packet. func (tc *testConn) wantPacket(expectation string, want *testPacket) { tc.t.Helper() @@ -593,6 +617,25 @@ func (tc *testConn) wantPacket(expectation string, want *testPacket) { } } +func packetEqual(a, b *testPacket) bool { + ac := *a + ac.frames = nil + bc := *b + bc.frames = nil + if !reflect.DeepEqual(ac, bc) { + return false + } + if len(a.frames) != len(b.frames) { + return false + } + for i := range a.frames { + if !frameEqual(a.frames[i], b.frames[i]) { + return false + } + } + return true +} + // wantFrame indicates that we expect the Conn to send a frame. func (tc *testConn) wantFrame(expectation string, wantType packetType, want debugFrame) { tc.t.Helper() @@ -603,11 +646,20 @@ func (tc *testConn) wantFrame(expectation string, wantType packetType, want debu if gotType != wantType { tc.t.Fatalf("%v:\ngot %v packet, want %v\ngot frame: %v", expectation, gotType, wantType, got) } - if !reflect.DeepEqual(got, want) { + if !frameEqual(got, want) { tc.t.Fatalf("%v:\ngot frame: %v\nwant frame: %v", expectation, got, want) } } +func frameEqual(a, b debugFrame) bool { + switch af := a.(type) { + case debugFrameConnectionCloseTransport: + bf, ok := b.(debugFrameConnectionCloseTransport) + return ok && af.code == bf.code + } + return reflect.DeepEqual(a, b) +} + // wantFrameType indicates that we expect the Conn to send a frame, // although we don't care about the contents. func (tc *testConn) wantFrameType(expectation string, wantType packetType, want debugFrame) { @@ -638,21 +690,29 @@ func (tc *testConn) wantIdle(expectation string) { } } -func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { - tc.t.Helper() +func encodeTestPacket(t *testing.T, tc *testConn, p *testPacket, pad int) []byte { + t.Helper() var w packetWriter w.reset(1200) var pnumMaxAcked packetNumber - if p.ptype != packetType1RTT { + switch p.ptype { + case packetTypeRetry: + return encodeRetryPacket(p.originalDstConnID, retryPacket{ + srcConnID: p.srcConnID, + dstConnID: p.dstConnID, + token: p.token, + }) + case packetType1RTT: + w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) + default: w.startProtectedLongHeaderPacket(pnumMaxAcked, longPacket{ ptype: p.ptype, version: p.version, num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + extra: p.token, }) - } else { - w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) } for _, f := range p.frames { f.write(&w) @@ -660,14 +720,22 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { w.appendPaddingTo(pad) if p.ptype != packetType1RTT { var k fixedKeys - switch p.ptype { - case packetTypeInitial: - k = tc.keysInitial.w - case packetTypeHandshake: - k = tc.keysHandshake.w + if tc == nil { + if p.ptype == packetTypeInitial { + k = initialKeys(p.dstConnID, serverSide).r + } else { + t.Fatalf("sending %v packet with no conn", p.ptype) + } + } else { + switch p.ptype { + case packetTypeInitial: + k = tc.keysInitial.w + case packetTypeHandshake: + k = tc.keysHandshake.w + } } if !k.isSet() { - tc.t.Fatalf("sending %v packet with no write key", p.ptype) + t.Fatalf("sending %v packet with no write key", p.ptype) } w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, longPacket{ ptype: p.ptype, @@ -675,10 +743,11 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + extra: p.token, }) } else { - if !tc.wkeyAppData.hdr.isSet() { - tc.t.Fatalf("sending 1-RTT packet with no write key") + if tc == nil || !tc.wkeyAppData.hdr.isSet() { + t.Fatalf("sending 1-RTT packet with no write key") } // Somewhat hackish: Generate a temporary updatingKeyPair that will // always use our desired key phase. @@ -700,8 +769,8 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { return w.datagram() } -func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { - tc.t.Helper() +func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) *testDatagram { + t.Helper() bufSize := len(buf) d := &testDatagram{} size := len(buf) @@ -711,25 +780,52 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { break } ptype := getPacketType(buf) - if isLongHeader(buf[0]) { - var k fixedKeyPair - switch ptype { - case packetTypeInitial: - k = tc.keysInitial - case packetTypeHandshake: - k = tc.keysHandshake + switch ptype { + case packetTypeRetry: + retry, ok := parseRetryPacket(buf, tl.lastInitialDstConnID) + if !ok { + t.Fatalf("could not parse %v packet", ptype) + } + return &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + dstConnID: retry.dstConnID, + srcConnID: retry.srcConnID, + token: retry.token, + }}, + } + case packetTypeInitial, packetTypeHandshake: + var k fixedKeys + if tc == nil { + if ptype == packetTypeInitial { + p, _ := parseGenericLongHeaderPacket(buf) + k = initialKeys(p.srcConnID, serverSide).w + } else { + t.Fatalf("reading %v packet with no conn", ptype) + } + } else { + switch ptype { + case packetTypeInitial: + k = tc.keysInitial.r + case packetTypeHandshake: + k = tc.keysHandshake.r + } } - if !k.canRead() { - tc.t.Fatalf("reading %v packet with no read key", ptype) + if !k.isSet() { + t.Fatalf("reading %v packet with no read key", ptype) } var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parseLongHeaderPacket(buf, k.r, pnumMax) + p, n := parseLongHeaderPacket(buf, k, pnumMax) if n < 0 { - tc.t.Fatalf("packet parse error") + t.Fatalf("packet parse error") } - frames, err := tc.parseTestFrames(p.payload) + frames, err := parseTestFrames(t, p.payload) if err != nil { - tc.t.Fatal(err) + t.Fatal(err) + } + var token []byte + if ptype == packetTypeInitial && len(p.extra) > 0 { + token = p.extra } d.packets = append(d.packets, &testPacket{ ptype: p.ptype, @@ -737,12 +833,13 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + token: token, frames: frames, }) buf = buf[n:] - } else { - if !tc.rkeyAppData.hdr.isSet() { - tc.t.Fatalf("reading 1-RTT packet with no read key") + case packetType1RTT: + if tc == nil || !tc.rkeyAppData.hdr.isSet() { + t.Fatalf("reading 1-RTT packet with no read key") } var pnumMax packetNumber // TODO: Track packet numbers. pnumOff := 1 + len(tc.peerConnID) @@ -756,7 +853,7 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { b := append([]byte{}, buf...) hdr, pay, pnum, err = tc.rkeyAppData.hdr.unprotect(b, pnumOff, pnumMax) if err != nil { - tc.t.Fatalf("1-RTT packet header parse error") + t.Fatalf("1-RTT packet header parse error") } k := tc.rkeyAppData.pkt[phase] pay, err = k.unprotect(hdr, pay, pnum) @@ -765,11 +862,11 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { } } if err != nil { - tc.t.Fatalf("1-RTT packet payload parse error") + t.Fatalf("1-RTT packet payload parse error") } - frames, err := tc.parseTestFrames(pay) + frames, err := parseTestFrames(t, pay) if err != nil { - tc.t.Fatal(err) + t.Fatal(err) } d.packets = append(d.packets, &testPacket{ ptype: packetType1RTT, @@ -780,6 +877,8 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { frames: frames, }) buf = buf[len(buf):] + default: + t.Fatalf("unhandled packet type %v", ptype) } } // This is rather hackish: If the last frame in the last packet @@ -799,8 +898,8 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { return d } -func (tc *testConn) parseTestFrames(payload []byte) ([]debugFrame, error) { - tc.t.Helper() +func parseTestFrames(t *testing.T, payload []byte) ([]debugFrame, error) { + t.Helper() var frames []debugFrame for len(payload) > 0 { f, n := parseDebugFrame(payload) @@ -822,7 +921,7 @@ func spaceForPacketType(ptype packetType) numberSpace { case packetTypeHandshake: return handshakeSpace case packetTypeRetry: - panic("TODO: packetTypeRetry") + panic("retry packets have no number space") case packetType1RTT: return appDataSpace } @@ -832,6 +931,15 @@ func spaceForPacketType(ptype packetType) numberSpace { // testConnHooks implements connTestHooks. type testConnHooks testConn +func (tc *testConnHooks) init() { + tc.conn.keysAppData.updateAfter = maxPacketNumber // disable key updates + tc.keysInitial.r = tc.conn.keysInitial.w + tc.keysInitial.w = tc.conn.keysInitial.r + if tc.conn.side == serverSide { + tc.listener.acceptQueue = append(tc.listener.acceptQueue, (*testConn)(tc)) + } +} + // handleTLSEvent processes TLS events generated by // the connection under test's tls.QUICConn. // @@ -929,20 +1037,20 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { tc.timer = timer for { - if !timer.IsZero() && !timer.After(tc.now) { + if !timer.IsZero() && !timer.After(tc.listener.now) { if timer.Equal(tc.timerLastFired) { // If the connection timer fires at time T, the Conn should take some // action to advance the timer into the future. If the Conn reschedules // the timer for the same time, it isn't making progress and we have a bug. - tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.now, timer) + tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.listener.now, timer) } else { tc.timerLastFired = timer - return tc.now, timerEvent{} + return tc.listener.now, timerEvent{} } } select { case m := <-msgc: - return tc.now, m + return tc.listener.now, m default: } if !tc.wakeAsync() { @@ -956,7 +1064,7 @@ func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.T close(idlec) } m = <-msgc - return tc.now, m + return tc.listener.now, m } func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { @@ -964,7 +1072,7 @@ func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { } func (tc *testConnHooks) timeNow() time.Time { - return tc.now + return tc.listener.now } // testLocalConnID returns the connection ID with a given sequence number @@ -984,6 +1092,13 @@ func testPeerConnID(seq int64) []byte { return []byte{0xbe, 0xee, 0xff, byte(seq)} } +func testPeerStatelessResetToken(seq int64) statelessResetToken { + return statelessResetToken{ + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, byte(seq), + } +} + // canceledContext returns a canceled Context. // // Functions which take a context preference progress over cancelation. diff --git a/internal/quic/crypto_stream.go b/internal/quic/crypto_stream.go index 8aa8f7b82..a4dcb32eb 100644 --- a/internal/quic/crypto_stream.go +++ b/internal/quic/crypto_stream.go @@ -30,7 +30,10 @@ type cryptoStream struct { func (s *cryptoStream) handleCrypto(off int64, b []byte, f func([]byte) error) error { end := off + int64(len(b)) if end-s.inset.min() > cryptoBufferSize { - return localTransportError(errCryptoBufferExceeded) + return localTransportError{ + code: errCryptoBufferExceeded, + reason: "crypto buffer exceeded", + } } s.inset.add(off, end) if off == s.in.start { diff --git a/internal/quic/crypto_stream_test.go b/internal/quic/crypto_stream_test.go index a6c1e1b52..6bee8bb9f 100644 --- a/internal/quic/crypto_stream_test.go +++ b/internal/quic/crypto_stream_test.go @@ -94,6 +94,21 @@ func TestCryptoStreamReceive(t *testing.T) { end: 3000, want: 4000, }}, + }, { + name: "resent consumed data", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + start: 0, + end: 1000, + want: 2000, + }}, }} { t.Run(test.name, func(t *testing.T) { var s cryptoStream diff --git a/internal/quic/errors.go b/internal/quic/errors.go index 8e01bb7cb..954793cfc 100644 --- a/internal/quic/errors.go +++ b/internal/quic/errors.go @@ -83,10 +83,16 @@ func (e transportError) String() string { } // A localTransportError is an error sent to the peer. -type localTransportError transportError +type localTransportError struct { + code transportError + reason string +} func (e localTransportError) Error() string { - return "closed connection: " + transportError(e).String() + if e.reason == "" { + return fmt.Sprintf("closed connection: %v", e.code) + } + return fmt.Sprintf("closed connection: %v: %q", e.code, e.reason) } // A peerTransportError is an error received from the peer. diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index 7a5aee57b..dc8009037 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -368,7 +368,7 @@ type debugFrameNewConnectionID struct { seq int64 retirePriorTo int64 connID []byte - token [16]byte + token statelessResetToken } func parseDebugFrameNewConnectionID(b []byte) (f debugFrameNewConnectionID, n int) { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 96b1e4593..ca8f9b25a 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -8,6 +8,7 @@ package quic import ( "context" + "crypto/rand" "errors" "net" "net/netip" @@ -23,21 +24,22 @@ import ( type Listener struct { config *Config udpConn udpConn - testHooks connTestHooks + testHooks listenerTestHooks + resetGen statelessResetTokenGenerator + retry retryState acceptQueue queue[*Conn] // new inbound connections + connsMap connsMap // only accessed by the listen loop connsMu sync.Mutex conns map[*Conn]struct{} closing bool // set when Close is called closec chan struct{} // closed when the listen loop exits +} - // The datagram receive loop keeps a mapping of connection IDs to conns. - // When a conn's connection IDs change, we add it to connIDUpdates and set - // connIDUpdateNeeded, and the receive loop updates its map. - connIDUpdateMu sync.Mutex - connIDUpdateNeeded atomic.Bool - connIDUpdates []connIDUpdate +type listenerTestHooks interface { + timeNow() time.Time + newConn(c *Conn) } // A udpConn is a UDP connection. @@ -49,12 +51,6 @@ type udpConn interface { WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) } -type connIDUpdate struct { - conn *Conn - retired bool - cid []byte -} - // Listen listens on a local network address. // The configuration config must be non-nil. func Listen(network, address string, config *Config) (*Listener, error) { @@ -69,10 +65,10 @@ func Listen(network, address string, config *Config) (*Listener, error) { if err != nil { return nil, err } - return newListener(udpConn, config, nil), nil + return newListener(udpConn, config, nil) } -func newListener(udpConn udpConn, config *Config, hooks connTestHooks) *Listener { +func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Listener, error) { l := &Listener{ config: config, udpConn: udpConn, @@ -81,8 +77,15 @@ func newListener(udpConn udpConn, config *Config, hooks connTestHooks) *Listener acceptQueue: newQueue[*Conn](), closec: make(chan struct{}), } + l.resetGen.init(config.StatelessResetKey) + l.connsMap.init() + if config.RequireAddressValidation { + if err := l.retry.init(); err != nil { + return nil, err + } + } go l.listen() - return l + return l, nil } // LocalAddr returns the local network address. @@ -104,7 +107,7 @@ func (l *Listener) Close(ctx context.Context) error { if !l.closing { l.closing = true for c := range l.conns { - c.Abort(errors.New("listener closed")) + c.Abort(localTransportError{code: errNo}) } if len(l.conns) == 0 { l.udpConn.Close() @@ -137,7 +140,7 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er } addr := u.AddrPort() addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) - c, err := l.newConn(time.Now(), clientSide, nil, addr) + c, err := l.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) if err != nil { return nil, err } @@ -148,13 +151,13 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er return c, nil } -func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort) (*Conn, error) { +func (l *Listener) newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort) (*Conn, error) { l.connsMu.Lock() defer l.connsMu.Unlock() if l.closing { return nil, errors.New("listener closed") } - c, err := newConn(now, side, initialConnID, peerAddr, l.config, l, l.testHooks) + c, err := newConn(now, side, cids, peerAddr, l.config, l) if err != nil { return nil, err } @@ -171,6 +174,22 @@ func (l *Listener) serverConnEstablished(c *Conn) { // connDrained is called by a conn when it leaves the draining state, // either when the peer acknowledges connection closure or the drain timeout expires. func (l *Listener) connDrained(c *Conn) { + var cids [][]byte + for i := range c.connIDState.local { + cids = append(cids, c.connIDState.local[i].cid) + } + var tokens []statelessResetToken + for i := range c.connIDState.remote { + tokens = append(tokens, c.connIDState.remote[i].resetToken) + } + l.connsMap.updateConnIDs(func(conns *connsMap) { + for _, cid := range cids { + conns.retireConnID(c, cid) + } + for _, token := range tokens { + conns.retireResetToken(c, token) + } + }) l.connsMu.Lock() defer l.connsMu.Unlock() delete(l.conns, c) @@ -179,39 +198,8 @@ func (l *Listener) connDrained(c *Conn) { } } -// connIDsChanged is called by a conn when its connection IDs change. -func (l *Listener) connIDsChanged(c *Conn, retired bool, cids []connID) { - l.connIDUpdateMu.Lock() - defer l.connIDUpdateMu.Unlock() - for _, cid := range cids { - l.connIDUpdates = append(l.connIDUpdates, connIDUpdate{ - conn: c, - retired: retired, - cid: cid.cid, - }) - } - l.connIDUpdateNeeded.Store(true) -} - -// updateConnIDs is called by the datagram receive loop to update its connection ID map. -func (l *Listener) updateConnIDs(conns map[string]*Conn) { - l.connIDUpdateMu.Lock() - defer l.connIDUpdateMu.Unlock() - for i, u := range l.connIDUpdates { - if u.retired { - delete(conns, string(u.cid)) - } else { - conns[string(u.cid)] = u.conn - } - l.connIDUpdates[i] = connIDUpdate{} // drop refs - } - l.connIDUpdates = l.connIDUpdates[:0] - l.connIDUpdateNeeded.Store(false) -} - func (l *Listener) listen() { defer close(l.closec) - conns := map[string]*Conn{} for { m := newDatagram() // TODO: Read and process the ECN (explicit congestion notification) field. @@ -227,22 +215,22 @@ func (l *Listener) listen() { if n == 0 { continue } - if l.connIDUpdateNeeded.Load() { - l.updateConnIDs(conns) + if l.connsMap.updateNeeded.Load() { + l.connsMap.applyUpdates() } m.addr = addr m.b = m.b[:n] - l.handleDatagram(m, conns) + l.handleDatagram(m) } } -func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) { +func (l *Listener) handleDatagram(m *datagram) { dstConnID, ok := dstConnIDForDatagram(m.b) if !ok { m.recycle() return } - c := conns[string(dstConnID)] + c := l.connsMap.byConnID[string(dstConnID)] if c == nil { // TODO: Move this branch into a separate goroutine to avoid blocking // the listener while processing packets. @@ -261,18 +249,35 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m.recycle() } }() - if len(m.b) < minimumClientInitialDatagramSize { + const minimumValidPacketSize = 21 + if len(m.b) < minimumValidPacketSize { + return + } + var now time.Time + if l.testHooks != nil { + now = l.testHooks.timeNow() + } else { + now = time.Now() + } + // Check to see if this is a stateless reset. + var token statelessResetToken + copy(token[:], m.b[len(m.b)-len(token):]) + if c := l.connsMap.byResetToken[token]; c != nil { + c.sendMsg(func(now time.Time, c *Conn) { + c.handleStatelessReset(now, token) + }) + return + } + // If this is a 1-RTT packet, there's nothing productive we can do with it. + // Send a stateless reset if possible. + if !isLongHeader(m.b[0]) { + l.maybeSendStatelessReset(m.b, m.addr) return } p, ok := parseGenericLongHeaderPacket(m.b) - if !ok { - // Not a long header packet, or not parseable. - // Short header (1-RTT) packets don't contain enough information - // to do anything useful with if we don't recognize the - // connection ID. + if !ok || len(m.b) < paddedInitialDatagramSize { return } - switch p.version { case quicVersion1: case 0: @@ -286,18 +291,27 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { if getPacketType(m.b) != packetTypeInitial { // This packet isn't trying to create a new connection. // It might be associated with some connection we've lost state for. - // TODO: Send a stateless reset when appropriate. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 + // We are technically permitted to send a stateless reset for + // a long-header packet, but this isn't generally useful. See: + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16 return } - var now time.Time - if l.testHooks != nil { - now = l.testHooks.timeNow() + cids := newServerConnIDs{ + srcConnID: p.srcConnID, + dstConnID: p.dstConnID, + } + if l.config.RequireAddressValidation { + var ok bool + cids.retrySrcConnID = p.dstConnID + cids.originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + if !ok { + return + } } else { - now = time.Now() + cids.originalDstConnID = p.dstConnID } var err error - c, err := l.newConn(now, serverSide, p.dstConnID, m.addr) + c, err := l.newConn(now, serverSide, cids, m.addr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. @@ -309,6 +323,50 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m = nil // don't recycle, sendMsg takes ownership } +func (l *Listener) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { + if !l.resetGen.canReset { + // Config.StatelessResetKey isn't set, so we don't send stateless resets. + return + } + // The smallest possible valid packet a peer can send us is: + // 1 byte of header + // connIDLen bytes of destination connection ID + // 1 byte of packet number + // 1 byte of payload + // 16 bytes AEAD expansion + if len(b) < 1+connIDLen+1+1+16 { + return + } + // TODO: Rate limit stateless resets. + cid := b[1:][:connIDLen] + token := l.resetGen.tokenForConnID(cid) + // We want to generate a stateless reset that is as short as possible, + // but long enough to be difficult to distinguish from a 1-RTT packet. + // + // The minimal 1-RTT packet is: + // 1 byte of header + // 0-20 bytes of destination connection ID + // 1-4 bytes of packet number + // 1 byte of payload + // 16 bytes AEAD expansion + // + // Assuming the maximum possible connection ID and packet number size, + // this gives 1 + 20 + 4 + 1 + 16 = 42 bytes. + // + // We also must generate a stateless reset that is shorter than the datagram + // we are responding to, in order to ensure that reset loops terminate. + // + // See: https://www.rfc-editor.org/rfc/rfc9000#section-10.3 + size := min(len(b)-1, 42) + // Reuse the input buffer for generating the stateless reset. + b = b[:size] + rand.Read(b[:len(b)-statelessResetTokenLen]) + b[0] &^= headerFormLong // clear long header bit + b[0] |= fixedBit // set fixed bit + copy(b[len(b)-statelessResetTokenLen:], token[:]) + l.sendDatagram(b, addr) +} + func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { m := newDatagram() m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1) @@ -316,7 +374,79 @@ func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPo m.recycle() } +func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) { + keys := initialKeys(in.dstConnID, serverSide) + var w packetWriter + p := longPacket{ + ptype: packetTypeInitial, + version: quicVersion1, + num: 0, + dstConnID: in.srcConnID, + srcConnID: in.dstConnID, + } + const pnumMaxAcked = 0 + w.reset(paddedInitialDatagramSize) + w.startProtectedLongHeaderPacket(pnumMaxAcked, p) + w.appendConnectionCloseTransportFrame(code, 0, "") + w.finishProtectedLongHeaderPacket(pnumMaxAcked, keys.w, p) + buf := w.datagram() + if len(buf) == 0 { + return + } + l.sendDatagram(buf, addr) +} + func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { _, err := l.udpConn.WriteToUDPAddrPort(p, addr) return err } + +// A connsMap is a listener's mapping of conn ids and reset tokens to conns. +type connsMap struct { + byConnID map[string]*Conn + byResetToken map[statelessResetToken]*Conn + + updateMu sync.Mutex + updateNeeded atomic.Bool + updates []func(*connsMap) +} + +func (m *connsMap) init() { + m.byConnID = map[string]*Conn{} + m.byResetToken = map[statelessResetToken]*Conn{} +} + +func (m *connsMap) addConnID(c *Conn, cid []byte) { + m.byConnID[string(cid)] = c +} + +func (m *connsMap) retireConnID(c *Conn, cid []byte) { + delete(m.byConnID, string(cid)) +} + +func (m *connsMap) addResetToken(c *Conn, token statelessResetToken) { + m.byResetToken[token] = c +} + +func (m *connsMap) retireResetToken(c *Conn, token statelessResetToken) { + delete(m.byResetToken, token) +} + +func (m *connsMap) updateConnIDs(f func(*connsMap)) { + m.updateMu.Lock() + defer m.updateMu.Unlock() + m.updates = append(m.updates, f) + m.updateNeeded.Store(true) +} + +// applyConnIDUpdates is called by the datagram receive loop to update its connection ID map. +func (m *connsMap) applyUpdates() { + m.updateMu.Lock() + defer m.updateMu.Unlock() + for _, f := range m.updates { + f(m) + } + clear(m.updates) + m.updates = m.updates[:0] + m.updateNeeded.Store(false) +} diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 9d0f314ec..037fb21b4 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -9,10 +9,13 @@ package quic import ( "bytes" "context" + "crypto/tls" "io" "net" "net/netip" + "reflect" "testing" + "time" ) func TestConnect(t *testing.T) { @@ -77,6 +80,8 @@ func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { t.Helper() if conf.TLSConfig == nil { + newConf := *conf + conf = &newConf conf.TLSConfig = newTestTLSConfig(side) } l, err := Listen("udp", "127.0.0.1:0", conf) @@ -90,20 +95,33 @@ func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { } type testListener struct { - t *testing.T - l *Listener - recvc chan *datagram - idlec chan struct{} - sentDatagrams [][]byte + t *testing.T + l *Listener + now time.Time + recvc chan *datagram + idlec chan struct{} + conns map[*Conn]*testConn + acceptQueue []*testConn + configTransportParams []func(*transportParameters) + configTestConn []func(*testConn) + sentDatagrams [][]byte + peerTLSConn *tls.QUICConn + lastInitialDstConnID []byte // for parsing Retry packets } -func newTestListener(t *testing.T, config *Config, testHooks connTestHooks) *testListener { +func newTestListener(t *testing.T, config *Config) *testListener { tl := &testListener{ t: t, + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), recvc: make(chan *datagram), idlec: make(chan struct{}), + conns: make(map[*Conn]*testConn), + } + var err error + tl.l, err = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + if err != nil { + t.Fatal(err) } - tl.l = newListener((*testListenerUDPConn)(tl), config, testHooks) t.Cleanup(tl.cleanup) return tl } @@ -113,7 +131,24 @@ func (tl *testListener) cleanup() { } func (tl *testListener) wait() { - tl.idlec <- struct{}{} + select { + case tl.idlec <- struct{}{}: + case <-tl.l.closec: + } + for _, tc := range tl.conns { + tc.wait() + } +} + +// accept returns a server connection from the listener. +// Unlike Listener.Accept, connections are available as soon as they are created. +func (tl *testListener) accept() *testConn { + if len(tl.acceptQueue) == 0 { + tl.t.Fatalf("accept: expected available conn, but found none") + } + tc := tl.acceptQueue[0] + tl.acceptQueue = tl.acceptQueue[1:] + return tc } func (tl *testListener) write(d *datagram) { @@ -121,7 +156,66 @@ func (tl *testListener) write(d *datagram) { tl.wait() } +var testClientAddr = netip.MustParseAddrPort("10.0.0.1:8000") + +func (tl *testListener) writeDatagram(d *testDatagram) { + tl.t.Helper() + logDatagram(tl.t, "<- listener under test receives", d) + var buf []byte + for _, p := range d.packets { + tc := tl.connForDestination(p.dstConnID) + if p.ptype != packetTypeRetry && tc != nil { + space := spaceForPacketType(p.ptype) + if p.num >= tc.peerNextPacketNum[space] { + tc.peerNextPacketNum[space] = p.num + 1 + } + } + if p.ptype == packetTypeInitial { + tl.lastInitialDstConnID = p.dstConnID + } + pad := 0 + if p.ptype == packetType1RTT { + pad = d.paddedSize - len(buf) + } + buf = append(buf, encodeTestPacket(tl.t, tc, p, pad)...) + } + for len(buf) < d.paddedSize { + buf = append(buf, 0) + } + addr := d.addr + if !addr.IsValid() { + addr = testClientAddr + } + tl.write(&datagram{ + b: buf, + addr: addr, + }) +} + +func (tl *testListener) connForDestination(dstConnID []byte) *testConn { + for _, tc := range tl.conns { + for _, loc := range tc.conn.connIDState.local { + if bytes.Equal(loc.cid, dstConnID) { + return tc + } + } + } + return nil +} + +func (tl *testListener) connForSource(srcConnID []byte) *testConn { + for _, tc := range tl.conns { + for _, loc := range tc.conn.connIDState.remote { + if bytes.Equal(loc.cid, srcConnID) { + return tc + } + } + } + return nil +} + func (tl *testListener) read() []byte { + tl.t.Helper() tl.wait() if len(tl.sentDatagrams) == 0 { return nil @@ -131,6 +225,68 @@ func (tl *testListener) read() []byte { return d } +func (tl *testListener) readDatagram() *testDatagram { + tl.t.Helper() + buf := tl.read() + if buf == nil { + return nil + } + p, _ := parseGenericLongHeaderPacket(buf) + tc := tl.connForSource(p.dstConnID) + d := parseTestDatagram(tl.t, tl, tc, buf) + logDatagram(tl.t, "-> listener under test sends", d) + return d +} + +// wantDatagram indicates that we expect the Listener to send a datagram. +func (tl *testListener) wantDatagram(expectation string, want *testDatagram) { + tl.t.Helper() + got := tl.readDatagram() + if !reflect.DeepEqual(got, want) { + tl.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) + } +} + +// wantIdle indicates that we expect the Listener to not send any more datagrams. +func (tl *testListener) wantIdle(expectation string) { + if got := tl.readDatagram(); got != nil { + tl.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, got) + } +} + +// advance causes time to pass. +func (tl *testListener) advance(d time.Duration) { + tl.t.Helper() + tl.advanceTo(tl.now.Add(d)) +} + +// advanceTo sets the current time. +func (tl *testListener) advanceTo(now time.Time) { + tl.t.Helper() + if tl.now.After(now) { + tl.t.Fatalf("time moved backwards: %v -> %v", tl.now, now) + } + tl.now = now + for _, tc := range tl.conns { + if !tc.timer.After(tl.now) { + tc.conn.sendMsg(timerEvent{}) + tc.wait() + } + } +} + +// testListenerHooks implements listenerTestHooks. +type testListenerHooks testListener + +func (tl *testListenerHooks) timeNow() time.Time { + return tl.now +} + +func (tl *testListenerHooks) newConn(c *Conn) { + tc := newTestConnForConn(tl.t, (*testListener)(tl), c) + tl.conns[c] = tc +} + // testListenerUDPConn implements UDPConn. type testListenerUDPConn testListener diff --git a/internal/quic/loss.go b/internal/quic/loss.go index 152815a29..c0f915b42 100644 --- a/internal/quic/loss.go +++ b/internal/quic/loss.go @@ -281,6 +281,19 @@ func (c *lossState) receiveAckEnd(now time.Time, space numberSpace, ackDelay tim c.cc.packetBatchEnd(now, space, &c.rtt, c.maxAckDelay) } +// discardPackets declares that packets within a number space will not be delivered +// and that data contained in them should be resent. +// For example, after receiving a Retry packet we discard already-sent Initial packets. +func (c *lossState) discardPackets(space numberSpace, lossf func(numberSpace, *sentPacket, packetFate)) { + for i := 0; i < c.spaces[space].size; i++ { + sent := c.spaces[space].nth(i) + sent.lost = true + c.cc.packetDiscarded(sent) + lossf(numberSpace(space), sent, packetLost) + } + c.spaces[space].clean() +} + // discardKeys is called when dropping packet protection keys for a number space. func (c *lossState) discardKeys(now time.Time, space numberSpace) { // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.4 diff --git a/internal/quic/packet.go b/internal/quic/packet.go index 7d69f96d2..df589ccca 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -97,6 +97,9 @@ const ( streamFinBit = 0x01 ) +// Maximum length of a connection ID. +const maxConnIDLen = 20 + // isLongHeader returns true if b is the first byte of a long header. func isLongHeader(b byte) bool { return b&headerFormLong == headerFormLong diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index ce0433902..02ef9fb14 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -47,7 +47,7 @@ func parseLongHeaderPacket(pkt []byte, k fixedKeys, pnumMax packetNumber) (p lon // Destination Connection ID Length (8), // Destination Connection ID (0..160), p.dstConnID, n = consumeUint8Bytes(b) - if n < 0 || len(p.dstConnID) > 20 { + if n < 0 || len(p.dstConnID) > maxConnIDLen { return longPacket{}, -1 } b = b[n:] @@ -55,7 +55,7 @@ func parseLongHeaderPacket(pkt []byte, k fixedKeys, pnumMax packetNumber) (p lon // Source Connection ID Length (8), // Source Connection ID (0..160), p.srcConnID, n = consumeUint8Bytes(b) - if n < 0 || len(p.dstConnID) > 20 { + if n < 0 || len(p.dstConnID) > maxConnIDLen { return longPacket{}, -1 } b = b[n:] @@ -420,32 +420,32 @@ func consumeStreamsBlockedFrame(b []byte) (typ streamType, max int64, n int) { return typ, max, n } -func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, resetToken [16]byte, n int) { +func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, resetToken statelessResetToken, n int) { n = 1 var nn int seq, nn = consumeVarintInt64(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn retire, nn = consumeVarintInt64(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn if seq < retire { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } connID, nn = consumeVarintBytes(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } if len(connID) < 1 || len(connID) > 20 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn if len(b[n:]) < len(resetToken) { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } copy(resetToken[:], b[n:]) n += len(resetToken) diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 7b141ac49..1f939f491 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -441,7 +441,7 @@ func (k *updatingKeyPair) unprotect(pkt []byte, pnumOff int, pnumMax packetNumbe if err != nil { k.authFailures++ if k.authFailures >= aeadIntegrityLimit(k.r.suite) { - return nil, 0, localTransportError(errAEADLimitReached) + return nil, 0, localTransportError{code: errAEADLimitReached} } return nil, 0, err } diff --git a/internal/quic/pipe.go b/internal/quic/pipe.go index 978a4f3d8..d3a448df3 100644 --- a/internal/quic/pipe.go +++ b/internal/quic/pipe.go @@ -146,4 +146,5 @@ func (p *pipe) discardBefore(off int64) { p.tail = nil } p.start = off + p.end = max(p.end, off) } diff --git a/internal/quic/pipe_test.go b/internal/quic/pipe_test.go index 7a05ff4d4..bcb3a8bc0 100644 --- a/internal/quic/pipe_test.go +++ b/internal/quic/pipe_test.go @@ -61,6 +61,12 @@ func TestPipeWrites(t *testing.T) { discardBeforeOp{10000}, writeOp{10000, 20000}, }, + }, { + desc: "discard before writing", + ops: []op{ + discardBeforeOp{1000}, + writeOp{0, 1}, + }, }} { var p pipe var wantset rangeset[int64] @@ -78,6 +84,9 @@ func TestPipeWrites(t *testing.T) { p.discardBefore(o.off) wantset.sub(0, o.off) wantStart = o.off + if o.off > wantEnd { + wantEnd = o.off + } } if p.start != wantStart || p.end != wantEnd { t.Errorf("%v: after %#v p contains [%v,%v), want [%v,%v)", test.desc, test.ops[:i+1], p.start, p.end, wantStart, wantEnd) diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 9de97b6d8..084887be6 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -58,9 +58,10 @@ const ( // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2-6 const timerGranularity = 1 * time.Millisecond -// Minimum size of a UDP datagram sent by a client carrying an Initial packet. +// Minimum size of a UDP datagram sent by a client carrying an Initial packet, +// or a server containing an ack-eliciting Initial packet. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 -const minimumClientInitialDatagramSize = 1200 +const paddedInitialDatagramSize = 1200 // Maximum number of streams of a given type which may be created. // https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 diff --git a/internal/quic/retry.go b/internal/quic/retry.go new file mode 100644 index 000000000..e3d9f4d7d --- /dev/null +++ b/internal/quic/retry.go @@ -0,0 +1,235 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "net/netip" + "time" + + "golang.org/x/crypto/chacha20poly1305" +) + +// AEAD and nonce used to compute the Retry Integrity Tag. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.8 +var ( + retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e} + retryNonce = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb} + retryAEAD = func() cipher.AEAD { + c, err := aes.NewCipher(retrySecret) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(c) + if err != nil { + panic(err) + } + return aead + }() +) + +// retryTokenValidityPeriod is how long we accept a Retry packet token after sending it. +const retryTokenValidityPeriod = 5 * time.Second + +// retryState generates and validates a listener's retry tokens. +type retryState struct { + aead cipher.AEAD +} + +func (rs *retryState) init() error { + // Retry tokens are authenticated using a per-server key chosen at start time. + // TODO: Provide a way for the user to set this key. + secret := make([]byte, chacha20poly1305.KeySize) + if _, err := rand.Read(secret); err != nil { + return err + } + aead, err := chacha20poly1305.NewX(secret) + if err != nil { + panic(err) + } + rs.aead = aead + return nil +} + +// Retry tokens are encrypted with an AEAD. +// The plaintext contains the time the token was created and +// the original destination connection ID. +// The additional data contains the sender's source address and original source connection ID. +// The token nonce is randomly generated. +// We use the nonce as the Source Connection ID of the Retry packet. +// Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID, +// we include the remaining 4 bytes of nonce in the token. +// +// Token { +// Last 4 Bytes of Nonce (32), +// Ciphertext (..), +// } +// +// Plaintext { +// Timestamp (64), +// Original Destination Connection ID, +// } +// +// +// Additional Data { +// Original Source Connection ID Length (8), +// Original Source Connection ID (..), +// IP Address (32..128), +// Port (16), +// } +// +// TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it. + +func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) { + nonce := make([]byte, rs.aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, nil, err + } + + var plaintext []byte + plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix())) + plaintext = append(plaintext, origDstConnID...) + + token = append(token, nonce[maxConnIDLen:]...) + token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr)) + return token, nonce[:maxConnIDLen], nil +} + +func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) { + tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen + if len(token) < tokenNonceLen { + return nil, false + } + nonce := append([]byte{}, dstConnID...) + nonce = append(nonce, token[:tokenNonceLen]...) + ciphertext := token[tokenNonceLen:] + + plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr)) + if err != nil { + return nil, false + } + if len(plaintext) < 8 { + return nil, false + } + when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0) + origDstConnID = plaintext[8:] + + // We allow for tokens created in the future (up to the validity period), + // which likely indicates that the system clock was adjusted backwards. + if d := abs(now.Sub(when)); d > retryTokenValidityPeriod { + return nil, false + } + + return origDstConnID, true +} + +func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte { + var additional []byte + additional = appendUint8Bytes(additional, srcConnID) + additional = append(additional, addr.Addr().AsSlice()...) + additional = binary.BigEndian.AppendUint16(additional, addr.Port()) + return additional +} + +func (l *Listener) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { + // The retry token is at the start of an Initial packet's data. + token, n := consumeUint8Bytes(p.data) + if n < 0 { + // We've already validated that the packet is at least 1200 bytes long, + // so there's no way for even a maximum size token to not fit. + // Check anyway. + return nil, false + } + if len(token) == 0 { + // The sender has not provided a token. + // Send a Retry packet to them with one. + l.sendRetry(now, p, addr) + return nil, false + } + origDstConnID, ok = l.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) + if !ok { + // This does not seem to be a valid token. + // Close the connection with an INVALID_TOKEN error. + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 + l.sendConnectionClose(p, addr, errInvalidToken) + return nil, false + } + return origDstConnID, true +} + +func (l *Listener) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { + token, srcConnID, err := l.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) + if err != nil { + return + } + b := encodeRetryPacket(p.dstConnID, retryPacket{ + dstConnID: p.srcConnID, + srcConnID: srcConnID, + token: token, + }) + l.sendDatagram(b, addr) +} + +type retryPacket struct { + dstConnID []byte + srcConnID []byte + token []byte +} + +func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte { + // Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over + // the original destination connection ID followed by the Retry packet + // (less the integrity tag itself). + // https://www.rfc-editor.org/rfc/rfc9001#section-5.8 + // + // Create the pseudo-packet (including the original DCID), append the tag, + // and return the Retry packet. + var b []byte + b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID + start := len(b) // start of the Retry packet + b = append(b, headerFormLong|fixedBit|longPacketTypeRetry) + b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version + b = appendUint8Bytes(b, p.dstConnID) // Destination Connection ID + b = appendUint8Bytes(b, p.srcConnID) // Source Connection ID + b = append(b, p.token...) // Token + b = retryAEAD.Seal(b, retryNonce, nil, b) // Retry Integrity Tag + return b[start:] +} + +func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) { + const retryIntegrityTagLength = 128 / 8 + + lp, ok := parseGenericLongHeaderPacket(b) + if !ok { + return retryPacket{}, false + } + if len(lp.data) < retryIntegrityTagLength { + return retryPacket{}, false + } + gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:] + + // Create the pseudo-packet consisting of the original destination connection ID + // followed by the Retry packet (less the integrity tag). + // Use this to validate the packet integrity tag. + pseudo := appendUint8Bytes(nil, origDstConnID) + pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...) + wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo) + if !bytes.Equal(gotTag, wantTag) { + return retryPacket{}, false + } + + token := lp.data[:len(lp.data)-retryIntegrityTagLength] + return retryPacket{ + dstConnID: lp.dstConnID, + srcConnID: lp.srcConnID, + token: token, + }, true +} diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go new file mode 100644 index 000000000..f754270a5 --- /dev/null +++ b/internal/quic/retry_test.go @@ -0,0 +1,568 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "bytes" + "context" + "crypto/tls" + "net/netip" + "testing" + "time" +) + +type retryServerTest struct { + tl *testListener + originalSrcConnID []byte + originalDstConnID []byte + retry retryPacket + initialCrypto []byte +} + +// newRetryServerTest creates a test server connection, +// sends the connection an Initial packet, +// and expects a Retry in response. +func newRetryServerTest(t *testing.T) *retryServerTest { + t.Helper() + config := &Config{ + TLSConfig: newTestTLSConfig(serverSide), + RequireAddressValidation: true, + } + tl := newTestListener(t, config) + srcID := testPeerConnID(0) + dstID := testLocalConnID(-1) + params := defaultTransportParameters() + params.initialSrcConnID = srcID + initialCrypto := initialClientCrypto(t, tl, params) + + // Initial packet with no Token. + // Server responds with a Retry containing a token. + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: quicVersion1, + srcConnID: srcID, + dstConnID: dstID, + frames: []debugFrame{ + debugFrameCrypto{ + data: initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + got := tl.readDatagram() + if len(got.packets) != 1 || got.packets[0].ptype != packetTypeRetry { + t.Fatalf("got datagram: %v\nwant Retry", got) + } + p := got.packets[0] + if got, want := p.dstConnID, srcID; !bytes.Equal(got, want) { + t.Fatalf("Retry destination = {%x}, want {%x}", got, want) + } + + return &retryServerTest{ + tl: tl, + originalSrcConnID: srcID, + originalDstConnID: dstID, + retry: retryPacket{ + dstConnID: p.dstConnID, + srcConnID: p.srcConnID, + token: p.token, + }, + initialCrypto: initialCrypto, + } +} + +func TestRetryServerSucceeds(t *testing.T) { + rt := newRetryServerTest(t) + tl := rt.tl + tl.advance(retryTokenValidityPeriod) + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tc := tl.accept() + initial := tc.readPacket() + if initial == nil || initial.ptype != packetTypeInitial { + t.Fatalf("got packet:\n%v\nwant: Initial", initial) + } + handshake := tc.readPacket() + if handshake == nil || handshake.ptype != packetTypeHandshake { + t.Fatalf("got packet:\n%v\nwant: Handshake", initial) + } + if got, want := tc.sentTransportParameters.retrySrcConnID, rt.retry.srcConnID; !bytes.Equal(got, want) { + t.Errorf("retry_source_connection_id = {%x}, want {%x}", got, want) + } + if got, want := tc.sentTransportParameters.initialSrcConnID, initial.srcConnID; !bytes.Equal(got, want) { + t.Errorf("initial_source_connection_id = {%x}, want {%x}", got, want) + } + if got, want := tc.sentTransportParameters.originalDstConnID, rt.originalDstConnID; !bytes.Equal(got, want) { + t.Errorf("original_destination_connection_id = {%x}, want {%x}", got, want) + } +} + +func TestRetryServerTokenInvalid(t *testing.T) { + // "If a server receives a client Initial that contains an invalid Retry token [...] + // the server SHOULD immediately close [...] the connection with an + // INVALID_TOKEN error." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 + rt := newRetryServerTest(t) + tl := rt.tl + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: append(rt.retry.token, 0), + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tl.wantDatagram("server closes connection after Initial with invalid Retry token", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerTokenTooOld(t *testing.T) { + // "[...] a token SHOULD have an expiration time [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3 + rt := newRetryServerTest(t) + tl := rt.tl + tl.advance(retryTokenValidityPeriod + time.Second) + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tl.wantDatagram("server closes connection after Initial with expired token", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerTokenWrongIP(t *testing.T) { + // "Tokens sent in Retry packets SHOULD include information that allows the server + // to verify that the source IP address and port in client packets remain constant." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3 + rt := newRetryServerTest(t) + tl := rt.tl + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + addr: netip.MustParseAddrPort("10.0.0.2:8000"), + }) + tl.wantDatagram("server closes connection after Initial from wrong address", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerIgnoresRetry(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(0), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + }) + // Send two packets, to trigger an immediate ACK. + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantFrameType("server connection ignores spurious Retry packet", + packetType1RTT, debugFrameAck{}) +} + +func TestRetryClientSuccess(t *testing.T) { + // "This token MUST be repeated by the client in all Initial packets it sends + // for that connection after it receives the Retry packet." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1 + tc := newTestConn(t, clientSide) + tc.wantFrame("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + newServerConnID := []byte("new_conn_id") + token := []byte("token") + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: newServerConnID, + dstConnID: testLocalConnID(0), + token: token, + }}, + }) + tc.wantPacket("client sends a new Initial packet with a token", + &testPacket{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: testLocalConnID(0), + dstConnID: newServerConnID, + token: token, + frames: []debugFrame{ + debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }, + }, + }, + ) + tc.advanceToTimer() + tc.wantPacket("after PTO client sends another Initial packet with a token", + &testPacket{ + ptype: packetTypeInitial, + num: 2, + version: quicVersion1, + srcConnID: testLocalConnID(0), + dstConnID: newServerConnID, + token: token, + frames: []debugFrame{ + debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }, + }, + }, + ) +} + +func TestRetryClientInvalidServerTransportParameters(t *testing.T) { + // Various permutations of missing or invalid values for transport parameters + // after a Retry. + // https://www.rfc-editor.org/rfc/rfc9000#section-7.3 + initialSrcConnID := testPeerConnID(0) + originalDstConnID := testLocalConnID(-1) + retrySrcConnID := testPeerConnID(100) + for _, test := range []struct { + name string + f func(*transportParameters) + ok bool + }{{ + name: "valid", + f: func(p *transportParameters) {}, + ok: true, + }, { + name: "missing initial_source_connection_id", + f: func(p *transportParameters) { + p.initialSrcConnID = nil + }, + }, { + name: "invalid initial_source_connection_id", + f: func(p *transportParameters) { + p.initialSrcConnID = []byte("invalid") + }, + }, { + name: "missing original_destination_connection_id", + f: func(p *transportParameters) { + p.originalDstConnID = nil + }, + }, { + name: "invalid original_destination_connection_id", + f: func(p *transportParameters) { + p.originalDstConnID = []byte("invalid") + }, + }, { + name: "missing retry_source_connection_id", + f: func(p *transportParameters) { + p.retrySrcConnID = nil + }, + }, { + name: "invalid retry_source_connection_id", + f: func(p *transportParameters) { + p.retrySrcConnID = []byte("invalid") + }, + }} { + t.Run(test.name, func(t *testing.T) { + tc := newTestConn(t, clientSide, + func(p *transportParameters) { + p.initialSrcConnID = initialSrcConnID + p.originalDstConnID = originalDstConnID + p.retrySrcConnID = retrySrcConnID + }, + test.f) + tc.ignoreFrame(frameTypeAck) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: originalDstConnID, + srcConnID: retrySrcConnID, + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + }) + tc.wantFrameType("client resends Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + if test.ok { + tc.wantFrameType("valid params, client sends Handshake", + packetTypeHandshake, debugFrameCrypto{}) + } else { + tc.wantFrame("invalid transport parameters", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }) + } + }) + } +} + +func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) { + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeNewConnectionID) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + retry := &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + } + tc.write(retry) + tc.wantIdle("client ignores Retry after receiving Initial packet") + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrameType("client Handshake CRYPTO data", + packetTypeHandshake, debugFrameCrypto{}) + tc.write(retry) + tc.wantIdle("client ignores Retry after discarding Initial keys") +} + +func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) { + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + retry := &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + } + tc.write(retry) + tc.wantFrameType("client resends Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(retry) + tc.wantIdle("client ignores second Retry") +} + +func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + pkt := encodeRetryPacket(testLocalConnID(-1), retryPacket{ + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }) + pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag + tc.listener.write(&datagram{ + b: pkt, + addr: testClientAddr, + }) + tc.wantIdle("client ignores Retry with invalid integrity tag") +} + +func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) { + // "A client MUST discard a Retry packet with a zero-length Retry Token field." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{}, + }}, + }) + tc.wantIdle("client ignores Retry with zero-length token") +} + +func TestRetryStateValidateInvalidToken(t *testing.T) { + // Test handling of tokens that may have a valid signature, + // but unexpected contents. + var rs retryState + if err := rs.init(); err != nil { + t.Fatal(err) + } + nonce := make([]byte, rs.aead.NonceSize()) + now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + srcConnID := []byte{1, 2, 3, 4} + dstConnID := nonce[:20] + addr := testClientAddr + + for _, test := range []struct { + name string + token []byte + }{{ + name: "token too short", + token: []byte{1, 2, 3}, + }, { + name: "token plaintext too short", + token: func() []byte { + plaintext := make([]byte, 7) // not enough bytes of content + token := append([]byte{}, nonce[20:]...) + return rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr)) + }(), + }} { + t.Run(test.name, func(t *testing.T) { + if _, ok := rs.validateToken(now, test.token, srcConnID, dstConnID, addr); ok { + t.Errorf("validateToken succeeded, want failure") + } + }) + } +} + +func TestParseInvalidRetryPackets(t *testing.T) { + originalDstConnID := []byte{1, 2, 3, 4} + goodPkt := encodeRetryPacket(originalDstConnID, retryPacket{ + dstConnID: []byte{1}, + srcConnID: []byte{2}, + token: []byte{3}, + }) + for _, test := range []struct { + name string + pkt []byte + }{{ + name: "packet too short", + pkt: goodPkt[:len(goodPkt)-4], + }, { + name: "packet header invalid", + pkt: goodPkt[:5], + }, { + name: "integrity tag invalid", + pkt: func() []byte { + pkt := cloneBytes(goodPkt) + pkt[len(pkt)-1] ^= 1 + return pkt + }(), + }} { + t.Run(test.name, func(t *testing.T) { + if _, ok := parseRetryPacket(test.pkt, originalDstConnID); ok { + t.Errorf("parseRetryPacket succeded, want failure") + } + }) + } +} + +func initialClientCrypto(t *testing.T, l *testListener, p transportParameters) []byte { + t.Helper() + config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} + tlsClient := tls.QUICClient(config) + tlsClient.SetTransportParameters(marshalTransportParameters(p)) + tlsClient.Start(context.Background()) + //defer tlsClient.Close() + l.peerTLSConn = tlsClient + var data []byte + for { + e := tlsClient.NextEvent() + switch e.Kind { + case tls.QUICNoEvent: + return data + case tls.QUICWriteData: + if e.Level != tls.QUICEncryptionLevelInitial { + t.Fatal("initial data at unexpected level") + } + data = append(data, e.Data...) + } + } +} + +func initialConnectionCloseDatagram(srcConnID, dstConnID []byte, code transportError) *testDatagram { + return &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: quicVersion1, + srcConnID: srcConnID, + dstConnID: dstConnID, + frames: []debugFrame{ + debugFrameConnectionCloseTransport{ + code: code, + }, + }, + }}, + } +} diff --git a/internal/quic/stateless_reset.go b/internal/quic/stateless_reset.go new file mode 100644 index 000000000..53c3ba539 --- /dev/null +++ b/internal/quic/stateless_reset.go @@ -0,0 +1,61 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "hash" + "sync" +) + +const statelessResetTokenLen = 128 / 8 + +// A statelessResetToken is a stateless reset token. +// https://www.rfc-editor.org/rfc/rfc9000#section-10.3 +type statelessResetToken [statelessResetTokenLen]byte + +type statelessResetTokenGenerator struct { + canReset bool + + // The hash.Hash interface is not concurrency safe, + // so we need a mutex here. + // + // There shouldn't be much contention on stateless reset token generation. + // If this proves to be a problem, we could avoid the mutex by using a separate + // generator per Conn, or by using a concurrency-safe generator. + mu sync.Mutex + mac hash.Hash +} + +func (g *statelessResetTokenGenerator) init(secret [32]byte) { + zero := true + for _, b := range secret { + if b != 0 { + zero = false + break + } + } + if zero { + // Generate tokens using a random secret, but don't send stateless resets. + rand.Read(secret[:]) + g.canReset = false + } else { + g.canReset = true + } + g.mac = hmac.New(sha256.New, secret[:]) +} + +func (g *statelessResetTokenGenerator) tokenForConnID(cid []byte) (token statelessResetToken) { + g.mu.Lock() + defer g.mu.Unlock() + defer g.mac.Reset() + g.mac.Write(cid) + copy(token[:], g.mac.Sum(nil)) + return token +} diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go new file mode 100644 index 000000000..8a16597c4 --- /dev/null +++ b/internal/quic/stateless_reset_test.go @@ -0,0 +1,280 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/tls" + "errors" + "net/netip" + "testing" + "time" +) + +func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { + // "[The stateless_reset_token] transport parameter MUST NOT be sent by a client [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.6.1 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("client provided stateless_reset_token transport parameter", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }) +} + +var testStatelessResetKey = func() (key [32]byte) { + if _, err := rand.Read(key[:]); err != nil { + panic(err) + } + return key +}() + +func testStatelessResetToken(cid []byte) statelessResetToken { + var gen statelessResetTokenGenerator + gen.init(testStatelessResetKey) + return gen.tokenForConnID(cid) +} + +func testLocalStatelessResetToken(seq int64) statelessResetToken { + return testStatelessResetToken(testLocalConnID(seq)) +} + +func newDatagramForReset(cid []byte, size int, addr netip.AddrPort) *datagram { + dgram := append([]byte{headerFormShort | fixedBit}, cid...) + for len(dgram) < size { + dgram = append(dgram, byte(len(dgram))) // semi-random junk + } + return &datagram{ + b: dgram, + addr: addr, + } +} + +func TestStatelessResetSentSizes(t *testing.T) { + config := &Config{ + TLSConfig: newTestTLSConfig(serverSide), + StatelessResetKey: testStatelessResetKey, + } + addr := netip.MustParseAddr("127.0.0.1") + tl := newTestListener(t, config) + for i, test := range []struct { + reqSize int + wantSize int + }{{ + // Datagrams larger than 42 bytes result in a 42-byte stateless reset. + // This isn't specifically mandated by RFC 9000, but is implied. + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 + reqSize: 1200, + wantSize: 42, + }, { + // "An endpoint that sends a Stateless Reset in response to a packet + // that is 43 bytes or shorter SHOULD send a Stateless Reset that is + // one byte shorter than the packet it responds to." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 + reqSize: 43, + wantSize: 42, + }, { + reqSize: 42, + wantSize: 41, + }, { + // We should send a stateless reset in response to the smallest possible + // valid datagram the peer can send us. + // The smallest packet is 1-RTT: + // header byte, conn id, packet num, payload, AEAD. + reqSize: 1 + connIDLen + 1 + 1 + 16, + wantSize: 1 + connIDLen + 1 + 1 + 16 - 1, + }, { + // The smallest possible stateless reset datagram is 21 bytes. + // Since our response must be smaller than the incoming datagram, + // we must not respond to a 21 byte or smaller packet. + reqSize: 21, + wantSize: 0, + }} { + cid := testLocalConnID(int64(i)) + token := testStatelessResetToken(cid) + addrport := netip.AddrPortFrom(addr, uint16(8000+i)) + tl.write(newDatagramForReset(cid, test.reqSize, addrport)) + + got := tl.read() + if len(got) != test.wantSize { + t.Errorf("got %v-byte response to %v-byte req, want %v", + len(got), test.reqSize, test.wantSize) + } + if len(got) == 0 { + continue + } + // "Endpoints MUST send Stateless Resets formatted as + // a packet with a short header." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 + if isLongHeader(got[0]) { + t.Errorf("response to %v-byte request is not a short-header packet\ngot: %x", test.reqSize, got) + } + if !bytes.HasSuffix(got, token[:]) { + t.Errorf("response to %v-byte request does not end in stateless reset token\ngot: %x\nwant suffix: %x", test.reqSize, got, token) + } + } +} + +func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { + // "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Retire connection ID 0. + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + retirePriorTo: 1, + seq: 2, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer requested we retire conn id 0", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + + resetToken := testPeerStatelessResetToken(1) // provided during handshake + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + tc.wantIdle("closed connection is idle in draining") + tc.advance(1 * time.Second) // long enough to exit the draining state + tc.wantIdle("closed connection is idle after draining") +} + +func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { + // "[...] Stateless Reset Token field values from [...] + // the server's transport parameters [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + tc.wantIdle("closed connection is idle") +} + +func TestStatelessResetSuccessfulPrefix(t *testing.T) { + for _, test := range []struct { + name string + prefix []byte + size int + }{{ + name: "short header and fixed bit", + prefix: []byte{ + headerFormShort | fixedBit, + }, + size: 100, + }, { + // "[...] endpoints MUST treat [long header packets] ending in a + // valid stateless reset token as a Stateless Reset [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 + name: "long header no fixed bit", + prefix: []byte{ + headerFormLong, + }, + size: 100, + }, { + // "[...] the comparison MUST be performed when the first packet + // in an incoming datagram [...] cannot be decrypted." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-2 + name: "short header valid DCID", + prefix: append([]byte{ + headerFormShort | fixedBit, + }, testLocalConnID(0)...), + size: 100, + }, { + name: "handshake valid DCID", + prefix: append([]byte{ + headerFormLong | fixedBit | longPacketTypeHandshake, + }, testLocalConnID(0)...), + size: 100, + }, { + name: "no fixed bit valid DCID", + prefix: append([]byte{ + 0, + }, testLocalConnID(0)...), + size: 100, + }} { + t.Run(test.name, func(t *testing.T) { + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + + dgram := test.prefix + for len(dgram) < test.size-len(resetToken) { + dgram = append(dgram, byte(len(dgram))) // semi-random junk + } + dgram = append(dgram, resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + }) + } +} + +func TestStatelessResetRetiredConnID(t *testing.T) { + // "An endpoint MUST NOT check for any stateless reset tokens [...] + // for connection IDs that have been retired." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-3 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // We retire connection ID 0. + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + + // Receive a stateless reset for connection ID 0. + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, context.Canceled) { + t.Errorf("conn.Wait() = %v, want connection to be alive", err) + } +} diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 89036b19b..58d84ed1b 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -567,19 +567,31 @@ func (s *Stream) handleReset(code uint64, finalSize int64) error { func (s *Stream) checkStreamBounds(end int64, fin bool) error { if end > s.inwin { // The peer sent us data past the maximum flow control window we gave them. - return localTransportError(errFlowControl) + return localTransportError{ + code: errFlowControl, + reason: "stream flow control window exceeded", + } } if s.insize != -1 && end > s.insize { // The peer sent us data past the final size of the stream they previously gave us. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "data received past end of stream", + } } if fin && s.insize != -1 && end != s.insize { // The peer changed the final size of the stream. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "final size of stream changed", + } } if fin && end < s.in.end { // The peer has previously sent us data past the final size. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "end of stream occurs before prior data", + } } return nil } diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go index 6eda7883b..2f42cf418 100644 --- a/internal/quic/stream_limits.go +++ b/internal/quic/stream_limits.go @@ -66,7 +66,10 @@ func (lim *remoteStreamLimits) init(maxOpen int64) { func (lim *remoteStreamLimits) open(id streamID) error { num := id.num() if num >= lim.max { - return localTransportError(errStreamLimit) + return localTransportError{ + code: errStreamLimit, + reason: "stream limit exceeded", + } } if num >= lim.opened { lim.opened = num + 1 diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 7c1377fae..9bf2b5871 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -13,7 +13,6 @@ import ( "errors" "fmt" "io" - "reflect" "strings" "testing" ) @@ -848,7 +847,7 @@ func TestStreamOffsetTooLarge(t *testing.T) { got, _ := tc.readFrame() want1 := debugFrameConnectionCloseTransport{code: errFrameEncoding} want2 := debugFrameConnectionCloseTransport{code: errFlowControl} - if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) { + if !frameEqual(got, want1) && !frameEqual(got, want2) { t.Fatalf("STREAM offset exceeds 2^62-1\ngot: %v\nwant: %v\n or: %v", got, want1, want2) } } diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 81d17b858..fa339b9fa 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -36,7 +36,7 @@ func (tc *testConn) handshake() { for { if i == len(dgrams)-1 { if tc.conn.side == clientSide { - want := tc.now.Add(maxAckDelay - timerGranularity) + want := tc.listener.now.Add(maxAckDelay - timerGranularity) if !tc.timer.Equal(want) { t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer) } @@ -71,9 +71,11 @@ func (tc *testConn) handshake() { func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { var ( - clientConnIDs [][]byte - serverConnIDs [][]byte - transientConnID []byte + clientConnIDs [][]byte + serverConnIDs [][]byte + clientResetToken statelessResetToken + serverResetToken statelessResetToken + transientConnID []byte ) localConnIDs := [][]byte{ testLocalConnID(0), @@ -83,14 +85,20 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { testPeerConnID(0), testPeerConnID(1), } + localResetToken := tc.listener.l.resetGen.tokenForConnID(localConnIDs[1]) + peerResetToken := testPeerStatelessResetToken(1) if tc.conn.side == clientSide { clientConnIDs = localConnIDs serverConnIDs = peerConnIDs + clientResetToken = localResetToken + serverResetToken = peerResetToken transientConnID = testLocalConnID(-1) } else { clientConnIDs = peerConnIDs serverConnIDs = localConnIDs - transientConnID = []byte{0xde, 0xad, 0xbe, 0xef} + clientResetToken = peerResetToken + serverResetToken = localResetToken + transientConnID = testPeerConnID(-1) } return []*testDatagram{{ // Client Initial @@ -136,9 +144,11 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: serverConnIDs[1], + token: serverResetToken, }, }, }}, + paddedSize: 1200, }, { // Client Initial + Handshake + 1-RTT packets: []*testPacket{{ @@ -175,6 +185,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: clientConnIDs[1], + token: clientResetToken, }, }, }}, @@ -337,6 +348,7 @@ func TestConnKeysDiscardedClient(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) // The client discards Initial keys after sending a Handshake packet. @@ -390,6 +402,7 @@ func TestConnKeysDiscardedServer(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.wantIdle("server has discarded Initial keys, cannot read CONNECTION_CLOSE") @@ -546,7 +559,9 @@ func TestConnAEADLimitReached(t *testing.T) { // exceeds the integrity limit for the selected AEAD, // the endpoint MUST immediately close the connection [...]" // https://www.rfc-editor.org/rfc/rfc9001#section-6.6-6 - tc := newTestConn(t, clientSide) + tc := newTestConn(t, clientSide, func(c *Config) { + clear(c.StatelessResetKey[:]) + }) tc.handshake() var limit int64 @@ -564,7 +579,7 @@ func TestConnAEADLimitReached(t *testing.T) { // Only use the transient connection ID in Initial packets. dstConnID = tc.conn.connIDState.local[1].cid } - invalid := tc.encodeTestPacket(&testPacket{ + invalid := encodeTestPacket(t, tc, &testPacket{ ptype: packetType1RTT, num: 1000, frames: []debugFrame{debugFramePing{}}, diff --git a/internal/quic/transport_params.go b/internal/quic/transport_params.go index dc76d1650..3cc56f4e4 100644 --- a/internal/quic/transport_params.go +++ b/internal/quic/transport_params.go @@ -169,12 +169,12 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { for len(params) > 0 { id, n := consumeVarint(params) if n < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } params = params[n:] val, n := consumeVarintBytes(params) if n < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } params = params[n:] n = 0 @@ -193,14 +193,14 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { p.maxIdleTimeout = time.Duration(v) * time.Millisecond case paramStatelessResetToken: if len(val) != 16 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.statelessResetToken = val n = 16 case paramMaxUDPPayloadSize: p.maxUDPPayloadSize, n = consumeVarintInt64(val) if p.maxUDPPayloadSize < 1200 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialMaxData: p.initialMaxData, n = consumeVarintInt64(val) @@ -213,32 +213,32 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { case paramInitialMaxStreamsBidi: p.initialMaxStreamsBidi, n = consumeVarintInt64(val) if p.initialMaxStreamsBidi > maxStreamsLimit { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialMaxStreamsUni: p.initialMaxStreamsUni, n = consumeVarintInt64(val) if p.initialMaxStreamsUni > maxStreamsLimit { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramAckDelayExponent: var v uint64 v, n = consumeVarint(val) if v > 20 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.ackDelayExponent = int8(v) case paramMaxAckDelay: var v uint64 v, n = consumeVarint(val) if v >= 1<<14 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.maxAckDelay = time.Duration(v) * time.Millisecond case paramDisableActiveMigration: p.disableActiveMigration = true case paramPreferredAddress: if len(val) < 4+2+16+2+1 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.preferredAddrV4 = netip.AddrPortFrom( netip.AddrFrom4(*(*[4]byte)(val[:4])), @@ -253,18 +253,18 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { var nn int p.preferredAddrConnID, nn = consumeUint8Bytes(val) if nn < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } val = val[nn:] if len(val) != 16 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.preferredAddrResetToken = val val = nil case paramActiveConnectionIDLimit: p.activeConnIDLimit, n = consumeVarintInt64(val) if p.activeConnIDLimit < 2 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialSourceConnectionID: p.initialSrcConnID = val @@ -276,7 +276,7 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { n = len(val) } if n != len(val) { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } } return p, nil diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go index cfb7ce4be..830e0e1c8 100644 --- a/internal/quic/version_test.go +++ b/internal/quic/version_test.go @@ -17,7 +17,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { config := &Config{ TLSConfig: newTestTLSConfig(serverSide), } - tl := newTestListener(t, config, nil) + tl := newTestListener(t, config) // Packet of unknown contents for some unrecognized QUIC version. dstConnID := []byte{1, 2, 3, 4} @@ -30,7 +30,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { pkt = append(pkt, dstConnID...) pkt = append(pkt, byte(len(srcConnID))) pkt = append(pkt, srcConnID...) - for len(pkt) < minimumClientInitialDatagramSize { + for len(pkt) < paddedInitialDatagramSize { pkt = append(pkt, 0) } diff --git a/internal/socket/cmsghdr.go b/internal/socket/cmsghdr.go index 4bdaaaf1a..33a5bf59c 100644 --- a/internal/socket/cmsghdr.go +++ b/internal/socket/cmsghdr.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/cmsghdr_bsd.go b/internal/socket/cmsghdr_bsd.go index 0d30e0a0f..68f438c84 100644 --- a/internal/socket/cmsghdr_bsd.go +++ b/internal/socket/cmsghdr_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package socket diff --git a/internal/socket/cmsghdr_linux_32bit.go b/internal/socket/cmsghdr_linux_32bit.go index 4936e8a6f..058ea8de8 100644 --- a/internal/socket/cmsghdr_linux_32bit.go +++ b/internal/socket/cmsghdr_linux_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && linux -// +build arm mips mipsle 386 ppc -// +build linux package socket diff --git a/internal/socket/cmsghdr_linux_64bit.go b/internal/socket/cmsghdr_linux_64bit.go index f6877f98f..3ca0d3a0a 100644 --- a/internal/socket/cmsghdr_linux_64bit.go +++ b/internal/socket/cmsghdr_linux_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && linux -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build linux package socket diff --git a/internal/socket/cmsghdr_solaris_64bit.go b/internal/socket/cmsghdr_solaris_64bit.go index d3dbe1b8e..6d0e426cd 100644 --- a/internal/socket/cmsghdr_solaris_64bit.go +++ b/internal/socket/cmsghdr_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/cmsghdr_stub.go b/internal/socket/cmsghdr_stub.go index 1d9f2ed62..7ca9cb7e7 100644 --- a/internal/socket/cmsghdr_stub.go +++ b/internal/socket/cmsghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/cmsghdr_unix.go b/internal/socket/cmsghdr_unix.go index 19d46789d..0211f225b 100644 --- a/internal/socket/cmsghdr_unix.go +++ b/internal/socket/cmsghdr_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/complete_dontwait.go b/internal/socket/complete_dontwait.go index 5b1d50ae7..2038f2904 100644 --- a/internal/socket/complete_dontwait.go +++ b/internal/socket/complete_dontwait.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux netbsd openbsd solaris package socket diff --git a/internal/socket/complete_nodontwait.go b/internal/socket/complete_nodontwait.go index be6340958..70e6f448b 100644 --- a/internal/socket/complete_nodontwait.go +++ b/internal/socket/complete_nodontwait.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || windows || zos -// +build aix windows zos package socket diff --git a/internal/socket/defs_aix.go b/internal/socket/defs_aix.go index 0bc1703ca..2c847bbeb 100644 --- a/internal/socket/defs_aix.go +++ b/internal/socket/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_darwin.go b/internal/socket/defs_darwin.go index 0f07b5725..d94fff755 100644 --- a/internal/socket/defs_darwin.go +++ b/internal/socket/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_dragonfly.go b/internal/socket/defs_dragonfly.go index 0f07b5725..d94fff755 100644 --- a/internal/socket/defs_dragonfly.go +++ b/internal/socket/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_freebsd.go b/internal/socket/defs_freebsd.go index 0f07b5725..d94fff755 100644 --- a/internal/socket/defs_freebsd.go +++ b/internal/socket/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_linux.go b/internal/socket/defs_linux.go index bbaafdf30..d0d52bdfb 100644 --- a/internal/socket/defs_linux.go +++ b/internal/socket/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_netbsd.go b/internal/socket/defs_netbsd.go index 5b57b0c42..8db525bf4 100644 --- a/internal/socket/defs_netbsd.go +++ b/internal/socket/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_openbsd.go b/internal/socket/defs_openbsd.go index 0f07b5725..d94fff755 100644 --- a/internal/socket/defs_openbsd.go +++ b/internal/socket/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_solaris.go b/internal/socket/defs_solaris.go index 0f07b5725..d94fff755 100644 --- a/internal/socket/defs_solaris.go +++ b/internal/socket/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/empty.s b/internal/socket/empty.s index 90ab4ca3d..49d79791e 100644 --- a/internal/socket/empty.s +++ b/internal/socket/empty.s @@ -3,6 +3,5 @@ // license that can be found in the LICENSE file. //go:build darwin && go1.12 -// +build darwin,go1.12 // This exists solely so we can linkname in symbols from syscall. diff --git a/internal/socket/error_unix.go b/internal/socket/error_unix.go index 78f412904..7a5cc5c43 100644 --- a/internal/socket/error_unix.go +++ b/internal/socket/error_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/iovec_32bit.go b/internal/socket/iovec_32bit.go index 2b8fbb3f3..340e53fbd 100644 --- a/internal/socket/iovec_32bit.go +++ b/internal/socket/iovec_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && (darwin || dragonfly || freebsd || linux || netbsd || openbsd) -// +build arm mips mipsle 386 ppc -// +build darwin dragonfly freebsd linux netbsd openbsd package socket diff --git a/internal/socket/iovec_64bit.go b/internal/socket/iovec_64bit.go index 2e94e96f8..26470c191 100644 --- a/internal/socket/iovec_64bit.go +++ b/internal/socket/iovec_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || zos) -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build aix darwin dragonfly freebsd linux netbsd openbsd zos package socket diff --git a/internal/socket/iovec_solaris_64bit.go b/internal/socket/iovec_solaris_64bit.go index f7da2bc4d..8859ce103 100644 --- a/internal/socket/iovec_solaris_64bit.go +++ b/internal/socket/iovec_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/iovec_stub.go b/internal/socket/iovec_stub.go index 14caf5248..da886b032 100644 --- a/internal/socket/iovec_stub.go +++ b/internal/socket/iovec_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/mmsghdr_stub.go b/internal/socket/mmsghdr_stub.go index 113e773cd..4825b21e3 100644 --- a/internal/socket/mmsghdr_stub.go +++ b/internal/socket/mmsghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !linux && !netbsd -// +build !aix,!linux,!netbsd package socket diff --git a/internal/socket/mmsghdr_unix.go b/internal/socket/mmsghdr_unix.go index 41883c530..311fd2c78 100644 --- a/internal/socket/mmsghdr_unix.go +++ b/internal/socket/mmsghdr_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || linux || netbsd -// +build aix linux netbsd package socket diff --git a/internal/socket/msghdr_bsd.go b/internal/socket/msghdr_bsd.go index 25f6847f9..ebff4f6e0 100644 --- a/internal/socket/msghdr_bsd.go +++ b/internal/socket/msghdr_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package socket diff --git a/internal/socket/msghdr_bsdvar.go b/internal/socket/msghdr_bsdvar.go index 5b8e00f1c..62e6fe861 100644 --- a/internal/socket/msghdr_bsdvar.go +++ b/internal/socket/msghdr_bsdvar.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd -// +build aix darwin dragonfly freebsd netbsd package socket diff --git a/internal/socket/msghdr_linux_32bit.go b/internal/socket/msghdr_linux_32bit.go index b4658fbae..3dd07250a 100644 --- a/internal/socket/msghdr_linux_32bit.go +++ b/internal/socket/msghdr_linux_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && linux -// +build arm mips mipsle 386 ppc -// +build linux package socket diff --git a/internal/socket/msghdr_linux_64bit.go b/internal/socket/msghdr_linux_64bit.go index 42411affa..5af9ddd6a 100644 --- a/internal/socket/msghdr_linux_64bit.go +++ b/internal/socket/msghdr_linux_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && linux -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build linux package socket diff --git a/internal/socket/msghdr_solaris_64bit.go b/internal/socket/msghdr_solaris_64bit.go index 3098f5d78..e212b50f8 100644 --- a/internal/socket/msghdr_solaris_64bit.go +++ b/internal/socket/msghdr_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/msghdr_stub.go b/internal/socket/msghdr_stub.go index eb79151f6..e87677645 100644 --- a/internal/socket/msghdr_stub.go +++ b/internal/socket/msghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/msghdr_zos_s390x.go b/internal/socket/msghdr_zos_s390x.go index 324e9ee7d..529db68ee 100644 --- a/internal/socket/msghdr_zos_s390x.go +++ b/internal/socket/msghdr_zos_s390x.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build s390x && zos -// +build s390x,zos package socket diff --git a/internal/socket/norace.go b/internal/socket/norace.go index de0ad420f..8af30ecfb 100644 --- a/internal/socket/norace.go +++ b/internal/socket/norace.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !race -// +build !race package socket diff --git a/internal/socket/race.go b/internal/socket/race.go index f0a28a625..9afa95808 100644 --- a/internal/socket/race.go +++ b/internal/socket/race.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build race -// +build race package socket diff --git a/internal/socket/rawconn_mmsg.go b/internal/socket/rawconn_mmsg.go index 8f79b38f7..043139078 100644 --- a/internal/socket/rawconn_mmsg.go +++ b/internal/socket/rawconn_mmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package socket diff --git a/internal/socket/rawconn_msg.go b/internal/socket/rawconn_msg.go index f7d0b0d2b..7c0d7410b 100644 --- a/internal/socket/rawconn_msg.go +++ b/internal/socket/rawconn_msg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket diff --git a/internal/socket/rawconn_nommsg.go b/internal/socket/rawconn_nommsg.go index 02f328556..e363fb5a8 100644 --- a/internal/socket/rawconn_nommsg.go +++ b/internal/socket/rawconn_nommsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package socket diff --git a/internal/socket/rawconn_nomsg.go b/internal/socket/rawconn_nomsg.go index dd785877b..ff7a8baf0 100644 --- a/internal/socket/rawconn_nomsg.go +++ b/internal/socket/rawconn_nomsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package socket diff --git a/internal/socket/socket_dontwait_test.go b/internal/socket/socket_dontwait_test.go index 8eab9900b..1eb3580f6 100644 --- a/internal/socket/socket_dontwait_test.go +++ b/internal/socket/socket_dontwait_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux netbsd openbsd solaris package socket_test diff --git a/internal/socket/socket_test.go b/internal/socket/socket_test.go index 84907d8bc..faba10606 100644 --- a/internal/socket/socket_test.go +++ b/internal/socket/socket_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket_test diff --git a/internal/socket/sys_bsd.go b/internal/socket/sys_bsd.go index b258879d4..e7664d48b 100644 --- a/internal/socket/sys_bsd.go +++ b/internal/socket/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || openbsd || solaris -// +build aix darwin dragonfly freebsd openbsd solaris package socket diff --git a/internal/socket/sys_const_unix.go b/internal/socket/sys_const_unix.go index 5d99f2373..d7627f87e 100644 --- a/internal/socket/sys_const_unix.go +++ b/internal/socket/sys_const_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/sys_linux.go b/internal/socket/sys_linux.go index 76f5b8ae5..08d491077 100644 --- a/internal/socket/sys_linux.go +++ b/internal/socket/sys_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux && !s390x && !386 -// +build linux,!s390x,!386 package socket diff --git a/internal/socket/sys_linux_loong64.go b/internal/socket/sys_linux_loong64.go index af964e617..1d182470d 100644 --- a/internal/socket/sys_linux_loong64.go +++ b/internal/socket/sys_linux_loong64.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build loong64 -// +build loong64 package socket diff --git a/internal/socket/sys_linux_riscv64.go b/internal/socket/sys_linux_riscv64.go index 5b128fbb2..0e407d125 100644 --- a/internal/socket/sys_linux_riscv64.go +++ b/internal/socket/sys_linux_riscv64.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build riscv64 -// +build riscv64 package socket diff --git a/internal/socket/sys_posix.go b/internal/socket/sys_posix.go index 42b8f2340..58d865482 100644 --- a/internal/socket/sys_posix.go +++ b/internal/socket/sys_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket diff --git a/internal/socket/sys_stub.go b/internal/socket/sys_stub.go index 7cfb349c0..2e5b473c6 100644 --- a/internal/socket/sys_stub.go +++ b/internal/socket/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package socket diff --git a/internal/socket/sys_unix.go b/internal/socket/sys_unix.go index de823932b..93058db5b 100644 --- a/internal/socket/sys_unix.go +++ b/internal/socket/sys_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package socket diff --git a/internal/socket/zsys_aix_ppc64.go b/internal/socket/zsys_aix_ppc64.go index 00691bd52..45bab004c 100644 --- a/internal/socket/zsys_aix_ppc64.go +++ b/internal/socket/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package socket diff --git a/internal/socket/zsys_linux_loong64.go b/internal/socket/zsys_linux_loong64.go index 6a94fec2c..b6fc15a1a 100644 --- a/internal/socket/zsys_linux_loong64.go +++ b/internal/socket/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package socket diff --git a/internal/socket/zsys_linux_riscv64.go b/internal/socket/zsys_linux_riscv64.go index c066272dd..e67fc3cba 100644 --- a/internal/socket/zsys_linux_riscv64.go +++ b/internal/socket/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package socket diff --git a/ipv4/control_bsd.go b/ipv4/control_bsd.go index b7385dfd9..c88da8cbe 100644 --- a/ipv4/control_bsd.go +++ b/ipv4/control_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package ipv4 diff --git a/ipv4/control_pktinfo.go b/ipv4/control_pktinfo.go index 0e748dbdc..14ae2dae4 100644 --- a/ipv4/control_pktinfo.go +++ b/ipv4/control_pktinfo.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || linux || solaris -// +build darwin linux solaris package ipv4 diff --git a/ipv4/control_stub.go b/ipv4/control_stub.go index f27322c3e..3ba661160 100644 --- a/ipv4/control_stub.go +++ b/ipv4/control_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/control_unix.go b/ipv4/control_unix.go index 2413e02f8..2e765548f 100644 --- a/ipv4/control_unix.go +++ b/ipv4/control_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv4 diff --git a/ipv4/defs_aix.go b/ipv4/defs_aix.go index b70b61824..5e590a7df 100644 --- a/ipv4/defs_aix.go +++ b/ipv4/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_darwin.go b/ipv4/defs_darwin.go index 0ceadfce2..2494ff86a 100644 --- a/ipv4/defs_darwin.go +++ b/ipv4/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_dragonfly.go b/ipv4/defs_dragonfly.go index a84630c5c..43e9f67bb 100644 --- a/ipv4/defs_dragonfly.go +++ b/ipv4/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_freebsd.go b/ipv4/defs_freebsd.go index b068087a4..05899b3b4 100644 --- a/ipv4/defs_freebsd.go +++ b/ipv4/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_linux.go b/ipv4/defs_linux.go index 7c8554d4b..fc869b019 100644 --- a/ipv4/defs_linux.go +++ b/ipv4/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_netbsd.go b/ipv4/defs_netbsd.go index a84630c5c..43e9f67bb 100644 --- a/ipv4/defs_netbsd.go +++ b/ipv4/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_openbsd.go b/ipv4/defs_openbsd.go index a84630c5c..43e9f67bb 100644 --- a/ipv4/defs_openbsd.go +++ b/ipv4/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_solaris.go b/ipv4/defs_solaris.go index 0ceadfce2..2494ff86a 100644 --- a/ipv4/defs_solaris.go +++ b/ipv4/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/errors_other_test.go b/ipv4/errors_other_test.go index 615435391..93a7f9d74 100644 --- a/ipv4/errors_other_test.go +++ b/ipv4/errors_other_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !(aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris package ipv4_test diff --git a/ipv4/errors_unix_test.go b/ipv4/errors_unix_test.go index 566e070a5..7cff0097c 100644 --- a/ipv4/errors_unix_test.go +++ b/ipv4/errors_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv4_test diff --git a/ipv4/gen.go b/ipv4/gen.go index e7b053a17..121c7643e 100644 --- a/ipv4/gen.go +++ b/ipv4/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/ipv4/helper_posix_test.go b/ipv4/helper_posix_test.go index 4f6ecc0fd..ab8ffd90d 100644 --- a/ipv4/helper_posix_test.go +++ b/ipv4/helper_posix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv4_test diff --git a/ipv4/helper_stub_test.go b/ipv4/helper_stub_test.go index e47ddf7f3..791e6d4c0 100644 --- a/ipv4/helper_stub_test.go +++ b/ipv4/helper_stub_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4_test diff --git a/ipv4/icmp_stub.go b/ipv4/icmp_stub.go index cd4ee6e1c..c2c4ce7ff 100644 --- a/ipv4/icmp_stub.go +++ b/ipv4/icmp_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv4 diff --git a/ipv4/payload_cmsg.go b/ipv4/payload_cmsg.go index 1bb370e25..91c685e8f 100644 --- a/ipv4/payload_cmsg.go +++ b/ipv4/payload_cmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv4 diff --git a/ipv4/payload_nocmsg.go b/ipv4/payload_nocmsg.go index 53f0794eb..2afd4b50e 100644 --- a/ipv4/payload_nocmsg.go +++ b/ipv4/payload_nocmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package ipv4 diff --git a/ipv4/sockopt_posix.go b/ipv4/sockopt_posix.go index eb07c1c02..82e2c3783 100644 --- a/ipv4/sockopt_posix.go +++ b/ipv4/sockopt_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv4 diff --git a/ipv4/sockopt_stub.go b/ipv4/sockopt_stub.go index cf036893b..840108bf7 100644 --- a/ipv4/sockopt_stub.go +++ b/ipv4/sockopt_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/sys_aix.go b/ipv4/sys_aix.go index 02730cdfd..9244a68a3 100644 --- a/ipv4/sys_aix.go +++ b/ipv4/sys_aix.go @@ -4,7 +4,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv4 diff --git a/ipv4/sys_asmreq.go b/ipv4/sys_asmreq.go index 22322b387..645f254c6 100644 --- a/ipv4/sys_asmreq.go +++ b/ipv4/sys_asmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd netbsd openbsd solaris windows package ipv4 diff --git a/ipv4/sys_asmreq_stub.go b/ipv4/sys_asmreq_stub.go index fde640142..48cfb6db2 100644 --- a/ipv4/sys_asmreq_stub.go +++ b/ipv4/sys_asmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!windows package ipv4 diff --git a/ipv4/sys_asmreqn.go b/ipv4/sys_asmreqn.go index 54eb9901b..0b27b632f 100644 --- a/ipv4/sys_asmreqn.go +++ b/ipv4/sys_asmreqn.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || freebsd || linux -// +build darwin freebsd linux package ipv4 diff --git a/ipv4/sys_asmreqn_stub.go b/ipv4/sys_asmreqn_stub.go index dcb15f25a..303a5e2e6 100644 --- a/ipv4/sys_asmreqn_stub.go +++ b/ipv4/sys_asmreqn_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !darwin && !freebsd && !linux -// +build !darwin,!freebsd,!linux package ipv4 diff --git a/ipv4/sys_bpf.go b/ipv4/sys_bpf.go index fb11e324e..1b4780df4 100644 --- a/ipv4/sys_bpf.go +++ b/ipv4/sys_bpf.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package ipv4 diff --git a/ipv4/sys_bpf_stub.go b/ipv4/sys_bpf_stub.go index fc53a0d33..b1f779b49 100644 --- a/ipv4/sys_bpf_stub.go +++ b/ipv4/sys_bpf_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv4 diff --git a/ipv4/sys_bsd.go b/ipv4/sys_bsd.go index e191b2f14..b7b032d26 100644 --- a/ipv4/sys_bsd.go +++ b/ipv4/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build netbsd || openbsd -// +build netbsd openbsd package ipv4 diff --git a/ipv4/sys_ssmreq.go b/ipv4/sys_ssmreq.go index 6a4e7abf9..a295e15ea 100644 --- a/ipv4/sys_ssmreq.go +++ b/ipv4/sys_ssmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || freebsd || linux || solaris -// +build darwin freebsd linux solaris package ipv4 diff --git a/ipv4/sys_ssmreq_stub.go b/ipv4/sys_ssmreq_stub.go index 157159fd5..74bd454e2 100644 --- a/ipv4/sys_ssmreq_stub.go +++ b/ipv4/sys_ssmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !darwin && !freebsd && !linux && !solaris -// +build !darwin,!freebsd,!linux,!solaris package ipv4 diff --git a/ipv4/sys_stub.go b/ipv4/sys_stub.go index d55085165..20af4074c 100644 --- a/ipv4/sys_stub.go +++ b/ipv4/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/zsys_aix_ppc64.go b/ipv4/zsys_aix_ppc64.go index b7f2d6e5c..dd454025c 100644 --- a/ipv4/zsys_aix_ppc64.go +++ b/ipv4/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv4 diff --git a/ipv4/zsys_linux_loong64.go b/ipv4/zsys_linux_loong64.go index e15c22c74..54f9e1394 100644 --- a/ipv4/zsys_linux_loong64.go +++ b/ipv4/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package ipv4 diff --git a/ipv4/zsys_linux_riscv64.go b/ipv4/zsys_linux_riscv64.go index e2edebdb8..78374a525 100644 --- a/ipv4/zsys_linux_riscv64.go +++ b/ipv4/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package ipv4 diff --git a/ipv6/control_rfc2292_unix.go b/ipv6/control_rfc2292_unix.go index 2733ddbe2..a8f04e7b3 100644 --- a/ipv6/control_rfc2292_unix.go +++ b/ipv6/control_rfc2292_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin -// +build darwin package ipv6 diff --git a/ipv6/control_rfc3542_unix.go b/ipv6/control_rfc3542_unix.go index 9c90844aa..51fbbb1f1 100644 --- a/ipv6/control_rfc3542_unix.go +++ b/ipv6/control_rfc3542_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/control_stub.go b/ipv6/control_stub.go index b7e8643fc..eb28ce753 100644 --- a/ipv6/control_stub.go +++ b/ipv6/control_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/control_unix.go b/ipv6/control_unix.go index 63e475db8..9c73b8647 100644 --- a/ipv6/control_unix.go +++ b/ipv6/control_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/defs_aix.go b/ipv6/defs_aix.go index 97db07e8d..de171ce2c 100644 --- a/ipv6/defs_aix.go +++ b/ipv6/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_darwin.go b/ipv6/defs_darwin.go index 1d31e22c1..3b9e6ba64 100644 --- a/ipv6/defs_darwin.go +++ b/ipv6/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_dragonfly.go b/ipv6/defs_dragonfly.go index ddaed6597..b40d34b13 100644 --- a/ipv6/defs_dragonfly.go +++ b/ipv6/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_freebsd.go b/ipv6/defs_freebsd.go index 6f6bc6dbc..fe9a0f70f 100644 --- a/ipv6/defs_freebsd.go +++ b/ipv6/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_linux.go b/ipv6/defs_linux.go index 0adcbd92d..b947c225a 100644 --- a/ipv6/defs_linux.go +++ b/ipv6/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_netbsd.go b/ipv6/defs_netbsd.go index ddaed6597..b40d34b13 100644 --- a/ipv6/defs_netbsd.go +++ b/ipv6/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_openbsd.go b/ipv6/defs_openbsd.go index ddaed6597..b40d34b13 100644 --- a/ipv6/defs_openbsd.go +++ b/ipv6/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_solaris.go b/ipv6/defs_solaris.go index 03193da9b..7981a0452 100644 --- a/ipv6/defs_solaris.go +++ b/ipv6/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/errors_other_test.go b/ipv6/errors_other_test.go index 5a87d7361..5f6c0cb27 100644 --- a/ipv6/errors_other_test.go +++ b/ipv6/errors_other_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !(aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris package ipv6_test diff --git a/ipv6/errors_unix_test.go b/ipv6/errors_unix_test.go index 978ae61f8..9e8efd313 100644 --- a/ipv6/errors_unix_test.go +++ b/ipv6/errors_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv6_test diff --git a/ipv6/gen.go b/ipv6/gen.go index bd53468eb..2973dff5c 100644 --- a/ipv6/gen.go +++ b/ipv6/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/ipv6/helper_posix_test.go b/ipv6/helper_posix_test.go index 8ca6a3c3c..f412a78cb 100644 --- a/ipv6/helper_posix_test.go +++ b/ipv6/helper_posix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv6_test diff --git a/ipv6/helper_stub_test.go b/ipv6/helper_stub_test.go index 15e99fa94..9412a4cf5 100644 --- a/ipv6/helper_stub_test.go +++ b/ipv6/helper_stub_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6_test diff --git a/ipv6/helper_unix_test.go b/ipv6/helper_unix_test.go index 5ccff9d9b..c2459e320 100644 --- a/ipv6/helper_unix_test.go +++ b/ipv6/helper_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6_test diff --git a/ipv6/icmp_bsd.go b/ipv6/icmp_bsd.go index 120bf8775..2814534a0 100644 --- a/ipv6/icmp_bsd.go +++ b/ipv6/icmp_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package ipv6 diff --git a/ipv6/icmp_stub.go b/ipv6/icmp_stub.go index d60136a90..c92c9b51e 100644 --- a/ipv6/icmp_stub.go +++ b/ipv6/icmp_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/payload_cmsg.go b/ipv6/payload_cmsg.go index b0692e430..be04e4d6a 100644 --- a/ipv6/payload_cmsg.go +++ b/ipv6/payload_cmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/payload_nocmsg.go b/ipv6/payload_nocmsg.go index cd0ff5083..29b9ccf69 100644 --- a/ipv6/payload_nocmsg.go +++ b/ipv6/payload_nocmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package ipv6 diff --git a/ipv6/sockopt_posix.go b/ipv6/sockopt_posix.go index 37c628713..34dfed588 100644 --- a/ipv6/sockopt_posix.go +++ b/ipv6/sockopt_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv6 diff --git a/ipv6/sockopt_stub.go b/ipv6/sockopt_stub.go index 32fd8664c..a09c3aaf2 100644 --- a/ipv6/sockopt_stub.go +++ b/ipv6/sockopt_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/sys_aix.go b/ipv6/sys_aix.go index a47182afb..93c8efc46 100644 --- a/ipv6/sys_aix.go +++ b/ipv6/sys_aix.go @@ -4,7 +4,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv6 diff --git a/ipv6/sys_asmreq.go b/ipv6/sys_asmreq.go index 6ff9950d1..5c9cb4447 100644 --- a/ipv6/sys_asmreq.go +++ b/ipv6/sys_asmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package ipv6 diff --git a/ipv6/sys_asmreq_stub.go b/ipv6/sys_asmreq_stub.go index 485290cb8..dc7049468 100644 --- a/ipv6/sys_asmreq_stub.go +++ b/ipv6/sys_asmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows package ipv6 diff --git a/ipv6/sys_bpf.go b/ipv6/sys_bpf.go index b5661fb8f..e39f75f49 100644 --- a/ipv6/sys_bpf.go +++ b/ipv6/sys_bpf.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package ipv6 diff --git a/ipv6/sys_bpf_stub.go b/ipv6/sys_bpf_stub.go index cb0066187..8532a8f5d 100644 --- a/ipv6/sys_bpf_stub.go +++ b/ipv6/sys_bpf_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv6 diff --git a/ipv6/sys_bsd.go b/ipv6/sys_bsd.go index bde41a6ce..9f3bc2afd 100644 --- a/ipv6/sys_bsd.go +++ b/ipv6/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build dragonfly || netbsd || openbsd -// +build dragonfly netbsd openbsd package ipv6 diff --git a/ipv6/sys_ssmreq.go b/ipv6/sys_ssmreq.go index 023488a49..b40f5c685 100644 --- a/ipv6/sys_ssmreq.go +++ b/ipv6/sys_ssmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || freebsd || linux || solaris || zos -// +build aix darwin freebsd linux solaris zos package ipv6 diff --git a/ipv6/sys_ssmreq_stub.go b/ipv6/sys_ssmreq_stub.go index acdf2e5cf..6526aad58 100644 --- a/ipv6/sys_ssmreq_stub.go +++ b/ipv6/sys_ssmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !freebsd && !linux && !solaris && !zos -// +build !aix,!darwin,!freebsd,!linux,!solaris,!zos package ipv6 diff --git a/ipv6/sys_stub.go b/ipv6/sys_stub.go index 5807bba39..76602c34e 100644 --- a/ipv6/sys_stub.go +++ b/ipv6/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/zsys_aix_ppc64.go b/ipv6/zsys_aix_ppc64.go index f604b0f3b..668716df4 100644 --- a/ipv6/zsys_aix_ppc64.go +++ b/ipv6/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv6 diff --git a/ipv6/zsys_linux_loong64.go b/ipv6/zsys_linux_loong64.go index 598fbfa06..6a53284db 100644 --- a/ipv6/zsys_linux_loong64.go +++ b/ipv6/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package ipv6 diff --git a/ipv6/zsys_linux_riscv64.go b/ipv6/zsys_linux_riscv64.go index d4f78e405..13b347205 100644 --- a/ipv6/zsys_linux_riscv64.go +++ b/ipv6/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package ipv6 diff --git a/lif/address.go b/lif/address.go index 8eaddb508..0ed62a2c4 100644 --- a/lif/address.go +++ b/lif/address.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/address_test.go b/lif/address_test.go index fdaa7f3aa..0e99b8d34 100644 --- a/lif/address_test.go +++ b/lif/address_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/binary.go b/lif/binary.go index f31ca3ad0..8a6c45606 100644 --- a/lif/binary.go +++ b/lif/binary.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/defs_solaris.go b/lif/defs_solaris.go index dbed7c86e..6bc8fa8e6 100644 --- a/lif/defs_solaris.go +++ b/lif/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/lif/lif.go b/lif/lif.go index f1fce48b3..e9f2a9e0e 100644 --- a/lif/lif.go +++ b/lif/lif.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris // Package lif provides basic functions for the manipulation of // logical network interfaces and interface addresses on Solaris. diff --git a/lif/link.go b/lif/link.go index 00b78545b..d0c615a0b 100644 --- a/lif/link.go +++ b/lif/link.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/link_test.go b/lif/link_test.go index 40b3f3ff2..fe56697f8 100644 --- a/lif/link_test.go +++ b/lif/link_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/sys.go b/lif/sys.go index d0b532d9d..caba2fe90 100644 --- a/lif/sys.go +++ b/lif/sys.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/syscall.go b/lif/syscall.go index 8d03b4aa9..329a65fe6 100644 --- a/lif/syscall.go +++ b/lif/syscall.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/nettest/conntest_test.go b/nettest/conntest_test.go index 7c5aeb9b3..c57e64004 100644 --- a/nettest/conntest_test.go +++ b/nettest/conntest_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.8 -// +build go1.8 package nettest diff --git a/nettest/nettest_stub.go b/nettest/nettest_stub.go index 6e3a9312b..1725b6aa1 100644 --- a/nettest/nettest_stub.go +++ b/nettest/nettest_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package nettest diff --git a/nettest/nettest_unix.go b/nettest/nettest_unix.go index b1cb8b2f3..9ba269d02 100644 --- a/nettest/nettest_unix.go +++ b/nettest/nettest_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package nettest diff --git a/publicsuffix/gen.go b/publicsuffix/gen.go index 2ad0abdc1..21c191415 100644 --- a/publicsuffix/gen.go +++ b/publicsuffix/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package main diff --git a/route/address.go b/route/address.go index 5a3cc0654..5443d6722 100644 --- a/route/address.go +++ b/route/address.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/address_test.go b/route/address_test.go index bd7db4a1f..31087576e 100644 --- a/route/address_test.go +++ b/route/address_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/binary.go b/route/binary.go index a5e28f1e9..db3f7e0c2 100644 --- a/route/binary.go +++ b/route/binary.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/defs_darwin.go b/route/defs_darwin.go index 8da584571..ec56ca02e 100644 --- a/route/defs_darwin.go +++ b/route/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_dragonfly.go b/route/defs_dragonfly.go index acf3d1c55..9bf202dda 100644 --- a/route/defs_dragonfly.go +++ b/route/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_freebsd.go b/route/defs_freebsd.go index 3f115121b..abb2dc095 100644 --- a/route/defs_freebsd.go +++ b/route/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_netbsd.go b/route/defs_netbsd.go index c4304df84..8e89934c5 100644 --- a/route/defs_netbsd.go +++ b/route/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_openbsd.go b/route/defs_openbsd.go index 9af0e1af5..8f3218bc6 100644 --- a/route/defs_openbsd.go +++ b/route/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/empty.s b/route/empty.s index 90ab4ca3d..49d79791e 100644 --- a/route/empty.s +++ b/route/empty.s @@ -3,6 +3,5 @@ // license that can be found in the LICENSE file. //go:build darwin && go1.12 -// +build darwin,go1.12 // This exists solely so we can linkname in symbols from syscall. diff --git a/route/interface.go b/route/interface.go index 9e9407830..0aa70555c 100644 --- a/route/interface.go +++ b/route/interface.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/interface_announce.go b/route/interface_announce.go index 8282bfe9e..70614c1b1 100644 --- a/route/interface_announce.go +++ b/route/interface_announce.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build dragonfly || freebsd || netbsd -// +build dragonfly freebsd netbsd package route diff --git a/route/interface_classic.go b/route/interface_classic.go index 903a19634..be1bf2652 100644 --- a/route/interface_classic.go +++ b/route/interface_classic.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || netbsd -// +build darwin dragonfly netbsd package route diff --git a/route/interface_multicast.go b/route/interface_multicast.go index dd0b214ba..2ee37b9c7 100644 --- a/route/interface_multicast.go +++ b/route/interface_multicast.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd -// +build darwin dragonfly freebsd package route diff --git a/route/message.go b/route/message.go index 456a8363f..dc8bfc5b3 100644 --- a/route/message.go +++ b/route/message.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/message_test.go b/route/message_test.go index 61927d62c..9381f1b2d 100644 --- a/route/message_test.go +++ b/route/message_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/route.go b/route/route.go index 3ab5bcdc0..ca2ce2b88 100644 --- a/route/route.go +++ b/route/route.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd // Package route provides basic functions for the manipulation of // packet routing facilities on BSD variants. diff --git a/route/route_classic.go b/route/route_classic.go index d6ee42f1b..e273fe39a 100644 --- a/route/route_classic.go +++ b/route/route_classic.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd -// +build darwin dragonfly freebsd netbsd package route diff --git a/route/route_test.go b/route/route_test.go index 55c8f2372..ba5770217 100644 --- a/route/route_test.go +++ b/route/route_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/sys.go b/route/sys.go index 7c75574f1..fcebee58e 100644 --- a/route/sys.go +++ b/route/sys.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/syscall.go b/route/syscall.go index 68d37c962..0ed53750a 100644 --- a/route/syscall.go +++ b/route/syscall.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/webdav/litmus_test_server.go b/webdav/litmus_test_server.go index 6334d7e23..4d49072c4 100644 --- a/webdav/litmus_test_server.go +++ b/webdav/litmus_test_server.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore /* This program is a server for the WebDAV 'litmus' compliance test at