From 92728b3b3c00280fd64b9ac7641d5e81f39afc4c Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Tue, 10 Oct 2023 15:15:25 +0000 Subject: [PATCH 01/22] dns/dnsmessage: document that Skip does not fully validate the header The Skip methods do not fully validate the name in header, the compression pointers are not followed Change-Id: If34a041d799a22117afac9bd23e76606f5d0c8f7 GitHub-Last-Rev: f8f2586fb2528308f0b130c64cc7c13ca7820607 GitHub-Pull-Request: golang/net#196 Reviewed-on: https://go-review.googlesource.com/c/net/+/534175 LUCI-TryBot-Result: Go LUCI Reviewed-by: Ian Lance Taylor Reviewed-by: Damien Neil Auto-Submit: Ian Lance Taylor --- dns/dnsmessage/message.go | 9 +++++++++ 1 file changed, 9 insertions(+) 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) } From d23d9bc549229fd1a9d375dc91141fcf1385d257 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 10 Oct 2023 13:49:18 -0400 Subject: [PATCH 02/22] all: update go directive to 1.18 Done with: go get go@1.18 go mod tidy go fix ./... Using go1.21.3. With a manual change to keep golang.org/x/net/context testing itself, not context in the standard library. For golang/go#60268. Change-Id: I00682bf7cf1e3ba4370e2a3e7f63dc245b294a36 Reviewed-on: https://go-review.googlesource.com/c/net/+/534241 LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Damien Neil Auto-Submit: Dmitri Shuralyov --- context/context_test.go | 1 - context/ctxhttp/ctxhttp_test.go | 1 - context/go17.go | 1 - context/go19.go | 1 - context/pre_go17.go | 1 - context/pre_go19.go | 1 - go.mod | 2 +- go.sum | 34 ------------------------ html/atom/gen.go | 1 - http/httpproxy/go19_test.go | 1 - http2/go111.go | 1 - http2/go115.go | 1 - http2/go118.go | 1 - http2/h2i/h2i.go | 1 - http2/hpack/gen.go | 1 - http2/not_go111.go | 1 - http2/not_go115.go | 1 - http2/not_go118.go | 1 - http2/transport_go117_test.go | 1 - icmp/helper_posix.go | 1 - icmp/listen_posix.go | 1 - icmp/listen_stub.go | 1 - idna/go118.go | 1 - idna/idna10.0.0.go | 1 - idna/idna9.0.0.go | 1 - idna/pre_go118.go | 1 - idna/tables10.0.0.go | 1 - idna/tables11.0.0.go | 1 - idna/tables12.0.0.go | 1 - idna/tables13.0.0.go | 1 - idna/tables15.0.0.go | 1 - idna/tables9.0.0.go | 1 - idna/trie12.0.0.go | 1 - idna/trie13.0.0.go | 1 - internal/iana/gen.go | 1 - internal/socket/cmsghdr.go | 1 - internal/socket/cmsghdr_bsd.go | 1 - internal/socket/cmsghdr_linux_32bit.go | 2 -- internal/socket/cmsghdr_linux_64bit.go | 2 -- internal/socket/cmsghdr_solaris_64bit.go | 1 - internal/socket/cmsghdr_stub.go | 1 - internal/socket/cmsghdr_unix.go | 1 - internal/socket/complete_dontwait.go | 1 - internal/socket/complete_nodontwait.go | 1 - internal/socket/defs_aix.go | 1 - internal/socket/defs_darwin.go | 1 - internal/socket/defs_dragonfly.go | 1 - internal/socket/defs_freebsd.go | 1 - internal/socket/defs_linux.go | 1 - internal/socket/defs_netbsd.go | 1 - internal/socket/defs_openbsd.go | 1 - internal/socket/defs_solaris.go | 1 - internal/socket/empty.s | 1 - internal/socket/error_unix.go | 1 - internal/socket/iovec_32bit.go | 2 -- internal/socket/iovec_64bit.go | 2 -- internal/socket/iovec_solaris_64bit.go | 1 - internal/socket/iovec_stub.go | 1 - internal/socket/mmsghdr_stub.go | 1 - internal/socket/mmsghdr_unix.go | 1 - internal/socket/msghdr_bsd.go | 1 - internal/socket/msghdr_bsdvar.go | 1 - internal/socket/msghdr_linux_32bit.go | 2 -- internal/socket/msghdr_linux_64bit.go | 2 -- internal/socket/msghdr_solaris_64bit.go | 1 - internal/socket/msghdr_stub.go | 1 - internal/socket/msghdr_zos_s390x.go | 1 - internal/socket/norace.go | 1 - internal/socket/race.go | 1 - internal/socket/rawconn_mmsg.go | 1 - internal/socket/rawconn_msg.go | 1 - internal/socket/rawconn_nommsg.go | 1 - internal/socket/rawconn_nomsg.go | 1 - internal/socket/socket_dontwait_test.go | 1 - internal/socket/socket_test.go | 1 - internal/socket/sys_bsd.go | 1 - internal/socket/sys_const_unix.go | 1 - internal/socket/sys_linux.go | 1 - internal/socket/sys_linux_loong64.go | 1 - internal/socket/sys_linux_riscv64.go | 1 - internal/socket/sys_posix.go | 1 - internal/socket/sys_stub.go | 1 - internal/socket/sys_unix.go | 1 - internal/socket/zsys_aix_ppc64.go | 1 - internal/socket/zsys_linux_loong64.go | 1 - internal/socket/zsys_linux_riscv64.go | 1 - ipv4/control_bsd.go | 1 - ipv4/control_pktinfo.go | 1 - ipv4/control_stub.go | 1 - ipv4/control_unix.go | 1 - ipv4/defs_aix.go | 1 - ipv4/defs_darwin.go | 1 - ipv4/defs_dragonfly.go | 1 - ipv4/defs_freebsd.go | 1 - ipv4/defs_linux.go | 1 - ipv4/defs_netbsd.go | 1 - ipv4/defs_openbsd.go | 1 - ipv4/defs_solaris.go | 1 - ipv4/errors_other_test.go | 1 - ipv4/errors_unix_test.go | 1 - ipv4/gen.go | 1 - ipv4/helper_posix_test.go | 1 - ipv4/helper_stub_test.go | 1 - ipv4/icmp_stub.go | 1 - ipv4/payload_cmsg.go | 1 - ipv4/payload_nocmsg.go | 1 - ipv4/sockopt_posix.go | 1 - ipv4/sockopt_stub.go | 1 - ipv4/sys_aix.go | 1 - ipv4/sys_asmreq.go | 1 - ipv4/sys_asmreq_stub.go | 1 - ipv4/sys_asmreqn.go | 1 - ipv4/sys_asmreqn_stub.go | 1 - ipv4/sys_bpf.go | 1 - ipv4/sys_bpf_stub.go | 1 - ipv4/sys_bsd.go | 1 - ipv4/sys_ssmreq.go | 1 - ipv4/sys_ssmreq_stub.go | 1 - ipv4/sys_stub.go | 1 - ipv4/zsys_aix_ppc64.go | 1 - ipv4/zsys_linux_loong64.go | 1 - ipv4/zsys_linux_riscv64.go | 1 - ipv6/control_rfc2292_unix.go | 1 - ipv6/control_rfc3542_unix.go | 1 - ipv6/control_stub.go | 1 - ipv6/control_unix.go | 1 - ipv6/defs_aix.go | 1 - ipv6/defs_darwin.go | 1 - ipv6/defs_dragonfly.go | 1 - ipv6/defs_freebsd.go | 1 - ipv6/defs_linux.go | 1 - ipv6/defs_netbsd.go | 1 - ipv6/defs_openbsd.go | 1 - ipv6/defs_solaris.go | 1 - ipv6/errors_other_test.go | 1 - ipv6/errors_unix_test.go | 1 - ipv6/gen.go | 1 - ipv6/helper_posix_test.go | 1 - ipv6/helper_stub_test.go | 1 - ipv6/helper_unix_test.go | 1 - ipv6/icmp_bsd.go | 1 - ipv6/icmp_stub.go | 1 - ipv6/payload_cmsg.go | 1 - ipv6/payload_nocmsg.go | 1 - ipv6/sockopt_posix.go | 1 - ipv6/sockopt_stub.go | 1 - ipv6/sys_aix.go | 1 - ipv6/sys_asmreq.go | 1 - ipv6/sys_asmreq_stub.go | 1 - ipv6/sys_bpf.go | 1 - ipv6/sys_bpf_stub.go | 1 - ipv6/sys_bsd.go | 1 - ipv6/sys_ssmreq.go | 1 - ipv6/sys_ssmreq_stub.go | 1 - ipv6/sys_stub.go | 1 - ipv6/zsys_aix_ppc64.go | 1 - ipv6/zsys_linux_loong64.go | 1 - ipv6/zsys_linux_riscv64.go | 1 - lif/address.go | 1 - lif/address_test.go | 1 - lif/binary.go | 1 - lif/defs_solaris.go | 1 - lif/lif.go | 1 - lif/link.go | 1 - lif/link_test.go | 1 - lif/sys.go | 1 - lif/syscall.go | 1 - nettest/conntest_test.go | 1 - nettest/nettest_stub.go | 1 - nettest/nettest_unix.go | 1 - publicsuffix/gen.go | 1 - route/address.go | 1 - route/address_test.go | 1 - route/binary.go | 1 - route/defs_darwin.go | 1 - route/defs_dragonfly.go | 1 - route/defs_freebsd.go | 1 - route/defs_netbsd.go | 1 - route/defs_openbsd.go | 1 - route/empty.s | 1 - route/interface.go | 1 - route/interface_announce.go | 1 - route/interface_classic.go | 1 - route/interface_multicast.go | 1 - route/message.go | 1 - route/message_test.go | 1 - route/route.go | 1 - route/route_classic.go | 1 - route/route_test.go | 1 - route/sys.go | 1 - route/syscall.go | 1 - webdav/litmus_test_server.go | 1 - 192 files changed, 1 insertion(+), 231 deletions(-) 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/go.mod b/go.mod index 38ac82b44..f83c0890a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/net -go 1.17 +go 1.18 require ( golang.org/x/crypto v0.14.0 diff --git a/go.sum b/go.sum index dc4dc125c..ddbbdd3ef 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= 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/go111.go b/http2/go111.go index 5bf62b032..4ced74a0b 100644 --- a/http2/go111.go +++ b/http2/go111.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.11 -// +build go1.11 package http2 diff --git a/http2/go115.go b/http2/go115.go index 908af1ab9..13a2d9215 100644 --- a/http2/go115.go +++ b/http2/go115.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.15 -// +build go1.15 package http2 diff --git a/http2/go118.go b/http2/go118.go index aca4b2b31..a445ae1d5 100644 --- a/http2/go118.go +++ b/http2/go118.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.18 -// +build go1.18 package http2 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 index cc0baa819..f4d63f458 100644 --- a/http2/not_go111.go +++ b/http2/not_go111.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.11 -// +build !go1.11 package http2 diff --git a/http2/not_go115.go b/http2/not_go115.go index e6c04cf7a..635753408 100644 --- a/http2/not_go115.go +++ b/http2/not_go115.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.15 -// +build !go1.15 package http2 diff --git a/http2/not_go118.go b/http2/not_go118.go index eab532c96..b1b11c072 100644 --- a/http2/not_go118.go +++ b/http2/not_go118.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.18 -// +build !go1.18 package http2 diff --git a/http2/transport_go117_test.go b/http2/transport_go117_test.go index f5d4e0c1a..0f257ad24 100644 --- a/http2/transport_go117_test.go +++ b/http2/transport_go117_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.17 -// +build go1.17 package http2 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/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 From 37479d671cd577ab022df2c2b7164ddc8ad735f7 Mon Sep 17 00:00:00 2001 From: Mauri de Souza Meneguzzo Date: Mon, 16 Oct 2023 00:15:08 +0000 Subject: [PATCH 03/22] http2: fix underflow in http2 server push After CL 534215 was merged to fix a CVE it introduced an underflow when we try to decrement sc.curHandlers in handlerDone. The func startPush calls runHandler without incrementing curHandlers. Seems to only affect users of http.Pusher. For golang/go#63511 Change-Id: Ic537c27c9945c2c2d4306ddb04e9527b65cee320 GitHub-Last-Rev: 249fe55f7501ca607f70e8050c6546995cd808e8 GitHub-Pull-Request: golang/net#197 Reviewed-on: https://go-review.googlesource.com/c/net/+/535595 Reviewed-by: Damien Neil Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Run-TryBot: Mauri de Souza Meneguzzo --- http2/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/http2/server.go b/http2/server.go index 02c88b6b3..7f3bed926 100644 --- a/http2/server.go +++ b/http2/server.go @@ -3187,6 +3187,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 } From 9ef1b7226adc64e857d2d883d930d189c15d6e54 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 6 Oct 2023 15:49:18 -0700 Subject: [PATCH 04/22] quic: move more testConn behavior into testListener Refactor the testConn/testListener relationship some. Move synthetic time tracking into the listener. Let the testListener create testConns. These changes will allow us to test Retry behavior, where the listener responds to a new connection request with a Retry packet, and only initiates the connection upon receiving a valid Retry token. For golang/go#58547 Change-Id: Ib6fc86a21819059f2a603fa6c9be14ab87a7a44c Reviewed-on: https://go-review.googlesource.com/c/net/+/535236 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 25 ++- internal/quic/conn_test.go | 301 ++++++++++++++++++--------------- internal/quic/listener.go | 12 +- internal/quic/listener_test.go | 180 +++++++++++++++++++- internal/quic/tls_test.go | 6 +- internal/quic/version_test.go | 2 +- 6 files changed, 374 insertions(+), 152 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 9db00fe09..ea03bbf98 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -61,14 +61,29 @@ 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) { +func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -76,7 +91,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,6 +100,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // non-blocking operation. c.msgc = make(chan any, 1) + if l.testHooks != nil { + l.testHooks.newConn(c) + } + var originalDstConnID []byte if c.side == clientSide { if err := c.connIDState.initClient(c); err != nil { @@ -126,6 +144,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 } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 6a359e89a..ea47b0b29 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 @@ -184,27 +189,10 @@ type keySecret struct { // by first ensuring the loop goroutine is idle. func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() - tc := &testConn{ - t: t, - now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - peerConnID: testPeerConnID(0), - ignoreFrames: map[byte]bool{ - frameTypePadding: true, // ignore PADDING by default - }, - cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), - cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), - recvDatagram: make(chan *datagram), - } - t.Cleanup(tc.cleanup) - config := &Config{ TLSConfig: newTestTLSConfig(side), } - peerProvidedParams := defaultTransportParameters() - peerProvidedParams.initialSrcConnID = testPeerConnID(0) - if side == clientSide { - peerProvidedParams.originalDstConnID = testLocalConnID(-1) - } + var configTransportParams []func(*transportParameters) for _, o := range opts { switch o := o.(type) { case func(*Config): @@ -212,7 +200,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { case func(*tls.Config): o(config.TLSConfig) case func(p *transportParameters): - o(&peerProvidedParams) + configTransportParams = append(configTransportParams, o) default: t.Fatalf("unknown newTestConn option %T", o) } @@ -224,52 +212,75 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { initialConnID = testPeerConnID(-1) } - peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(side.peer())} - if side == clientSide { - tc.peerTLSConn = tls.QUICServer(peerQUICConfig) - } else { - tc.peerTLSConn = tls.QUICClient(peerQUICConfig) - } - 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, + listener := newTestListener(t, config) + listener.configTransportParams = configTransportParams + conn, err := listener.l.newConn( + listener.now, side, initialConnID, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { - tc.t.Fatal(err) + 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, + listener: listener, + conn: conn, + peerConnID: testPeerConnID(0), + ignoreFrames: map[byte]bool{ + frameTypePadding: true, // ignore PADDING by default + }, + cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), + cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), + recvDatagram: make(chan *datagram), } - tc.conn = conn + t.Cleanup(tc.cleanup) + conn.testHooks = (*testConnHooks)(tc) - conn.keysAppData.updateAfter = maxPacketNumber // disable key updates - tc.keysInitial.r = conn.keysInitial.w - tc.keysInitial.w = conn.keysInitial.r + if listener.peerTLSConn != nil { + tc.peerTLSConn = listener.peerTLSConn + listener.peerTLSConn = nil + return tc + } + + peerProvidedParams := defaultTransportParameters() + peerProvidedParams.initialSrcConnID = testPeerConnID(0) + if conn.side == clientSide { + peerProvidedParams.originalDstConnID = testLocalConnID(-1) + } + for _, f := range listener.configTransportParams { + f(&peerProvidedParams) + } + + peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(conn.side.peer())} + if conn.side == clientSide { + tc.peerTLSConn = tls.QUICServer(peerQUICConfig) + } else { + tc.peerTLSConn = tls.QUICClient(peerQUICConfig) + } + tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) + tc.peerTLSConn.Start(context.Background()) - 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 +295,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 +308,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 +351,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 +360,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 +369,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 +388,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 +458,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. @@ -638,21 +632,23 @@ 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 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 +656,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 +679,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 +705,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 +716,39 @@ 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 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 +756,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 +776,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 +785,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 +800,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 +821,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 +844,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 +854,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 +960,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 +987,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 +995,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 diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 96b1e4593..9f14b0904 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -23,7 +23,7 @@ import ( type Listener struct { config *Config udpConn udpConn - testHooks connTestHooks + testHooks listenerTestHooks acceptQueue queue[*Conn] // new inbound connections @@ -40,6 +40,11 @@ type Listener struct { connIDUpdates []connIDUpdate } +type listenerTestHooks interface { + timeNow() time.Time + newConn(c *Conn) +} + // A udpConn is a UDP connection. // It is implemented by net.UDPConn. type udpConn interface { @@ -72,7 +77,7 @@ func Listen(network, address string, config *Config) (*Listener, error) { return newListener(udpConn, config, nil), nil } -func newListener(udpConn udpConn, config *Config, hooks connTestHooks) *Listener { +func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *Listener { l := &Listener{ config: config, udpConn: udpConn, @@ -154,11 +159,10 @@ func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, p 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, initialConnID, peerAddr, l.config, l) if err != nil { return nil, err } - l.conns[c] = struct{}{} return c, nil } diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 9d0f314ec..77362aa9b 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) { @@ -90,20 +93,28 @@ 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) + 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), } - tl.l = newListener((*testListenerUDPConn)(tl), config, testHooks) + tl.l = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) t.Cleanup(tl.cleanup) return tl } @@ -114,6 +125,20 @@ func (tl *testListener) cleanup() { func (tl *testListener) wait() { tl.idlec <- struct{}{} + 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 +146,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 + } + 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 +215,88 @@ 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) + } +} + +func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { + peerProvidedParams := defaultTransportParameters() + peerProvidedParams.initialSrcConnID = srcConnID + peerProvidedParams.originalDstConnID = dstConnID + for _, f := range tl.configTransportParams { + f(&peerProvidedParams) + } + + config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} + tl.peerTLSConn = tls.QUICClient(config) + tl.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) + tl.peerTLSConn.Start(context.Background()) + var data []byte + for { + e := tl.peerTLSConn.NextEvent() + switch e.Kind { + case tls.QUICNoEvent: + return data + case tls.QUICWriteData: + if e.Level != tls.QUICEncryptionLevelInitial { + tl.t.Fatal("initial data at unexpected level") + } + data = append(data, e.Data...) + } + } +} + +// 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/tls_test.go b/internal/quic/tls_test.go index 81d17b858..337657e32 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) } @@ -90,7 +90,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { } else { clientConnIDs = peerConnIDs serverConnIDs = localConnIDs - transientConnID = []byte{0xde, 0xad, 0xbe, 0xef} + transientConnID = testPeerConnID(-1) } return []*testDatagram{{ // Client Initial @@ -564,7 +564,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/version_test.go b/internal/quic/version_test.go index cfb7ce4be..264df9dbc 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} From 48a597731ce9fbe97fed1733da5e808233b6cdcb Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 13 Oct 2023 09:44:23 -0700 Subject: [PATCH 05/22] quic: support Retry Add a RequireAddressValidation configuration setting to enable sending Retry packets on the server. Support receiving Retry packets on the client. RFC 9000, Section 8.1.2. For golang/go#58547 Change-Id: Ia78b9594a03ce1b1143b95cb3c1ef4c38b2b39ef Reviewed-on: https://go-review.googlesource.com/c/net/+/535237 LUCI-TryBot-Result: Go LUCI Reviewed-by: Roland Shoemaker Reviewed-by: Jonathan Amsterdam --- internal/quic/config.go | 8 + internal/quic/conn.go | 26 +- internal/quic/conn_id.go | 51 +-- internal/quic/conn_id_test.go | 3 - internal/quic/conn_recv.go | 39 +++ internal/quic/conn_send.go | 1 + internal/quic/conn_test.go | 28 +- internal/quic/listener.go | 53 ++- internal/quic/listener_test.go | 13 +- internal/quic/loss.go | 13 + internal/quic/packet.go | 3 + internal/quic/packet_parser.go | 4 +- internal/quic/retry.go | 235 ++++++++++++++ internal/quic/retry_test.go | 568 +++++++++++++++++++++++++++++++++ 14 files changed, 999 insertions(+), 46 deletions(-) create mode 100644 internal/quic/retry.go create mode 100644 internal/quic/retry_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index b390d6911..99ef68fea 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -47,6 +47,14 @@ 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 } func configDefault(v, def, limit int64) int64 { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index ea03bbf98..4acf5ddfe 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 @@ -83,7 +86,7 @@ type connTestHooks interface { timeNow() time.Time } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { +func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -104,17 +107,21 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. l.testHooks.newConn(c) } - var originalDstConnID []byte + // 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 { + initialConnID = originalDstConnID + if retrySrcConnID != nil { + initialConnID = retrySrcConnID + } if err := c.connIDState.initServer(c, initialConnID); err != nil { return nil, err } - originalDstConnID = initialConnID } // The smallest allowed maximum QUIC datagram size is 1200 bytes. @@ -125,10 +132,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, + retrySrcConnID: retrySrcConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, @@ -195,7 +202,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.side, isRetry, p); err != nil { return err } c.streams.outflow.setMaxData(p.initialMaxData) @@ -220,9 +228,11 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { 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_id.go b/internal/quic/conn_id.go index 045e646ac..ff7e2d1c6 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -28,6 +28,9 @@ type connIDState struct { 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 } @@ -78,6 +81,7 @@ func (s *connIDState) initClient(c *Conn) error { seq: -1, cid: remid, }) + s.originalDstConnID = remid const retired = false c.listener.connIDsChanged(c, retired, s.local[:]) return nil @@ -163,27 +167,21 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { // 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(side connSide, 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) - } - // 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) - } + // 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(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(errTransportParameter) } + 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) @@ -203,13 +201,10 @@ 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{ + s.remote[0] = connID{ seq: 0, cid: cloneBytes(srcConnID), - }) + } } case ptype == packetTypeInitial && c.side == serverSide: if len(s.remote) == 0 { @@ -232,6 +227,14 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) } } +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(seq, retire int64, cid []byte, resetToken [16]byte) error { if len(s.remote[0].cid) == 0 { // "An endpoint that is sending packets with a zero-length diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 44755ecf4..784c5e2c4 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -48,9 +48,6 @@ func TestConnIDClientHandshake(t *testing.T) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []connID{{ - cid: testLocalConnID(-1), - seq: -1, - }, { cid: testPeerConnID(0), seq: 0, }} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 9b1ba1ae1..e789ae045 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -34,6 +34,9 @@ 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 @@ -128,6 +131,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) { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 00b02c2a3..efeb04fe3 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) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index ea47b0b29..cfb0d062c 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -218,6 +218,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { listener.now, side, initialConnID, + nil, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) @@ -545,7 +546,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:] @@ -638,6 +645,12 @@ func encodeTestPacket(t *testing.T, tc *testConn, p *testPacket, pad int) []byte w.reset(1200) var pnumMaxAcked packetNumber 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: @@ -717,6 +730,19 @@ func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) } ptype := getPacketType(buf) 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 { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 9f14b0904..aa258395e 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -24,6 +24,7 @@ type Listener struct { config *Config udpConn udpConn testHooks listenerTestHooks + retry retryState acceptQueue queue[*Conn] // new inbound connections @@ -74,10 +75,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 listenerTestHooks) *Listener { +func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Listener, error) { l := &Listener{ config: config, udpConn: udpConn, @@ -86,8 +87,13 @@ func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *List acceptQueue: newQueue[*Conn](), closec: make(chan struct{}), } + 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. @@ -142,7 +148,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, nil, nil, addr) if err != nil { return nil, err } @@ -153,13 +159,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, originalDstConnID, retrySrcConnID []byte, 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) + c, err := newConn(now, side, originalDstConnID, retrySrcConnID, peerAddr, l.config, l) if err != nil { return nil, err } @@ -300,8 +306,19 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { } else { now = time.Now() } + var originalDstConnID, retrySrcConnID []byte + if l.config.RequireAddressValidation { + var ok bool + retrySrcConnID = p.dstConnID + originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + if !ok { + return + } + } else { + originalDstConnID = p.dstConnID + } var err error - c, err := l.newConn(now, serverSide, p.dstConnID, m.addr) + c, err := l.newConn(now, serverSide, originalDstConnID, retrySrcConnID, m.addr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. @@ -320,6 +337,28 @@ 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(minimumClientInitialDatagramSize) + 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 diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 77362aa9b..346f81c38 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -114,7 +114,11 @@ func newTestListener(t *testing.T, config *Config) *testListener { idlec: make(chan struct{}), conns: make(map[*Conn]*testConn), } - tl.l = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + var err error + tl.l, err = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + if err != nil { + t.Fatal(err) + } t.Cleanup(tl.cleanup) return tl } @@ -237,6 +241,13 @@ func (tl *testListener) wantDatagram(expectation string, want *testDatagram) { } } +// 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) + } +} + func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { peerProvidedParams := defaultTransportParameters() peerProvidedParams.initialSrcConnID = srcConnID 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..8bcd8668e 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:] 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, + }, + }, + }}, + } +} From 4c7a5b64f145becd6fbd7d22f7ad4e9b891ce43a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 25 Oct 2023 09:49:26 -0700 Subject: [PATCH 06/22] http2: add test for push promise accounting underflow Verify that repeated requests resulting in a PUSH_PROMISE result all complete successfully, validating the fix in CL 535595. For golang/go#63511 Change-Id: I6bebdcfcecb6c53f076e4ac6873d61a150d1040e Reviewed-on: https://go-review.googlesource.com/c/net/+/537715 Auto-Submit: Damien Neil Reviewed-by: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov Reviewed-by: Mauri de Souza Meneguzzo LUCI-TryBot-Result: Go LUCI --- http2/server_push_test.go | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) 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++ + } + } +} From 642f15ebba2e78391db8e6cccf2bc868cb6fc771 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 18 Oct 2023 13:33:05 -0400 Subject: [PATCH 07/22] quic: support stateless reset Add a StatelessResetKey config field to permit generating consistent stateless reset tokens across server restarts. Set the stateless_reset_token transport parameter and populate the Token field in NEW_CONNECTION_ID frames. Detect reset tokens in datagrams which cannot be associated with a connection or cannot be parsed. RFC 9000, Section 10.3. For golang/go#58547 Change-Id: Idb52ba07092ab5c08b323d6b531964a7e7e5ecea Reviewed-on: https://go-review.googlesource.com/c/net/+/536315 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil --- internal/quic/config.go | 17 ++ internal/quic/conn.go | 4 +- internal/quic/conn_close_test.go | 4 +- internal/quic/conn_id.go | 117 ++++++++--- internal/quic/conn_id_test.go | 98 ++++++++- internal/quic/conn_loss_test.go | 4 + internal/quic/conn_recv.go | 27 ++- internal/quic/conn_send.go | 2 +- internal/quic/conn_test.go | 10 +- internal/quic/frame_debug.go | 2 +- internal/quic/listener.go | 199 ++++++++++++------ internal/quic/packet_parser.go | 14 +- internal/quic/stateless_reset.go | 61 ++++++ internal/quic/stateless_reset_test.go | 277 ++++++++++++++++++++++++++ internal/quic/tls_test.go | 22 +- 15 files changed, 740 insertions(+), 118 deletions(-) create mode 100644 internal/quic/stateless_reset.go create mode 100644 internal/quic/stateless_reset_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index 99ef68fea..6278bf89c 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -55,6 +55,23 @@ type Config struct { // 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 4acf5ddfe..b3d6feabc 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -203,7 +203,7 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { isRetry := c.retryToken != nil - if err := c.connIDState.validateTransportParameters(c.side, isRetry, p); err != nil { + if err := c.connIDState.validateTransportParameters(c, isRetry, p); err != nil { return err } c.streams.outflow.setMaxData(p.initialMaxData) @@ -224,7 +224,7 @@ 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 } } diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 20c00e754..d5c3499e4 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) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index ff7e2d1c6..c23613759 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -22,7 +22,7 @@ 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 @@ -58,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. @@ -70,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. @@ -77,13 +86,13 @@ 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, + }, }) s.originalDstConnID = remid - const retired = false - c.listener.connIDsChanged(c, retired, s.local[:]) return nil } @@ -107,8 +116,10 @@ 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) + }) return nil } @@ -131,6 +142,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 { @@ -145,12 +169,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, @@ -160,14 +185,17 @@ 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, isRetry bool, p transportParameters) error { +func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p transportParameters) error { // TODO: Consider returning more detailed errors, for debugging. // Verify original_destination_connection_id matches // the transient remote connection ID we chose (client) @@ -189,6 +217,16 @@ func (s *connIDState) validateTransportParameters(side connSide, isRetry bool, p if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) { return localTransportError(errTransportParameter) } + if len(p.statelessResetToken) > 0 { + if c.side == serverSide { + return localTransportError(errTransportParameter) + } + token := statelessResetToken(p.statelessResetToken) + s.remote[0].resetToken = token + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addResetToken(c, token) + }) + } return nil } @@ -201,18 +239,22 @@ 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. - s.remote[0] = connID{ - seq: 0, - cid: cloneBytes(srcConnID), + s.remote[0] = remoteConnID{ + connID: 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 = append(s.remote, remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(srcConnID), + }, }) } case ptype == packetTypeHandshake && c.side == serverSide: @@ -220,8 +262,10 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // 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:]...) } } @@ -235,7 +279,7 @@ func (s *connIDState) handleRetryPacket(srcConnID []byte) { s.remote[0].cid = s.retrySrcConnID } -func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error { +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 @@ -254,6 +298,9 @@ 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++ @@ -272,15 +319,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) + }) } } @@ -305,7 +358,7 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken } // 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 @@ -317,8 +370,10 @@ func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { } 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 } @@ -363,7 +418,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 @@ -376,11 +431,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 } @@ -390,7 +445,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 784c5e2c4..63feec992 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -47,12 +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: 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)) } } @@ -93,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 @@ -134,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 { @@ -142,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 { @@ -174,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") } @@ -255,6 +288,7 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { seq: 2, retirePriorTo: 1, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) }) } @@ -455,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") } @@ -583,3 +618,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 e789ae045..183316780 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -41,9 +41,23 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { 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(token) + } // Invalid data at the end of a datagram is ignored. break } @@ -468,7 +482,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 @@ -515,3 +529,12 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa } return 1 } + +var errStatelessReset = errors.New("received stateless reset") + +func (c *Conn) handleStatelessReset(resetToken statelessResetToken) { + if !c.connIDState.isValidStatelessResetToken(resetToken) { + return + } + c.enterDraining(errStatelessReset) +} diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index efeb04fe3..f512518ef 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -250,7 +250,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 } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index cfb0d062c..df28907f4 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -190,7 +190,8 @@ type keySecret struct { func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() config := &Config{ - TLSConfig: newTestTLSConfig(side), + TLSConfig: newTestTLSConfig(side), + StatelessResetKey: testStatelessResetKey, } var configTransportParams []func(*transportParameters) for _, o := range opts { @@ -1041,6 +1042,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/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 aa258395e..668d270b3 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" @@ -24,21 +25,16 @@ type Listener struct { config *Config udpConn udpConn 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 { @@ -55,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) { @@ -87,6 +77,8 @@ func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Lis 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 @@ -181,6 +173,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) @@ -189,39 +197,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. @@ -237,22 +214,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. @@ -271,18 +248,29 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m.recycle() } }() - if len(m.b) < minimumClientInitialDatagramSize { + const minimumValidPacketSize = 21 + if len(m.b) < minimumValidPacketSize { + return + } + // 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(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) < minimumClientInitialDatagramSize { return } - switch p.version { case quicVersion1: case 0: @@ -296,8 +284,9 @@ 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 @@ -330,6 +319,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) @@ -363,3 +396,53 @@ 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/packet_parser.go b/internal/quic/packet_parser.go index 8bcd8668e..02ef9fb14 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -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/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..b12e97560 --- /dev/null +++ b/internal/quic/stateless_reset_test.go @@ -0,0 +1,277 @@ +// 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" +) + +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") +} + +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/tls_test.go b/internal/quic/tls_test.go index 337657e32..6f4e06522 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -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,13 +85,19 @@ 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 + clientResetToken = peerResetToken + serverResetToken = localResetToken transientConnID = testPeerConnID(-1) } return []*testDatagram{{ @@ -136,6 +144,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: serverConnIDs[1], + token: serverResetToken, }, }, }}, @@ -175,6 +184,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: clientConnIDs[1], + token: clientResetToken, }, }, }}, @@ -337,6 +347,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 +401,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 +558,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 From 6d267b1f96339103f3a322266f022b21a1d0688d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 26 Oct 2023 11:34:01 -0700 Subject: [PATCH 08/22] quic: properly shut down connections on listener close We were failing to add new connections to the listener's set of live connections, so closing a listener wouldn't abort connections or wait for them to shut down. We were also aborting active connections with an error that resulted in the connection closing with an INTERNAL_ERROR status. Close with NO_ERROR instead. For golang/go#58547 Change-Id: I89b6c4fabf744ae5178c0cae655929db1ae40ee4 Reviewed-on: https://go-review.googlesource.com/c/net/+/537935 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_close_test.go | 12 ++++++++++++ internal/quic/listener.go | 3 ++- internal/quic/listener_test.go | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index d5c3499e4..d583ae92a 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -186,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/listener.go b/internal/quic/listener.go index 668d270b3..cfe45b137 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -107,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(errNo)) } if len(l.conns) == 0 { l.udpConn.Close() @@ -161,6 +161,7 @@ func (l *Listener) newConn(now time.Time, side connSide, originalDstConnID, retr if err != nil { return nil, err } + l.conns[c] = struct{}{} return c, nil } diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 346f81c38..a5cc690ac 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -128,7 +128,10 @@ 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() } From 0526b49b345664cadb8ea67dd4b7c02964c55b3a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 30 Oct 2023 11:01:23 -0700 Subject: [PATCH 09/22] quic: fix data race caused by aliased DCID The initServer function was retaining a reference to a []byte that aliases a packet buffer, which is subsequently recycled. Make a copy of the data before retaining it. Fixes golang/go#63783 Change-Id: I3dbb0cdfd78681014dec97ff9909ff6c7dbf82ba Reviewed-on: https://go-review.googlesource.com/c/net/+/538615 LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_id.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index c23613759..91ccaade1 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -97,12 +97,13 @@ func (s *connIDState) initClient(c *Conn) error { } func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { + dstConnID = cloneBytes(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 From 05086a79fc73b01ecd1d1c303fc3f4a1311afc17 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Oct 2023 08:49:27 -0700 Subject: [PATCH 10/22] quic: fix panic when handling resent CRYPTO data When pipe.discardBefore was called with an offset greater than the current pipe.end position, we would update pipe.start but not pipe.end, leaving the pipe in an inconsistent state where start > end. This could then subsequently cause a panic when writing data that lies before pipe.start. This sequence occurs when handling several in-order CRYPTO frames (where we skip writing in-order data to the pipe, but still call discardBefore), followed by an out-of-order frame containing resent data. Change-Id: Ibac0caad53cd30dac1cd4719a825226809872d96 Reviewed-on: https://go-review.googlesource.com/c/net/+/538775 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/crypto_stream_test.go | 15 +++++++++++++++ internal/quic/pipe.go | 1 + internal/quic/pipe_test.go | 9 +++++++++ 3 files changed, 25 insertions(+) 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/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) From 770149e9886ec895bb824b608bd02d661fce552d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Oct 2023 09:37:48 -0700 Subject: [PATCH 11/22] quic: pad ack-eliciting server Initial datagrams UDP datagrams containing Initial packets are expanded to 1200 bytes to validate that the path is capable of supporting the smallest allowed maximum QUIC datagram size. (In addition, client Initial packets must be sent in datagrams of at least 1200 bytes, to defend against amplification attacks.) We were expanding client datagrams containing Initial packets, but not server datagrams. Fix this. (More specifically, server datagrams must be expanded to 1200 bytes when they contain ack-eliciting Initial packets.) RFC 9000, Section 14.1. Change-Id: I0c0c36321c055e960be3e29a49d7cb7620640b82 Reviewed-on: https://go-review.googlesource.com/c/net/+/538776 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_recv.go | 2 +- internal/quic/conn_send.go | 13 +++++++------ internal/quic/listener.go | 4 ++-- internal/quic/listener_test.go | 2 +- internal/quic/quic.go | 5 +++-- internal/quic/tls_test.go | 1 + internal/quic/version_test.go | 2 +- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 183316780..e966b7ef5 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 diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index f512518ef..64e5d7548 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -77,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 } } @@ -123,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 { @@ -149,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 diff --git a/internal/quic/listener.go b/internal/quic/listener.go index cfe45b137..08f011092 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -269,7 +269,7 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { return } p, ok := parseGenericLongHeaderPacket(m.b) - if !ok || len(m.b) < minimumClientInitialDatagramSize { + if !ok || len(m.b) < paddedInitialDatagramSize { return } switch p.version { @@ -382,7 +382,7 @@ func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort srcConnID: in.dstConnID, } const pnumMaxAcked = 0 - w.reset(minimumClientInitialDatagramSize) + w.reset(paddedInitialDatagramSize) w.startProtectedLongHeaderPacket(pnumMaxAcked, p) w.appendConnectionCloseTransportFrame(code, 0, "") w.finishProtectedLongHeaderPacket(pnumMaxAcked, keys.w, p) diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index a5cc690ac..21717e251 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -172,7 +172,7 @@ func (tl *testListener) writeDatagram(d *testDatagram) { } pad := 0 if p.ptype == packetType1RTT { - pad = d.paddedSize + pad = d.paddedSize - len(buf) } buf = append(buf, encodeTestPacket(tl.t, tc, p, pad)...) } 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/tls_test.go b/internal/quic/tls_test.go index 6f4e06522..fa339b9fa 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -148,6 +148,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { }, }, }}, + paddedSize: 1200, }, { // Client Initial + Handshake + 1-RTT packets: []*testPacket{{ diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go index 264df9dbc..830e0e1c8 100644 --- a/internal/quic/version_test.go +++ b/internal/quic/version_test.go @@ -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) } From 4865e2af27b9cc42cbb25a29294154b519cc8e56 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 3 Oct 2023 10:57:24 -0700 Subject: [PATCH 12/22] internal/quic/cmd/interop: add interop test runner The QUIC interop tests at https://interop.seemann.io/ invoke a program and instruct it to perform some set of operations (mostly serve files from a directory, or download a set of files). The cmd/interop binary executes test cases for our implementation. For golang/go#58547 Change-Id: Ic1c8be2f3f49a30464650d9eaa5ded74c92fa5a7 Reviewed-on: https://go-review.googlesource.com/c/net/+/532435 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil --- internal/quic/cmd/interop/Dockerfile | 32 +++ internal/quic/cmd/interop/README.md | 7 + internal/quic/cmd/interop/main.go | 262 ++++++++++++++++++++++ internal/quic/cmd/interop/main_test.go | 155 +++++++++++++ internal/quic/cmd/interop/run_endpoint.sh | 17 ++ 5 files changed, 473 insertions(+) create mode 100644 internal/quic/cmd/interop/Dockerfile create mode 100644 internal/quic/cmd/interop/README.md create mode 100644 internal/quic/cmd/interop/main.go create mode 100644 internal/quic/cmd/interop/main_test.go create mode 100644 internal/quic/cmd/interop/run_endpoint.sh 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..9afd22433 --- /dev/null +++ b/internal/quic/cmd/interop/main_test.go @@ -0,0 +1,155 @@ +// 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" + "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) + } +} + +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 { + 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 t.Logf("%v done", name) + defer close(addrc) + defer close(donec) + 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 From 5791239e3d7093c8867413137a35e5fcaaf8277b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 1 Nov 2023 12:22:13 -0700 Subject: [PATCH 13/22] internal/quic/cmd/interop: skip tests when exec is unavailable Some platforms, such as js and wasip1, can't exec. Skip tests that need exec when it isn't available. Change-Id: Id3787b28c2ffe780eb24800c59fe69d12e04bbdd Reviewed-on: https://go-review.googlesource.com/c/net/+/539035 Reviewed-by: Bryan Mills LUCI-TryBot-Result: Go LUCI --- internal/quic/cmd/interop/main_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go index 9afd22433..6fd9c0f2d 100644 --- a/internal/quic/cmd/interop/main_test.go +++ b/internal/quic/cmd/interop/main_test.go @@ -15,6 +15,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "testing" ) @@ -26,6 +27,23 @@ func init() { } } +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 @@ -33,6 +51,7 @@ type interopTest struct { } 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() From ec29a9498a02f880ede985f4671b24c62016f936 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 3 Nov 2023 16:37:26 -0700 Subject: [PATCH 14/22] quic: provide source conn ID when creating server conns New server-side conns need to know a variety of connection IDs, such as the Initial DCID used to create Initial encryption keys. We've been providing these as an ever-growing list of []byte parameters to newConn. Bundle them all up into a struct. Add the client's SCID to the set of IDs we pass to newConn. Up until now, we've been setting this when processing the first Initial packet from the client. Passing it to newConn will makes it available when logging the connection_started event. Update some test infrastructure to deal with the fact that we need to know the peer's SCID earlier in the test now. Change-Id: I760ee94af36125acf21c5bf135f1168830ba1ab8 Reviewed-on: https://go-review.googlesource.com/c/net/+/539341 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 22 ++++++++++++------ internal/quic/conn_id.go | 12 ++++++++-- internal/quic/conn_id_test.go | 5 +++- internal/quic/conn_test.go | 24 ++++++++++++------- internal/quic/listener.go | 19 ++++++++------- internal/quic/listener_test.go | 42 ++++++++-------------------------- 6 files changed, 65 insertions(+), 59 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index b3d6feabc..1292f2b20 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -86,7 +86,15 @@ type connTestHooks interface { timeNow() time.Time } -func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*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, @@ -115,11 +123,11 @@ func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []b } initialConnID, _ = c.connIDState.dstConnID() } else { - initialConnID = originalDstConnID - if retrySrcConnID != nil { - initialConnID = retrySrcConnID + initialConnID = cids.originalDstConnID + if cids.retrySrcConnID != nil { + initialConnID = cids.retrySrcConnID } - if err := c.connIDState.initServer(c, initialConnID); err != nil { + if err := c.connIDState.initServer(c, cids); err != nil { return nil, err } } @@ -134,8 +142,8 @@ func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []b if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), - originalDstConnID: originalDstConnID, - retrySrcConnID: retrySrcConnID, + originalDstConnID: cids.originalDstConnID, + retrySrcConnID: cids.retrySrcConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 91ccaade1..b77ad8edf 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -96,8 +96,8 @@ func (s *connIDState) initClient(c *Conn) error { return nil } -func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { - dstConnID = cloneBytes(dstConnID) +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. @@ -121,6 +121,14 @@ func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { 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 } diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 63feec992..314a6b384 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -578,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{ diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index df28907f4..248be9641 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -193,33 +193,38 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { 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) } } - var initialConnID []byte - if side == serverSide { - // The initial connection ID for the server is chosen by the client. - initialConnID = testPeerConnID(-1) - } - listener := newTestListener(t, config) listener.configTransportParams = configTransportParams + listener.configTestConn = configTestConn conn, err := listener.l.newConn( listener.now, side, - initialConnID, - nil, + cids, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) @@ -244,6 +249,9 @@ func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testC recvDatagram: make(chan *datagram), } t.Cleanup(tc.cleanup) + for _, f := range listener.configTestConn { + f(tc) + } conn.testHooks = (*testConnHooks)(tc) if listener.peerTLSConn != nil { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 08f011092..24484eb6f 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -140,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, nil, addr) + c, err := l.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) if err != nil { return nil, err } @@ -151,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, originalDstConnID, retrySrcConnID []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, originalDstConnID, retrySrcConnID, peerAddr, l.config, l) + c, err := newConn(now, side, cids, peerAddr, l.config, l) if err != nil { return nil, err } @@ -296,19 +296,22 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { } else { now = time.Now() } - var originalDstConnID, retrySrcConnID []byte + cids := newServerConnIDs{ + srcConnID: p.srcConnID, + dstConnID: p.dstConnID, + } if l.config.RequireAddressValidation { var ok bool - retrySrcConnID = p.dstConnID - originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + cids.retrySrcConnID = p.dstConnID + cids.originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) if !ok { return } } else { - originalDstConnID = p.dstConnID + cids.originalDstConnID = p.dstConnID } var err error - c, err := l.newConn(now, serverSide, originalDstConnID, retrySrcConnID, 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. diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 21717e251..674d4e4a1 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -19,12 +19,12 @@ import ( ) func TestConnect(t *testing.T) { - newLocalConnPair(t, &Config{}, &Config{}) + NewLocalConnPair(t, &Config{}, &Config{}) } func TestStreamTransfer(t *testing.T) { ctx := context.Background() - cli, srv := newLocalConnPair(t, &Config{}, &Config{}) + cli, srv := NewLocalConnPair(t, &Config{}, &Config{}) data := makeTestData(1 << 20) srvdone := make(chan struct{}) @@ -61,11 +61,11 @@ func TestStreamTransfer(t *testing.T) { } } -func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { +func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() - l1 := newLocalListener(t, serverSide, conf1) - l2 := newLocalListener(t, clientSide, conf2) + l1 := NewLocalListener(t, serverSide, conf1) + l2 := NewLocalListener(t, clientSide, conf2) c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) if err != nil { t.Fatal(err) @@ -77,9 +77,11 @@ func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon return c2, c1 } -func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { +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) @@ -101,6 +103,7 @@ type testListener struct { conns map[*Conn]*testConn acceptQueue []*testConn configTransportParams []func(*transportParameters) + configTestConn []func(*testConn) sentDatagrams [][]byte peerTLSConn *tls.QUICConn lastInitialDstConnID []byte // for parsing Retry packets @@ -251,33 +254,6 @@ func (tl *testListener) wantIdle(expectation string) { } } -func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { - peerProvidedParams := defaultTransportParameters() - peerProvidedParams.initialSrcConnID = srcConnID - peerProvidedParams.originalDstConnID = dstConnID - for _, f := range tl.configTransportParams { - f(&peerProvidedParams) - } - - config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} - tl.peerTLSConn = tls.QUICClient(config) - tl.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) - tl.peerTLSConn.Start(context.Background()) - var data []byte - for { - e := tl.peerTLSConn.NextEvent() - switch e.Kind { - case tls.QUICNoEvent: - return data - case tls.QUICWriteData: - if e.Level != tls.QUICEncryptionLevelInitial { - tl.t.Fatal("initial data at unexpected level") - } - data = append(data, e.Data...) - } - } -} - // advance causes time to pass. func (tl *testListener) advance(d time.Duration) { tl.t.Helper() From 434956a1a8671fa67f9ec468cda2fd83937227b7 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 3 Nov 2023 16:43:48 -0700 Subject: [PATCH 15/22] quic: include more detail in connection close errors When closing a connection with an error, include a reason string in the CONNECTION_CLOSE frame as well as the error code, when the code isn't sufficient to explain the error. Change-Id: I055a4e11b222e87d1ff01d8c45fcb7cc17fe4196 Reviewed-on: https://go-review.googlesource.com/c/net/+/539342 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close.go | 4 +- internal/quic/conn_flow.go | 5 ++- internal/quic/conn_id.go | 61 ++++++++++++++++++++---------- internal/quic/conn_recv.go | 40 ++++++++++++++++---- internal/quic/conn_send.go | 2 +- internal/quic/conn_streams.go | 10 ++++- internal/quic/conn_test.go | 44 ++++++++++++++++++++- internal/quic/crypto_stream.go | 5 ++- internal/quic/errors.go | 10 ++++- internal/quic/listener.go | 2 +- internal/quic/packet_protection.go | 2 +- internal/quic/stream.go | 20 ++++++++-- internal/quic/stream_limits.go | 5 ++- internal/quic/stream_test.go | 3 +- internal/quic/transport_params.go | 26 ++++++------- 15 files changed, 178 insertions(+), 61 deletions(-) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index b8b86fd6f..daf425b76 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -156,7 +156,7 @@ func (c *Conn) enterDraining(err error) { if c.isDraining() { return } - if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != errNo { + 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 +220,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) 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 b77ad8edf..439c22123 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -210,25 +210,40 @@ func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p trans // the transient remote connection ID we chose (client) // or is empty (server). if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "original_destination_connection_id mismatch", + } } 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(errTransportParameter) + 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(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "client sent stateless_reset_token", + } } token := statelessResetToken(p.statelessResetToken) s.remote[0].resetToken = token @@ -255,17 +270,6 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) }, } } - 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, 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 @@ -294,7 +298,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // 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 { @@ -316,7 +323,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re } 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 } @@ -350,7 +360,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // 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 @@ -360,7 +373,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // 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 @@ -375,7 +391,10 @@ func (s *connIDState) retireRemote(rcid *remoteConnID) { 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 { diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index e966b7ef5..8fa3a3906 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -79,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 } @@ -129,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 } @@ -222,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. @@ -232,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 @@ -347,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:] @@ -360,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) @@ -521,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() { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 64e5d7548..22e780479 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -328,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 248be9641..c70c58ef0 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -594,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() @@ -603,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() @@ -613,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) { 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/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/listener.go b/internal/quic/listener.go index 24484eb6f..8b31dcbe8 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -107,7 +107,7 @@ func (l *Listener) Close(ctx context.Context) error { if !l.closing { l.closing = true for c := range l.conns { - c.Abort(localTransportError(errNo)) + c.Abort(localTransportError{code: errNo}) } if len(l.conns) == 0 { l.udpConn.Close() 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/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/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 From 45fa4142093c874c1b07cada45cf09b1dff803f8 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 10:12:03 -0800 Subject: [PATCH 16/22] quic: undo accidental rename of test helpers https://go.dev/cl/539341 inadvertently made the newLocalConnPair and newLocalListener helpers exported. These are test-only functions, so the change isn't really important, but undo the rename to keep them consistent with other test helpers. Change-Id: Ie3860db3584fc83c0c0aa2ad0dda4cc5cb03351a Reviewed-on: https://go-review.googlesource.com/c/net/+/540116 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/listener_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 674d4e4a1..037fb21b4 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -19,12 +19,12 @@ import ( ) func TestConnect(t *testing.T) { - NewLocalConnPair(t, &Config{}, &Config{}) + newLocalConnPair(t, &Config{}, &Config{}) } func TestStreamTransfer(t *testing.T) { ctx := context.Background() - cli, srv := NewLocalConnPair(t, &Config{}, &Config{}) + cli, srv := newLocalConnPair(t, &Config{}, &Config{}) data := makeTestData(1 << 20) srvdone := make(chan struct{}) @@ -61,11 +61,11 @@ func TestStreamTransfer(t *testing.T) { } } -func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { +func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() - l1 := NewLocalListener(t, serverSide, conf1) - l2 := NewLocalListener(t, clientSide, conf2) + l1 := newLocalListener(t, serverSide, conf1) + l2 := newLocalListener(t, clientSide, conf2) c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) if err != nil { t.Fatal(err) @@ -77,7 +77,7 @@ func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon return c2, c1 } -func NewLocalListener(t *testing.T, side connSide, conf *Config) *Listener { +func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { t.Helper() if conf.TLSConfig == nil { newConf := *conf From 39c9d01355726eae01c9adf4745b7f05e4734576 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 11:22:05 -0800 Subject: [PATCH 17/22] quic: don't send CONNECTION_CLOSE after stateless reset After receiving a stateless reset, we must enter the draining state and send no further packets (including CONNECTION_CLOSE). We were sending one last CONNECTION_CLOSE after the user closed the Conn; fix this. RFC 9000, Section 10.3.1. Change-Id: I6a9cc6019470a25476df518022a32eefe0c50fcd Reviewed-on: https://go-review.googlesource.com/c/net/+/540117 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close.go | 16 +++++++++++----- internal/quic/conn_recv.go | 10 +++++----- internal/quic/listener.go | 14 +++++++------- internal/quic/stateless_reset_test.go | 5 ++++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index daf425b76..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 && e.code != 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 @@ -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_recv.go b/internal/quic/conn_recv.go index 8fa3a3906..896c6d74e 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -56,7 +56,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen { var token statelessResetToken copy(token[:], buf[len(buf)-len(token):]) - c.handleStatelessReset(token) + c.handleStatelessReset(now, token) } // Invalid data at the end of a datagram is ignored. break @@ -525,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 } @@ -534,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 } @@ -556,9 +556,9 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa var errStatelessReset = errors.New("received stateless reset") -func (c *Conn) handleStatelessReset(resetToken statelessResetToken) { +func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) { if !c.connIDState.isValidStatelessResetToken(resetToken) { return } - c.enterDraining(errStatelessReset) + c.enterDraining(now, errStatelessReset) } diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 8b31dcbe8..ca8f9b25a 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -253,12 +253,18 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { 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(token) + c.handleStatelessReset(now, token) }) return } @@ -290,12 +296,6 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16 return } - var now time.Time - if l.testHooks != nil { - now = l.testHooks.timeNow() - } else { - now = time.Now() - } cids := newServerConnIDs{ srcConnID: p.srcConnID, dstConnID: p.dstConnID, diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go index b12e97560..8a16597c4 100644 --- a/internal/quic/stateless_reset_test.go +++ b/internal/quic/stateless_reset_test.go @@ -14,6 +14,7 @@ import ( "errors" "net/netip" "testing" + "time" ) func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { @@ -154,7 +155,9 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { t.Errorf("conn.Wait() = %v, want errStatelessReset", err) } - tc.wantIdle("closed connection is idle") + 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) { From 26ea8175a156eecf99974b64a6906b0fa4c76532 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 10 Oct 2023 15:32:20 +0200 Subject: [PATCH 18/22] http2: unconditionally recycle responseWriterState CL 46008 fixed golang/go#20704 by not recycling the responseWriterState if any previous Write call failed, as there could be outstanding goroutines referencing the responseWriterState memory. More recently, CL 467355 fixed a variant of the same issue by not referencing that memory after exiting the Write call. This fix supersedes the fix in CL 46008, as it is more general and does not require the caller to know whether any previous Write calls failed. This CL partially reverts CL 46008 just leaving the test case to ensure that golang/go#20704 does not regress. Change-Id: I18ea4d27420265a94cc7af21f1dffa3f7dc3bd34 Reviewed-on: https://go-review.googlesource.com/c/net/+/534315 TryBot-Result: Gopher Robot Auto-Submit: Damien Neil Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Run-TryBot: Quim Muntal Commit-Queue: Damien Neil Reviewed-by: Bryan Mills Reviewed-by: Dmitri Shuralyov --- http2/server.go | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/http2/server.go b/http2/server.go index 7f3bed926..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. From a7ef1a2680592dd6662ec0bde823885ae1f3fa5a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 15:22:28 -0800 Subject: [PATCH 19/22] internal/quic/cmd/interop: don't t.Log after test finishes Fixes golang/go#63971 Change-Id: I795356202880daa2d4a0cfd019c542e5820e8020 Reviewed-on: https://go-review.googlesource.com/c/net/+/539857 Reviewed-by: Bryan Mills Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI --- internal/quic/cmd/interop/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go index 6fd9c0f2d..4119740e6 100644 --- a/internal/quic/cmd/interop/main_test.go +++ b/internal/quic/cmd/interop/main_test.go @@ -72,9 +72,9 @@ func run(ctx context.Context, t *testing.T, name, testcase string, args []string addrc := make(chan string, 1) donec := make(chan struct{}) go func() { - defer t.Logf("%v done", name) defer close(addrc) defer close(donec) + defer t.Logf("%v done", name) s := bufio.NewScanner(out) for s.Scan() { line := s.Text() From a720b30cbc2733d7b4daaab7a8cac65f3ff5131d Mon Sep 17 00:00:00 2001 From: Jorropo Date: Sun, 5 Nov 2023 06:54:42 +0100 Subject: [PATCH 20/22] http2: allocate buffer pools using pointers to arrays This remove the allocation for the slice header in sync.Pool.New and putDataBufferChunk. It also divide the number of allocations kept alive by the pool. Change-Id: Icf493ebc568ae80a4e73e9768a6f1c7fce8e1365 Reviewed-on: https://go-review.googlesource.com/c/net/+/539915 Reviewed-by: Bryan Mills Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Brad Fitzpatrick Reviewed-by: Damien Neil TryBot-Result: Gopher Robot Run-TryBot: qiulaidongfeng <2645477756@qq.com> Reviewed-by: qiulaidongfeng <2645477756@qq.com> --- http2/databuffer.go | 59 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 28 deletions(-) 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. From cc6f4d19f58efdd0be1ead82856f76a446496516 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 6 Nov 2023 20:04:58 -0800 Subject: [PATCH 21/22] http2: remove ancient build-tagged files for unsupported Go versions x/net requires Go 1.18. No need to keep untested Go 1.11, Go 1.15, etc support around. Change-Id: I3588d273b543dec9ca120894ab36255f845abc20 Reviewed-on: https://go-review.googlesource.com/c/net/+/540236 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Heschi Kreinick Reviewed-by: Christopher Taylor Run-TryBot: Brad Fitzpatrick TryBot-Result: Gopher Robot --- http2/go111.go | 29 ------ http2/go115.go | 26 ------ http2/go118.go | 16 ---- http2/not_go111.go | 20 ---- http2/not_go115.go | 30 ------ http2/not_go118.go | 16 ---- http2/transport.go | 33 ++++++- http2/transport_go117_test.go | 168 ---------------------------------- http2/transport_test.go | 151 ++++++++++++++++++++++++++++++ 9 files changed, 183 insertions(+), 306 deletions(-) delete mode 100644 http2/go111.go delete mode 100644 http2/go115.go delete mode 100644 http2/go118.go delete mode 100644 http2/not_go111.go delete mode 100644 http2/not_go115.go delete mode 100644 http2/not_go118.go delete mode 100644 http2/transport_go117_test.go diff --git a/http2/go111.go b/http2/go111.go deleted file mode 100644 index 4ced74a0b..000000000 --- a/http2/go111.go +++ /dev/null @@ -1,29 +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 - -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 13a2d9215..000000000 --- a/http2/go115.go +++ /dev/null @@ -1,26 +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 - -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 a445ae1d5..000000000 --- a/http2/go118.go +++ /dev/null @@ -1,16 +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 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return tc.NetConn() -} diff --git a/http2/not_go111.go b/http2/not_go111.go deleted file mode 100644 index f4d63f458..000000000 --- a/http2/not_go111.go +++ /dev/null @@ -1,20 +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 - -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 635753408..000000000 --- a/http2/not_go115.go +++ /dev/null @@ -1,30 +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 - -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 b1b11c072..000000000 --- a/http2/not_go118.go +++ /dev/null @@ -1,16 +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 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return nil -} 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 0f257ad24..000000000 --- a/http2/transport_go117_test.go +++ /dev/null @@ -1,168 +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 - -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: + } +} From fbaf41277f28102c36926d1368dafbe2b54b4c1d Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 8 Nov 2023 19:30:54 +0000 Subject: [PATCH 22/22] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I828e052f9d32ff73d75d07087fcd25c8ed61d9de Reviewed-on: https://go-review.googlesource.com/c/net/+/540816 LUCI-TryBot-Result: Go LUCI Auto-Submit: Gopher Robot Reviewed-by: Carlos Amedee Reviewed-by: Dmitri Shuralyov --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index f83c0890a..21deffd4b 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net 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 ddbbdd3ef..54759e489 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -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/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=