mirror of
https://github.com/golang/net.git
synced 2026-03-31 02:17:08 +09:00
internal/http3: add support for plugging into net/http
This CL adds RegisterServer and RegisterTransport, which are used to plug our HTTP/3 implementation into net/http. For now, (inaccurately) assume that RegisterTransport do not take any configs for simplicity. Various exported symbols have also been changed to be unexported, since HTTP/3 will be used only via net/http and will not support direct usage. For golang/go#77440 Change-Id: I7b9528c193c18f5049da7f497d4259153b8770eb Reviewed-on: https://go-review.googlesource.com/c/net/+/739881 Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Nicholas Husin <husin@google.com>
This commit is contained in:
committed by
Nicholas Husin
parent
4a812844d8
commit
9767a42264
98
internal/http3/nethttp_test.go
Normal file
98
internal/http3/nethttp_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2026 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.27
|
||||
|
||||
package http3_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "unsafe" // for linkname
|
||||
|
||||
"golang.org/x/net/internal/http3"
|
||||
"golang.org/x/net/internal/testcert"
|
||||
"golang.org/x/net/quic"
|
||||
)
|
||||
|
||||
//go:linkname protocolSetHTTP3
|
||||
func protocolSetHTTP3(p *http.Protocols)
|
||||
|
||||
func newTestTLSConfig() *tls.Config {
|
||||
testCert := func() tls.Certificate {
|
||||
cert, err := tls.X509KeyPair(testcert.LocalhostCert, testcert.LocalhostKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cert
|
||||
}()
|
||||
config := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{testCert},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func TestNetHTTPIntegration(t *testing.T) {
|
||||
body := []byte("some body")
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(body)
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: "127.0.0.1:0",
|
||||
Handler: handler,
|
||||
TLSConfig: newTestTLSConfig(),
|
||||
}
|
||||
srv.Protocols = &http.Protocols{}
|
||||
protocolSetHTTP3(srv.Protocols)
|
||||
|
||||
var listenAddr string
|
||||
listenAddrSet := make(chan any)
|
||||
http3.RegisterServer(srv, http3.ServerOpts{
|
||||
ListenQUIC: func(addr string, config *quic.Config) (*quic.Endpoint, error) {
|
||||
e, err := quic.Listen("udp", addr, config)
|
||||
listenAddr = e.LocalAddr().String()
|
||||
listenAddrSet <- struct{}{}
|
||||
return e, err
|
||||
},
|
||||
})
|
||||
go func() {
|
||||
if err := srv.ListenAndServeTLS("", ""); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
tr := &http.Transport{TLSClientConfig: newTestTLSConfig()}
|
||||
tr.Protocols = &http.Protocols{}
|
||||
protocolSetHTTP3(tr.Protocols)
|
||||
http3.RegisterTransport(tr)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: time.Second,
|
||||
}
|
||||
<-listenAddrSet
|
||||
req, err := http.NewRequest("GET", "https://"+listenAddr, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("got %v err, want nil", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(b, body) {
|
||||
t.Errorf("got %v, want %v", string(b), string(body))
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
type roundTripState struct {
|
||||
cc *ClientConn
|
||||
cc *clientConn
|
||||
st *stream
|
||||
|
||||
// Request body, provided by the caller.
|
||||
@@ -87,7 +87,7 @@ func (rt *roundTripState) maybeCallWait100Continue() {
|
||||
}
|
||||
|
||||
// RoundTrip sends a request on the connection.
|
||||
func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) {
|
||||
func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) {
|
||||
// Each request gets its own QUIC stream.
|
||||
st, err := newConnStream(req.Context(), cc.qconn, streamTypeRequest)
|
||||
if err != nil {
|
||||
@@ -239,7 +239,7 @@ func actualContentLength(req *http.Request) int64 {
|
||||
|
||||
// writeBodyAndTrailer handles writing the body and trailer for a given
|
||||
// request, if any. This function will close the write direction of the stream.
|
||||
func (cc *ClientConn) writeBodyAndTrailer(rt *roundTripState, req *http.Request) {
|
||||
func (cc *clientConn) writeBodyAndTrailer(rt *roundTripState, req *http.Request) {
|
||||
defer rt.closeReqBody()
|
||||
|
||||
declaredTrailer := req.Trailer.Clone()
|
||||
@@ -339,7 +339,7 @@ func parseResponseContentLength(method string, statusCode int, h http.Header) (i
|
||||
return int64(contentLen), nil
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handleHeaders(st *stream) (statusCode int, h http.Header, err error) {
|
||||
func (cc *clientConn) handleHeaders(st *stream) (statusCode int, h http.Header, err error) {
|
||||
haveStatus := false
|
||||
cookie := ""
|
||||
// Issue #71374: Consider tracking the never-indexed status of headers
|
||||
@@ -406,7 +406,7 @@ func (cc *ClientConn) handleHeaders(st *stream) (statusCode int, h http.Header,
|
||||
return statusCode, h, err
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handlePushPromise(st *stream) error {
|
||||
func (cc *clientConn) handlePushPromise(st *stream) error {
|
||||
// "A client MUST treat receipt of a PUSH_PROMISE frame that contains a
|
||||
// larger push ID than the client has advertised as a connection error of H3_ID_ERROR."
|
||||
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.5-5
|
||||
|
||||
@@ -6,6 +6,7 @@ package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
@@ -20,52 +21,120 @@ import (
|
||||
"golang.org/x/net/quic"
|
||||
)
|
||||
|
||||
// A Server is an HTTP/3 server.
|
||||
// The zero value for Server is a valid server.
|
||||
type Server struct {
|
||||
// Handler to invoke for requests, http.DefaultServeMux if nil.
|
||||
Handler http.Handler
|
||||
// A server is an HTTP/3 server.
|
||||
// The zero value for server is a valid server.
|
||||
type server struct {
|
||||
// handler to invoke for requests, http.DefaultServeMux if nil.
|
||||
handler http.Handler
|
||||
|
||||
// Config is the QUIC configuration used by the server.
|
||||
// The Config may be nil.
|
||||
//
|
||||
// ListenAndServe may clone and modify the Config.
|
||||
// The Config must not be modified after calling ListenAndServe.
|
||||
Config *quic.Config
|
||||
config *quic.Config
|
||||
|
||||
listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)
|
||||
|
||||
initOnce sync.Once
|
||||
|
||||
serveCtx context.Context
|
||||
}
|
||||
|
||||
func (s *Server) init() {
|
||||
// netHTTPHandler is an interface that is implemented by
|
||||
// net/http.http3ServerHandler in std.
|
||||
//
|
||||
// It provides a way for information to be passed between x/net and net/http
|
||||
// that would otherwise be inaccessible, such as the TLS configs that users
|
||||
// have supplied to net/http servers.
|
||||
//
|
||||
// This allows us to integrate our HTTP/3 server implementation with the
|
||||
// net/http server when RegisterServer is called.
|
||||
type netHTTPHandler interface {
|
||||
http.Handler
|
||||
TLSConfig() *tls.Config
|
||||
BaseContext() context.Context
|
||||
Addr() string
|
||||
ListenErrHook(err error)
|
||||
}
|
||||
|
||||
type ServerOpts struct {
|
||||
// ListenQUIC determines how the server will open a QUIC endpoint.
|
||||
// By default, quic.Listen("udp", addr, config) is used.
|
||||
ListenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)
|
||||
|
||||
// QUICConfig is the QUIC configuration used by the server.
|
||||
// QUICConfig may be nil and should not be modified after calling
|
||||
// RegisterServer.
|
||||
// If QUICConfig.TLSConfig is nil, the TLSConfig of the net/http Server
|
||||
// given to RegisterServer will be used.
|
||||
QUICConfig *quic.Config
|
||||
}
|
||||
|
||||
// RegisterServer adds HTTP/3 support to a net/http Server.
|
||||
//
|
||||
// RegisterServer must be called before s begins serving, and only affects
|
||||
// s.ListenAndServeTLS.
|
||||
func RegisterServer(s *http.Server, opts ServerOpts) {
|
||||
if s.TLSNextProto == nil {
|
||||
s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
}
|
||||
s.TLSNextProto["http/3"] = func(s *http.Server, c *tls.Conn, h http.Handler) {
|
||||
stdHandler, ok := h.(netHTTPHandler)
|
||||
if !ok {
|
||||
panic("RegisterServer was given a server that does not implement netHTTPHandler")
|
||||
}
|
||||
if opts.QUICConfig == nil {
|
||||
opts.QUICConfig = &quic.Config{}
|
||||
}
|
||||
if opts.QUICConfig.TLSConfig == nil {
|
||||
opts.QUICConfig.TLSConfig = stdHandler.TLSConfig()
|
||||
}
|
||||
s3 := &server{
|
||||
config: opts.QUICConfig,
|
||||
listenQUIC: opts.ListenQUIC,
|
||||
handler: stdHandler,
|
||||
serveCtx: stdHandler.BaseContext(),
|
||||
}
|
||||
stdHandler.ListenErrHook(s3.listenAndServe(stdHandler.Addr()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) init() {
|
||||
s.initOnce.Do(func() {
|
||||
s.Config = initConfig(s.Config)
|
||||
if s.Handler == nil {
|
||||
s.Handler = http.DefaultServeMux
|
||||
s.config = initConfig(s.config)
|
||||
if s.handler == nil {
|
||||
s.handler = http.DefaultServeMux
|
||||
}
|
||||
if s.serveCtx == nil {
|
||||
s.serveCtx = context.Background()
|
||||
}
|
||||
if s.listenQUIC == nil {
|
||||
s.listenQUIC = func(addr string, config *quic.Config) (*quic.Endpoint, error) {
|
||||
return quic.Listen("udp", addr, config)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the UDP network address addr
|
||||
// listenAndServe listens on the UDP network address addr
|
||||
// and then calls Serve to handle requests on incoming connections.
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
func (s *server) listenAndServe(addr string) error {
|
||||
s.init()
|
||||
e, err := quic.Listen("udp", addr, s.Config)
|
||||
e, err := s.listenQUIC(addr, s.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(e)
|
||||
go s.serve(e)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the QUIC endpoint e,
|
||||
// serve accepts incoming connections on the QUIC endpoint e,
|
||||
// and handles requests from those connections.
|
||||
func (s *Server) Serve(e *quic.Endpoint) error {
|
||||
func (s *server) serve(e *quic.Endpoint) error {
|
||||
s.init()
|
||||
defer e.Close(s.serveCtx)
|
||||
for {
|
||||
qconn, err := e.Accept(context.Background())
|
||||
qconn, err := e.Accept(s.serveCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go newServerConn(qconn, s.Handler)
|
||||
go newServerConn(qconn, s.handler)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -934,7 +934,7 @@ func TestServer304NotModified(t *testing.T) {
|
||||
|
||||
type testServer struct {
|
||||
t testing.TB
|
||||
s *Server
|
||||
s *server
|
||||
tn testNet
|
||||
*testQUICEndpoint
|
||||
|
||||
@@ -957,16 +957,16 @@ func newTestServer(t testing.TB, handler http.Handler) *testServer {
|
||||
t.Helper()
|
||||
ts := &testServer{
|
||||
t: t,
|
||||
s: &Server{
|
||||
Config: &quic.Config{
|
||||
s: &server{
|
||||
config: &quic.Config{
|
||||
TLSConfig: testTLSConfig,
|
||||
},
|
||||
Handler: handler,
|
||||
handler: handler,
|
||||
},
|
||||
}
|
||||
e := ts.tn.newQUICEndpoint(t, ts.s.Config)
|
||||
e := ts.tn.newQUICEndpoint(t, ts.s.config)
|
||||
ts.addr = e.LocalAddr()
|
||||
go ts.s.Serve(e)
|
||||
go ts.s.serve(e)
|
||||
return ts
|
||||
}
|
||||
|
||||
|
||||
@@ -7,60 +7,92 @@ package http3
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/quic"
|
||||
)
|
||||
|
||||
// A Transport is an HTTP/3 transport.
|
||||
// A transport is an HTTP/3 transport.
|
||||
//
|
||||
// It does not manage a pool of connections,
|
||||
// and therefore does not implement net/http.RoundTripper.
|
||||
//
|
||||
// TODO: Provide a way to register an HTTP/3 transport with a net/http.Transport's
|
||||
// TODO: Provide a way to register an HTTP/3 transport with a net/http.transport's
|
||||
// connection pool.
|
||||
type Transport struct {
|
||||
// Endpoint is the QUIC endpoint used by connections created by the transport.
|
||||
type transport struct {
|
||||
// endpoint is the QUIC endpoint used by connections created by the transport.
|
||||
// If unset, it is initialized by the first call to Dial.
|
||||
Endpoint *quic.Endpoint
|
||||
endpoint *quic.Endpoint
|
||||
|
||||
// Config is the QUIC configuration used for client connections.
|
||||
// The Config may be nil.
|
||||
// config is the QUIC configuration used for client connections.
|
||||
// The config may be nil.
|
||||
//
|
||||
// Dial may clone and modify the Config.
|
||||
// The Config must not be modified after calling Dial.
|
||||
Config *quic.Config
|
||||
// Dial may clone and modify the config.
|
||||
// The config must not be modified after calling Dial.
|
||||
config *quic.Config
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
func (tr *Transport) init() error {
|
||||
// netHTTPTransport implements the net/http.dialClientConner interface,
|
||||
// allowing our HTTP/3 transport to integrate with net/http.
|
||||
type netHTTPTransport struct {
|
||||
*transport
|
||||
}
|
||||
|
||||
// RoundTrip is defined since Transport.RegisterProtocol takes in a
|
||||
// RoundTripper. However, this method will never be used as net/http's
|
||||
// dialClientConner interface does not have a RoundTrip method and will only
|
||||
// use DialClientConn to create a new RoundTripper.
|
||||
func (t netHTTPTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
panic("netHTTPTransport.RoundTrip should never be called")
|
||||
}
|
||||
|
||||
func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, _ func()) (http.RoundTripper, error) {
|
||||
return t.transport.dial(ctx, addr)
|
||||
}
|
||||
|
||||
// RegisterTransport configures a net/http HTTP/1 Transport to use HTTP/3.
|
||||
//
|
||||
// TODO: most likely, add another arg for transport configuration.
|
||||
func RegisterTransport(tr *http.Transport) {
|
||||
tr3 := &transport{
|
||||
config: &quic.Config{
|
||||
TLSConfig: tr.TLSClientConfig.Clone(),
|
||||
},
|
||||
}
|
||||
tr.RegisterProtocol("http/3", netHTTPTransport{tr3})
|
||||
}
|
||||
|
||||
func (tr *transport) init() error {
|
||||
tr.initOnce.Do(func() {
|
||||
tr.Config = initConfig(tr.Config)
|
||||
if tr.Endpoint == nil {
|
||||
tr.Endpoint, tr.initErr = quic.Listen("udp", ":0", nil)
|
||||
tr.config = initConfig(tr.config)
|
||||
if tr.endpoint == nil {
|
||||
tr.endpoint, tr.initErr = quic.Listen("udp", ":0", nil)
|
||||
}
|
||||
})
|
||||
return tr.initErr
|
||||
}
|
||||
|
||||
// Dial creates a new HTTP/3 client connection.
|
||||
func (tr *Transport) Dial(ctx context.Context, target string) (*ClientConn, error) {
|
||||
// dial creates a new HTTP/3 client connection.
|
||||
func (tr *transport) dial(ctx context.Context, target string) (*clientConn, error) {
|
||||
if err := tr.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qconn, err := tr.Endpoint.Dial(ctx, "udp", target, tr.Config)
|
||||
qconn, err := tr.endpoint.Dial(ctx, "udp", target, tr.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newClientConn(ctx, qconn)
|
||||
}
|
||||
|
||||
// A ClientConn is a client HTTP/3 connection.
|
||||
// A clientConn is a client HTTP/3 connection.
|
||||
//
|
||||
// Multiple goroutines may invoke methods on a ClientConn simultaneously.
|
||||
type ClientConn struct {
|
||||
// Multiple goroutines may invoke methods on a clientConn simultaneously.
|
||||
type clientConn struct {
|
||||
qconn *quic.Conn
|
||||
genericConn
|
||||
|
||||
@@ -68,8 +100,8 @@ type ClientConn struct {
|
||||
dec qpackDecoder
|
||||
}
|
||||
|
||||
func newClientConn(ctx context.Context, qconn *quic.Conn) (*ClientConn, error) {
|
||||
cc := &ClientConn{
|
||||
func newClientConn(ctx context.Context, qconn *quic.Conn) (*clientConn, error) {
|
||||
cc := &clientConn{
|
||||
qconn: qconn,
|
||||
}
|
||||
cc.enc.init()
|
||||
@@ -86,10 +118,10 @@ func newClientConn(ctx context.Context, qconn *quic.Conn) (*ClientConn, error) {
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
// close closes the connection.
|
||||
// Any in-flight requests are canceled.
|
||||
// Close does not wait for the peer to acknowledge the connection closing.
|
||||
func (cc *ClientConn) Close() error {
|
||||
// close does not wait for the peer to acknowledge the connection closing.
|
||||
func (cc *clientConn) close() error {
|
||||
// Close the QUIC connection immediately with a status of NO_ERROR.
|
||||
cc.qconn.Abort(nil)
|
||||
|
||||
@@ -99,7 +131,7 @@ func (cc *ClientConn) Close() error {
|
||||
return cc.qconn.Wait(ctx)
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handleControlStream(st *stream) error {
|
||||
func (cc *clientConn) handleControlStream(st *stream) error {
|
||||
// "A SETTINGS frame MUST be sent as the first frame of each control stream [...]"
|
||||
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2
|
||||
if err := st.readSettings(func(settingsType, settingsValue int64) error {
|
||||
@@ -145,17 +177,17 @@ func (cc *ClientConn) handleControlStream(st *stream) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handleEncoderStream(*stream) error {
|
||||
func (cc *clientConn) handleEncoderStream(*stream) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handleDecoderStream(*stream) error {
|
||||
func (cc *clientConn) handleDecoderStream(*stream) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handlePushStream(*stream) error {
|
||||
func (cc *clientConn) handlePushStream(*stream) error {
|
||||
// "A client MUST treat receipt of a push stream as a connection error
|
||||
// of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]"
|
||||
// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3
|
||||
@@ -165,7 +197,7 @@ func (cc *ClientConn) handlePushStream(*stream) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) handleRequestStream(st *stream) error {
|
||||
func (cc *clientConn) handleRequestStream(st *stream) error {
|
||||
// "Clients MUST treat receipt of a server-initiated bidirectional
|
||||
// stream as a connection error of type H3_STREAM_CREATION_ERROR [...]"
|
||||
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3
|
||||
@@ -176,7 +208,7 @@ func (cc *ClientConn) handleRequestStream(st *stream) error {
|
||||
}
|
||||
|
||||
// abort closes the connection with an error.
|
||||
func (cc *ClientConn) abort(err error) {
|
||||
func (cc *clientConn) abort(err error) {
|
||||
if e, ok := err.(*connectionError); ok {
|
||||
cc.qconn.Abort(&quic.ApplicationError{
|
||||
Code: uint64(e.code),
|
||||
|
||||
@@ -370,8 +370,8 @@ func (ts *testQUICStream) Flush() error {
|
||||
|
||||
// A testClientConn is a ClientConn on a test network.
|
||||
type testClientConn struct {
|
||||
tr *Transport
|
||||
cc *ClientConn
|
||||
tr *transport
|
||||
cc *clientConn
|
||||
|
||||
// *testQUICConn is the server half of the connection.
|
||||
*testQUICConn
|
||||
@@ -380,19 +380,19 @@ type testClientConn struct {
|
||||
|
||||
func newTestClientConn(t testing.TB) *testClientConn {
|
||||
e1, e2 := newQUICEndpointPair(t)
|
||||
tr := &Transport{
|
||||
Endpoint: e1,
|
||||
Config: &quic.Config{
|
||||
tr := &transport{
|
||||
endpoint: e1,
|
||||
config: &quic.Config{
|
||||
TLSConfig: testTLSConfig,
|
||||
},
|
||||
}
|
||||
|
||||
cc, err := tr.Dial(t.Context(), e2.LocalAddr().String())
|
||||
cc, err := tr.dial(t.Context(), e2.LocalAddr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
cc.Close()
|
||||
cc.close()
|
||||
})
|
||||
srvConn, err := e2.Accept(t.Context())
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user