fix(netxlite): prefer composition over embedding (#733)

This diff has been extracted and adapted from 8848c8c516

The reason to prefer composition over embedding is that we want the
build to break if we add new methods to interfaces we define. If the build
does not break, we may forget about wrapping methods we should
actually be wrapping. I noticed this issue inside netxlite when I was working
on websteps-illustrated and I added support for NS and PTR queries.

See https://github.com/ooni/probe/issues/2096

While there, perform comprehensive netxlite code review
and apply minor changes and improve the docs.
This commit is contained in:
Simone Basso 2022-05-15 19:25:27 +02:00 committed by GitHub
parent 9d2301cae2
commit c1b06a2d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 328 additions and 92 deletions

View File

@ -18,7 +18,7 @@ import (
// function a non-IP address causes it to return true. // function a non-IP address causes it to return true.
func IsBogon(address string) bool { func IsBogon(address string) bool {
ip := net.ParseIP(address) ip := net.ParseIP(address)
return ip == nil || isPrivate(address, ip) return ip == nil || isBogon(address, ip)
} }
// IsLoopback returns whether an IP address is loopback. Passing to this // IsLoopback returns whether an IP address is loopback. Passing to this
@ -33,7 +33,7 @@ var (
bogons6 []*net.IPNet bogons6 []*net.IPNet
) )
func expandbogons(cidrs []string) (out []*net.IPNet) { func expandBogons(cidrs []string) (out []*net.IPNet) {
for _, cidr := range cidrs { for _, cidr := range cidrs {
_, block, err := net.ParseCIDR(cidr) _, block, err := net.ParseCIDR(cidr)
runtimex.PanicOnError(err, "net.ParseCIDR failed") runtimex.PanicOnError(err, "net.ParseCIDR failed")
@ -43,7 +43,7 @@ func expandbogons(cidrs []string) (out []*net.IPNet) {
} }
func init() { func init() {
bogons4 = append(bogons4, expandbogons([]string{ bogons4 = append(bogons4, expandBogons([]string{
// //
// List extracted from https://ipinfo.io/bogon // List extracted from https://ipinfo.io/bogon
// //
@ -64,7 +64,7 @@ func init() {
"240.0.0.0/4", // Reserved for future use "240.0.0.0/4", // Reserved for future use
"255.255.255.255/32", // Limited broadcast "255.255.255.255/32", // Limited broadcast
})...) })...)
bogons6 = append(bogons6, expandbogons([]string{ bogons6 = append(bogons6, expandBogons([]string{
// //
// List extracted from https://ipinfo.io/bogon // List extracted from https://ipinfo.io/bogon
// //
@ -110,7 +110,10 @@ func init() {
})...) })...)
} }
func isPrivate(address string, ip net.IP) bool { // isBogon implements IsBogon
func isBogon(address string, ip net.IP) bool {
// TODO(bassosimone): the following check is probably redundant given that these
// three checks are already included into the list of bogons.
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true return true
} }

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Mapping Go errors to OONI errors
//
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
@ -38,8 +42,7 @@ func classifyGenericError(err error) string {
// The list returned here matches the values used by MK unless // The list returned here matches the values used by MK unless
// explicitly noted otherwise with a comment. // explicitly noted otherwise with a comment.
// QUIRK: we cannot remove this check as long as this function // Robustness: handle the case where we're passed a wrapped error.
// is exported and used independently from NewErrWrapper.
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it return errwrapper.Error() // we've already wrapped it
@ -146,8 +149,7 @@ const (
// and returns to the caller its return value. // and returns to the caller its return value.
func classifyQUICHandshakeError(err error) string { func classifyQUICHandshakeError(err error) string {
// QUIRK: we cannot remove this check as long as this function // Robustness: handle the case where we're passed a wrapped error.
// is exported and used independently from NewErrWrapper.
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it return errwrapper.Error() // we've already wrapped it
@ -269,8 +271,7 @@ var (
// returns to the caller its return value. // returns to the caller its return value.
func classifyResolverError(err error) string { func classifyResolverError(err error) string {
// QUIRK: we cannot remove this check as long as this function // Robustness: handle the case where we're passed a wrapped error.
// is exported and used independently from NewErrWrapper.
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it return errwrapper.Error() // we've already wrapped it
@ -303,8 +304,7 @@ func classifyResolverError(err error) string {
// returns to the caller its return value. // returns to the caller its return value.
func classifyTLSHandshakeError(err error) string { func classifyTLSHandshakeError(err error) string {
// QUIRK: we cannot remove this check as long as this function // Robustness: handle the case where we're passed a wrapped error.
// is exported and used independently from NewErrWrapper.
var errwrapper *ErrWrapper var errwrapper *ErrWrapper
if errors.As(err, &errwrapper) { if errors.As(err, &errwrapper) {
return errwrapper.Error() // we've already wrapped it return errwrapper.Error() // we've already wrapped it

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Code for dialing TCP or UDP net.Conn-like connections
//
import ( import (
"context" "context"
"errors" "errors"
@ -96,8 +100,8 @@ func (d *dialerSystem) CloseIdleConnections() {
// dialerResolver combines dialing with domain name resolution. // dialerResolver combines dialing with domain name resolution.
type dialerResolver struct { type dialerResolver struct {
model.Dialer Dialer model.Dialer
model.Resolver Resolver model.Resolver
} }
var _ model.Dialer = &dialerResolver{} var _ model.Dialer = &dialerResolver{}
@ -144,10 +148,10 @@ func (d *dialerResolver) CloseIdleConnections() {
// dialerLogger is a Dialer with logging. // dialerLogger is a Dialer with logging.
type dialerLogger struct { type dialerLogger struct {
// Dialer is the underlying dialer. // Dialer is the underlying dialer.
model.Dialer Dialer model.Dialer
// Logger is the underlying logger. // DebugLogger is the underlying logger.
model.DebugLogger DebugLogger model.DebugLogger
// operationSuffix is appended to the operation name. // operationSuffix is appended to the operation name.
// //
@ -194,15 +198,15 @@ func NewSingleUseDialer(conn net.Conn) model.Dialer {
// dialerSingleUse is the Dialer returned by NewSingleDialer. // dialerSingleUse is the Dialer returned by NewSingleDialer.
type dialerSingleUse struct { type dialerSingleUse struct {
sync.Mutex mu sync.Mutex
conn net.Conn conn net.Conn
} }
var _ model.Dialer = &dialerSingleUse{} var _ model.Dialer = &dialerSingleUse{}
func (s *dialerSingleUse) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { func (s *dialerSingleUse) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
defer s.Unlock() defer s.mu.Unlock()
s.Lock() s.mu.Lock()
if s.conn == nil { if s.conn == nil {
return nil, ErrNoConnReuse return nil, ErrNoConnReuse
} }
@ -218,7 +222,7 @@ func (s *dialerSingleUse) CloseIdleConnections() {
// dialerErrWrapper is a dialer that performs error wrapping. The connection // dialerErrWrapper is a dialer that performs error wrapping. The connection
// returned by the DialContext function will also perform error wrapping. // returned by the DialContext function will also perform error wrapping.
type dialerErrWrapper struct { type dialerErrWrapper struct {
model.Dialer Dialer model.Dialer
} }
var _ model.Dialer = &dialerErrWrapper{} var _ model.Dialer = &dialerErrWrapper{}
@ -226,11 +230,15 @@ var _ model.Dialer = &dialerErrWrapper{}
func (d *dialerErrWrapper) DialContext(ctx context.Context, network, address string) (net.Conn, error) { func (d *dialerErrWrapper) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d.Dialer.DialContext(ctx, network, address) conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil { if err != nil {
return nil, NewErrWrapper(classifyGenericError, ConnectOperation, err) return nil, newErrWrapper(classifyGenericError, ConnectOperation, err)
} }
return &dialerErrWrapperConn{Conn: conn}, nil return &dialerErrWrapperConn{Conn: conn}, nil
} }
func (d *dialerErrWrapper) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
}
// dialerErrWrapperConn is a net.Conn that performs error wrapping. // dialerErrWrapperConn is a net.Conn that performs error wrapping.
type dialerErrWrapperConn struct { type dialerErrWrapperConn struct {
net.Conn net.Conn
@ -241,7 +249,7 @@ var _ net.Conn = &dialerErrWrapperConn{}
func (c *dialerErrWrapperConn) Read(b []byte) (int, error) { func (c *dialerErrWrapperConn) Read(b []byte) (int, error) {
count, err := c.Conn.Read(b) count, err := c.Conn.Read(b)
if err != nil { if err != nil {
return 0, NewErrWrapper(classifyGenericError, ReadOperation, err) return 0, newErrWrapper(classifyGenericError, ReadOperation, err)
} }
return count, nil return count, nil
} }
@ -249,7 +257,7 @@ func (c *dialerErrWrapperConn) Read(b []byte) (int, error) {
func (c *dialerErrWrapperConn) Write(b []byte) (int, error) { func (c *dialerErrWrapperConn) Write(b []byte) (int, error) {
count, err := c.Conn.Write(b) count, err := c.Conn.Write(b)
if err != nil { if err != nil {
return 0, NewErrWrapper(classifyGenericError, WriteOperation, err) return 0, newErrWrapper(classifyGenericError, WriteOperation, err)
} }
return count, nil return count, nil
} }
@ -257,7 +265,7 @@ func (c *dialerErrWrapperConn) Write(b []byte) (int, error) {
func (c *dialerErrWrapperConn) Close() error { func (c *dialerErrWrapperConn) Close() error {
err := c.Conn.Close() err := c.Conn.Close()
if err != nil { if err != nil {
return NewErrWrapper(classifyGenericError, CloseOperation, err) return newErrWrapper(classifyGenericError, CloseOperation, err)
} }
return nil return nil
} }

View File

@ -253,7 +253,7 @@ func TestDialerResolver(t *testing.T) {
mu := &sync.Mutex{} mu := &sync.Mutex{}
errorsList := []error{ errorsList := []error{
errors.New("a mocked error"), errors.New("a mocked error"),
NewErrWrapper( newErrWrapper(
classifyGenericError, classifyGenericError,
CloseOperation, CloseOperation,
io.EOF, io.EOF,
@ -295,7 +295,7 @@ func TestDialerResolver(t *testing.T) {
mu := &sync.Mutex{} mu := &sync.Mutex{}
errorsList := []error{ errorsList := []error{
errors.New("a mocked error"), errors.New("a mocked error"),
NewErrWrapper( newErrWrapper(
classifyGenericError, classifyGenericError,
CloseOperation, CloseOperation,
errors.New("antani"), errors.New("antani"),

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Decode byte arrays to DNS messages
//
import ( import (
"errors" "errors"

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Encode DNS queries to byte arrays
//
import ( import (
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// DNS-over-HTTPS transport
//
import ( import (
"bytes" "bytes"
"context" "context"

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// DNS-over-{TCP,TLS} transport
//
import ( import (
"context" "context"
"errors" "errors"

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// DNS-over-UDP transport
//
import ( import (
"context" "context"
"time" "time"

View File

@ -1 +0,0 @@
package netxlite

View File

@ -3,7 +3,7 @@
// This package is the basic networking building block that you // This package is the basic networking building block that you
// should be using every time you need networking. // should be using every time you need networking.
// //
// It implements interfaces defined in the internal/model package. // It implements interfaces defined in internal/model/netx.go.
// //
// You should consider checking the tutorial explaining how to use this package // You should consider checking the tutorial explaining how to use this package
// for network measurements: https://github.com/ooni/probe-cli/tree/master/internal/tutorial/netxlite. // for network measurements: https://github.com/ooni/probe-cli/tree/master/internal/tutorial/netxlite.

View File

@ -66,12 +66,12 @@ func (e *ErrWrapper) MarshalJSON() ([]byte, error) {
return json.Marshal(e.Failure) return json.Marshal(e.Failure)
} }
// Classifier is the type of the function that maps a Go error // classifier is the type of the function that maps a Go error
// to a OONI failure string defined at // to a OONI failure string defined at
// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md. // https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md.
type Classifier func(err error) string type classifier func(err error) string
// NewErrWrapper creates a new ErrWrapper using the given // newErrWrapper creates a new ErrWrapper using the given
// classifier, operation name, and underlying error. // classifier, operation name, and underlying error.
// //
// This function panics if classifier is nil, or operation // This function panics if classifier is nil, or operation
@ -81,7 +81,7 @@ type Classifier func(err error) string
// error wrapper will use the same classification string and // error wrapper will use the same classification string and
// will determine whether to keep the major operation as documented // will determine whether to keep the major operation as documented
// in the ErrWrapper.Operation documentation. // in the ErrWrapper.Operation documentation.
func NewErrWrapper(c Classifier, op string, err error) *ErrWrapper { func newErrWrapper(c classifier, op string, err error) *ErrWrapper {
var wrapper *ErrWrapper var wrapper *ErrWrapper
if errors.As(err, &wrapper) { if errors.As(err, &wrapper) {
return &ErrWrapper{ return &ErrWrapper{
@ -107,13 +107,15 @@ func NewErrWrapper(c Classifier, op string, err error) *ErrWrapper {
} }
// NewTopLevelGenericErrWrapper wraps an error occurring at top // NewTopLevelGenericErrWrapper wraps an error occurring at top
// level using ClassifyGenericError as classifier. // level using a generic classifier as classifier. This is the
// function you should call when you suspect a given error hasn't
// already been wrapped. This function panics if err is nil.
// //
// If the err argument has already been classified, the returned // If the err argument has already been classified, the returned
// error wrapper will use the same classification string and // error wrapper will use the same classification string and
// failed operation of the original error. // failed operation of the original error.
func NewTopLevelGenericErrWrapper(err error) *ErrWrapper { func NewTopLevelGenericErrWrapper(err error) *ErrWrapper {
return NewErrWrapper(classifyGenericError, TopLevelOperation, err) return newErrWrapper(classifyGenericError, TopLevelOperation, err)
} }
func classifyOperation(ew *ErrWrapper, operation string) string { func classifyOperation(ew *ErrWrapper, operation string) string {

View File

@ -53,7 +53,7 @@ func TestNewErrWrapper(t *testing.T) {
recovered.Add(1) recovered.Add(1)
} }
}() }()
NewErrWrapper(nil, CloseOperation, io.EOF) newErrWrapper(nil, CloseOperation, io.EOF)
}() }()
if recovered.Load() != 1 { if recovered.Load() != 1 {
t.Fatal("did not panic") t.Fatal("did not panic")
@ -68,7 +68,7 @@ func TestNewErrWrapper(t *testing.T) {
recovered.Add(1) recovered.Add(1)
} }
}() }()
NewErrWrapper(classifyGenericError, "", io.EOF) newErrWrapper(classifyGenericError, "", io.EOF)
}() }()
if recovered.Load() != 1 { if recovered.Load() != 1 {
t.Fatal("did not panic") t.Fatal("did not panic")
@ -83,7 +83,7 @@ func TestNewErrWrapper(t *testing.T) {
recovered.Add(1) recovered.Add(1)
} }
}() }()
NewErrWrapper(classifyGenericError, CloseOperation, nil) newErrWrapper(classifyGenericError, CloseOperation, nil)
}() }()
if recovered.Load() != 1 { if recovered.Load() != 1 {
t.Fatal("did not panic") t.Fatal("did not panic")
@ -91,7 +91,7 @@ func TestNewErrWrapper(t *testing.T) {
}) })
t.Run("otherwise, works as intended", func(t *testing.T) { t.Run("otherwise, works as intended", func(t *testing.T) {
ew := NewErrWrapper(classifyGenericError, CloseOperation, io.EOF) ew := newErrWrapper(classifyGenericError, CloseOperation, io.EOF)
if ew.Failure != FailureEOFError { if ew.Failure != FailureEOFError {
t.Fatal("unexpected failure") t.Fatal("unexpected failure")
} }
@ -104,10 +104,10 @@ func TestNewErrWrapper(t *testing.T) {
}) })
t.Run("when the underlying error is already a wrapped error", func(t *testing.T) { t.Run("when the underlying error is already a wrapped error", func(t *testing.T) {
ew := NewErrWrapper(classifySyscallError, ReadOperation, ECONNRESET) ew := newErrWrapper(classifySyscallError, ReadOperation, ECONNRESET)
var err1 error = ew var err1 error = ew
err2 := fmt.Errorf("cannot read: %w", err1) err2 := fmt.Errorf("cannot read: %w", err1)
ew2 := NewErrWrapper(classifyGenericError, HTTPRoundTripOperation, err2) ew2 := newErrWrapper(classifyGenericError, HTTPRoundTripOperation, err2)
if ew2.Failure != ew.Failure { if ew2.Failure != ew.Failure {
t.Fatal("not the same failure") t.Fatal("not the same failure")
} }

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// HTTP/1.1 and HTTP2 code
//
import ( import (
"context" "context"
"errors" "errors"
@ -13,9 +17,11 @@ import (
// httpTransportErrWrapper is an HTTPTransport with error wrapping. // httpTransportErrWrapper is an HTTPTransport with error wrapping.
type httpTransportErrWrapper struct { type httpTransportErrWrapper struct {
model.HTTPTransport HTTPTransport model.HTTPTransport
} }
var _ model.HTTPTransport = &httpTransportErrWrapper{}
func (txp *httpTransportErrWrapper) RoundTrip(req *http.Request) (*http.Response, error) { func (txp *httpTransportErrWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := txp.HTTPTransport.RoundTrip(req) resp, err := txp.HTTPTransport.RoundTrip(req)
if err != nil { if err != nil {
@ -24,10 +30,18 @@ func (txp *httpTransportErrWrapper) RoundTrip(req *http.Request) (*http.Response
return resp, nil return resp, nil
} }
func (txp *httpTransportErrWrapper) CloseIdleConnections() {
txp.HTTPTransport.CloseIdleConnections()
}
func (txp *httpTransportErrWrapper) Network() string {
return txp.HTTPTransport.Network()
}
// httpTransportLogger is an HTTPTransport with logging. // httpTransportLogger is an HTTPTransport with logging.
type httpTransportLogger struct { type httpTransportLogger struct {
// HTTPTransport is the underlying HTTP transport. // HTTPTransport is the underlying HTTP transport.
model.HTTPTransport HTTPTransport model.HTTPTransport
// Logger is the underlying logger. // Logger is the underlying logger.
Logger model.DebugLogger Logger model.DebugLogger
@ -62,12 +76,26 @@ func (txp *httpTransportLogger) CloseIdleConnections() {
txp.HTTPTransport.CloseIdleConnections() txp.HTTPTransport.CloseIdleConnections()
} }
func (txp *httpTransportLogger) Network() string {
return txp.HTTPTransport.Network()
}
// httpTransportConnectionsCloser is an HTTPTransport that // httpTransportConnectionsCloser is an HTTPTransport that
// correctly forwards CloseIdleConnections calls. // correctly forwards CloseIdleConnections calls.
type httpTransportConnectionsCloser struct { type httpTransportConnectionsCloser struct {
model.HTTPTransport HTTPTransport model.HTTPTransport
model.Dialer Dialer model.Dialer
model.TLSDialer TLSDialer model.TLSDialer
}
var _ model.HTTPTransport = &httpTransportConnectionsCloser{}
func (txp *httpTransportConnectionsCloser) RoundTrip(req *http.Request) (*http.Response, error) {
return txp.HTTPTransport.RoundTrip(req)
}
func (txp *httpTransportConnectionsCloser) Network() string {
return txp.HTTPTransport.Network()
} }
// CloseIdleConnections forwards the CloseIdleConnections calls. // CloseIdleConnections forwards the CloseIdleConnections calls.
@ -148,7 +176,17 @@ func NewOOHTTPBaseTransport(dialer model.Dialer, tlsDialer model.TLSDialer) mode
// stdlibTransport wraps oohttp.StdlibTransport to add .Network() // stdlibTransport wraps oohttp.StdlibTransport to add .Network()
type stdlibTransport struct { type stdlibTransport struct {
*oohttp.StdlibTransport StdlibTransport *oohttp.StdlibTransport
}
var _ model.HTTPTransport = &stdlibTransport{}
func (txp *stdlibTransport) CloseIdleConnections() {
txp.StdlibTransport.CloseIdleConnections()
}
func (txp *stdlibTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return txp.StdlibTransport.RoundTrip(req)
} }
// Network implements HTTPTransport.Network. // Network implements HTTPTransport.Network.
@ -170,7 +208,13 @@ func WrapHTTPTransport(logger model.DebugLogger, txp model.HTTPTransport) model.
// httpDialerWithReadTimeout enforces a read timeout for all HTTP // httpDialerWithReadTimeout enforces a read timeout for all HTTP
// connections. See https://github.com/ooni/probe/issues/1609. // connections. See https://github.com/ooni/probe/issues/1609.
type httpDialerWithReadTimeout struct { type httpDialerWithReadTimeout struct {
model.Dialer Dialer model.Dialer
}
var _ model.Dialer = &httpDialerWithReadTimeout{}
func (d *httpDialerWithReadTimeout) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
} }
// DialContext implements Dialer.DialContext. // DialContext implements Dialer.DialContext.
@ -186,7 +230,13 @@ func (d *httpDialerWithReadTimeout) DialContext(
// httpTLSDialerWithReadTimeout enforces a read timeout for all HTTP // httpTLSDialerWithReadTimeout enforces a read timeout for all HTTP
// connections. See https://github.com/ooni/probe/issues/1609. // connections. See https://github.com/ooni/probe/issues/1609.
type httpTLSDialerWithReadTimeout struct { type httpTLSDialerWithReadTimeout struct {
model.TLSDialer TLSDialer model.TLSDialer
}
var _ model.TLSDialer = &httpTLSDialerWithReadTimeout{}
func (d *httpTLSDialerWithReadTimeout) CloseIdleConnections() {
d.TLSDialer.CloseIdleConnections()
} }
// ErrNotTLSConn occur when an interface accepts a net.Conn but // ErrNotTLSConn occur when an interface accepts a net.Conn but
@ -280,7 +330,7 @@ func WrapHTTPClient(clnt model.HTTPClient) model.HTTPClient {
} }
type httpClientErrWrapper struct { type httpClientErrWrapper struct {
model.HTTPClient HTTPClient model.HTTPClient
} }
func (c *httpClientErrWrapper) Do(req *http.Request) (*http.Response, error) { func (c *httpClientErrWrapper) Do(req *http.Request) (*http.Response, error) {
@ -290,3 +340,7 @@ func (c *httpClientErrWrapper) Do(req *http.Request) (*http.Response, error) {
} }
return resp, nil return resp, nil
} }
func (c *httpClientErrWrapper) CloseIdleConnections() {
c.HTTPClient.CloseIdleConnections()
}

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// HTTP3 code
//
import ( import (
"crypto/tls" "crypto/tls"
"io" "io"

View File

@ -252,19 +252,19 @@ func TestNewHTTPTransport(t *testing.T) {
t.Fatal("invalid tls dialer") t.Fatal("invalid tls dialer")
} }
stdlib := connectionsCloser.HTTPTransport.(*stdlibTransport) stdlib := connectionsCloser.HTTPTransport.(*stdlibTransport)
if !stdlib.Transport.ForceAttemptHTTP2 { if !stdlib.StdlibTransport.ForceAttemptHTTP2 {
t.Fatal("invalid ForceAttemptHTTP2") t.Fatal("invalid ForceAttemptHTTP2")
} }
if !stdlib.Transport.DisableCompression { if !stdlib.StdlibTransport.DisableCompression {
t.Fatal("invalid DisableCompression") t.Fatal("invalid DisableCompression")
} }
if stdlib.Transport.MaxConnsPerHost != 1 { if stdlib.StdlibTransport.MaxConnsPerHost != 1 {
t.Fatal("invalid MaxConnPerHost") t.Fatal("invalid MaxConnPerHost")
} }
if stdlib.Transport.DialTLSContext == nil { if stdlib.StdlibTransport.DialTLSContext == nil {
t.Fatal("invalid DialTLSContext") t.Fatal("invalid DialTLSContext")
} }
if stdlib.Transport.DialContext == nil { if stdlib.StdlibTransport.DialContext == nil {
t.Fatal("invalid DialContext") t.Fatal("invalid DialContext")
} }
}) })
@ -500,6 +500,20 @@ func TestHTTPClientErrWrapper(t *testing.T) {
} }
}) })
}) })
t.Run("CloseIdleConnections", func(t *testing.T) {
var called bool
child := &mocks.HTTPClient{
MockCloseIdleConnections: func() {
called = true
},
}
clnt := &httpClientErrWrapper{child}
clnt.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
})
} }
func TestNewHTTPClientStdlib(t *testing.T) { func TestNewHTTPClientStdlib(t *testing.T) {

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// I/O extensions
//
import ( import (
"context" "context"
"errors" "errors"

View File

@ -41,7 +41,7 @@ func TestReadAllContext(t *testing.T) {
// //
// Note: Returning a wrapped error to ensure we address // Note: Returning a wrapped error to ensure we address
// https://github.com/ooni/probe/issues/1965 // https://github.com/ooni/probe/issues/1965
return len(b), NewErrWrapper(classifyGenericError, return len(b), newErrWrapper(classifyGenericError,
ReadOperation, io.EOF) ReadOperation, io.EOF)
}, },
} }
@ -171,7 +171,7 @@ func TestCopyContext(t *testing.T) {
// //
// Note: Returning a wrapped error to ensure we address // Note: Returning a wrapped error to ensure we address
// https://github.com/ooni/probe/issues/1965 // https://github.com/ooni/probe/issues/1965
return len(b), NewErrWrapper(classifyGenericError, return len(b), newErrWrapper(classifyGenericError,
ReadOperation, io.EOF) ReadOperation, io.EOF)
}, },
} }

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Legacy code
//
// These vars export internal names to legacy ooni/probe-cli code. // These vars export internal names to legacy ooni/probe-cli code.
// //
// Deprecated: do not use these names in new code. // Deprecated: do not use these names in new code.

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Names of operations
//
// Operations that we measure. They are the possible values of // Operations that we measure. They are the possible values of
// the ErrWrapper.Operation field. // the ErrWrapper.Operation field.
const ( const (

View File

@ -1,7 +1,7 @@
package netxlite package netxlite
// //
// Parallel resolver implementation // Parallel DNS resolver implementation
// //
import ( import (

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// QUIC implementation
//
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
@ -340,7 +344,7 @@ func NewSingleUseQUICDialer(qconn quic.EarlyConnection) model.QUICDialer {
// quicDialerSingleUse is the QUICDialer returned by NewSingleQUICDialer. // quicDialerSingleUse is the QUICDialer returned by NewSingleQUICDialer.
type quicDialerSingleUse struct { type quicDialerSingleUse struct {
sync.Mutex mu sync.Mutex
qconn quic.EarlyConnection qconn quic.EarlyConnection
} }
@ -351,8 +355,8 @@ func (s *quicDialerSingleUse) DialContext(
ctx context.Context, network, addr string, tlsCfg *tls.Config, ctx context.Context, network, addr string, tlsCfg *tls.Config,
cfg *quic.Config) (quic.EarlyConnection, error) { cfg *quic.Config) (quic.EarlyConnection, error) {
var qconn quic.EarlyConnection var qconn quic.EarlyConnection
defer s.Unlock() defer s.mu.Unlock()
s.Lock() s.mu.Lock()
if s.qconn == nil { if s.qconn == nil {
return nil, ErrNoConnReuse return nil, ErrNoConnReuse
} }
@ -368,7 +372,7 @@ func (s *quicDialerSingleUse) CloseIdleConnections() {
// quicListenerErrWrapper is a QUICListener that wraps errors. // quicListenerErrWrapper is a QUICListener that wraps errors.
type quicListenerErrWrapper struct { type quicListenerErrWrapper struct {
// QUICListener is the underlying listener. // QUICListener is the underlying listener.
model.QUICListener QUICListener model.QUICListener
} }
var _ model.QUICListener = &quicListenerErrWrapper{} var _ model.QUICListener = &quicListenerErrWrapper{}
@ -377,7 +381,7 @@ var _ model.QUICListener = &quicListenerErrWrapper{}
func (qls *quicListenerErrWrapper) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) { func (qls *quicListenerErrWrapper) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
pconn, err := qls.QUICListener.Listen(addr) pconn, err := qls.QUICListener.Listen(addr)
if err != nil { if err != nil {
return nil, NewErrWrapper(classifyGenericError, QUICListenOperation, err) return nil, newErrWrapper(classifyGenericError, QUICListenOperation, err)
} }
return &quicErrWrapperUDPLikeConn{pconn}, nil return &quicErrWrapperUDPLikeConn{pconn}, nil
} }
@ -394,7 +398,7 @@ var _ model.UDPLikeConn = &quicErrWrapperUDPLikeConn{}
func (c *quicErrWrapperUDPLikeConn) WriteTo(p []byte, addr net.Addr) (int, error) { func (c *quicErrWrapperUDPLikeConn) WriteTo(p []byte, addr net.Addr) (int, error) {
count, err := c.UDPLikeConn.WriteTo(p, addr) count, err := c.UDPLikeConn.WriteTo(p, addr)
if err != nil { if err != nil {
return 0, NewErrWrapper(classifyGenericError, WriteToOperation, err) return 0, newErrWrapper(classifyGenericError, WriteToOperation, err)
} }
return count, nil return count, nil
} }
@ -403,7 +407,7 @@ func (c *quicErrWrapperUDPLikeConn) WriteTo(p []byte, addr net.Addr) (int, error
func (c *quicErrWrapperUDPLikeConn) ReadFrom(b []byte) (int, net.Addr, error) { func (c *quicErrWrapperUDPLikeConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.UDPLikeConn.ReadFrom(b) n, addr, err := c.UDPLikeConn.ReadFrom(b)
if err != nil { if err != nil {
return 0, nil, NewErrWrapper(classifyGenericError, ReadFromOperation, err) return 0, nil, newErrWrapper(classifyGenericError, ReadFromOperation, err)
} }
return n, addr, nil return n, addr, nil
} }
@ -412,24 +416,30 @@ func (c *quicErrWrapperUDPLikeConn) ReadFrom(b []byte) (int, net.Addr, error) {
func (c *quicErrWrapperUDPLikeConn) Close() error { func (c *quicErrWrapperUDPLikeConn) Close() error {
err := c.UDPLikeConn.Close() err := c.UDPLikeConn.Close()
if err != nil { if err != nil {
return NewErrWrapper(classifyGenericError, ReadFromOperation, err) return newErrWrapper(classifyGenericError, ReadFromOperation, err)
} }
return nil return nil
} }
// quicDialerErrWrapper is a dialer that performs quic err wrapping // quicDialerErrWrapper is a dialer that performs quic err wrapping
type quicDialerErrWrapper struct { type quicDialerErrWrapper struct {
model.QUICDialer QUICDialer model.QUICDialer
} }
var _ model.QUICDialer = &quicDialerErrWrapper{}
// DialContext implements ContextDialer.DialContext // DialContext implements ContextDialer.DialContext
func (d *quicDialerErrWrapper) DialContext( func (d *quicDialerErrWrapper) DialContext(
ctx context.Context, network string, host string, ctx context.Context, network string, host string,
tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
qconn, err := d.QUICDialer.DialContext(ctx, network, host, tlsCfg, cfg) qconn, err := d.QUICDialer.DialContext(ctx, network, host, tlsCfg, cfg)
if err != nil { if err != nil {
return nil, NewErrWrapper( return nil, newErrWrapper(
classifyQUICHandshakeError, QUICHandshakeOperation, err) classifyQUICHandshakeError, QUICHandshakeOperation, err)
} }
return qconn, nil return qconn, nil
} }
func (d *quicDialerErrWrapper) CloseIdleConnections() {
d.QUICDialer.CloseIdleConnections()
}

View File

@ -1,15 +1,17 @@
package netxlite package netxlite
//
// This file contains weird stuff that we carried over from
// the original netx implementation and that we cannot remove
// or change without thinking about the consequences.
//
import ( import (
"errors" "errors"
"net" "net"
"strings" "strings"
) )
// This file contains weird stuff that we carried over from
// the original netx implementation and that we cannot remove
// or change without thinking about the consequences.
// See https://github.com/ooni/probe/issues/1985 // See https://github.com/ooni/probe/issues/1985
var errReduceErrorsEmptyList = errors.New("bug: reduceErrors given an empty list") var errReduceErrorsEmptyList = errors.New("bug: reduceErrors given an empty list")

View File

@ -34,12 +34,12 @@ func TestQuirkReduceErrors(t *testing.T) {
t.Run("multiple errors with meaningful ones", func(t *testing.T) { t.Run("multiple errors with meaningful ones", func(t *testing.T) {
err1 := errors.New("mocked error #1") err1 := errors.New("mocked error #1")
err2 := NewErrWrapper( err2 := newErrWrapper(
classifyGenericError, classifyGenericError,
CloseOperation, CloseOperation,
errors.New("antani"), errors.New("antani"),
) )
err3 := NewErrWrapper( err3 := newErrWrapper(
classifyGenericError, classifyGenericError,
CloseOperation, CloseOperation,
ECONNREFUSED, ECONNREFUSED,

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// DNS resolver
//
import ( import (
"context" "context"
"errors" "errors"
@ -134,8 +138,8 @@ func (r *resolverSystem) LookupHTTPS(
// resolverLogger is a resolver that emits events // resolverLogger is a resolver that emits events
type resolverLogger struct { type resolverLogger struct {
model.Resolver Resolver model.Resolver
Logger model.DebugLogger Logger model.DebugLogger
} }
var _ model.Resolver = &resolverLogger{} var _ model.Resolver = &resolverLogger{}
@ -172,13 +176,27 @@ func (r *resolverLogger) LookupHTTPS(
return https, nil return https, nil
} }
func (r *resolverLogger) Address() string {
return r.Resolver.Address()
}
func (r *resolverLogger) Network() string {
return r.Resolver.Network()
}
func (r *resolverLogger) CloseIdleConnections() {
r.Resolver.CloseIdleConnections()
}
// resolverIDNA supports resolving Internationalized Domain Names. // resolverIDNA supports resolving Internationalized Domain Names.
// //
// See RFC3492 for more information. // See RFC3492 for more information.
type resolverIDNA struct { type resolverIDNA struct {
model.Resolver Resolver model.Resolver
} }
var _ model.Resolver = &resolverIDNA{}
func (r *resolverIDNA) LookupHost(ctx context.Context, hostname string) ([]string, error) { func (r *resolverIDNA) LookupHost(ctx context.Context, hostname string) ([]string, error) {
host, err := idna.ToASCII(hostname) host, err := idna.ToASCII(hostname)
if err != nil { if err != nil {
@ -196,12 +214,26 @@ func (r *resolverIDNA) LookupHTTPS(
return r.Resolver.LookupHTTPS(ctx, host) return r.Resolver.LookupHTTPS(ctx, host)
} }
func (r *resolverIDNA) Network() string {
return r.Resolver.Network()
}
func (r *resolverIDNA) Address() string {
return r.Resolver.Address()
}
func (r *resolverIDNA) CloseIdleConnections() {
r.Resolver.CloseIdleConnections()
}
// resolverShortCircuitIPAddr recognizes when the input hostname is an // resolverShortCircuitIPAddr recognizes when the input hostname is an
// IP address and returns it immediately to the caller. // IP address and returns it immediately to the caller.
type resolverShortCircuitIPAddr struct { type resolverShortCircuitIPAddr struct {
model.Resolver Resolver model.Resolver
} }
var _ model.Resolver = &resolverShortCircuitIPAddr{}
func (r *resolverShortCircuitIPAddr) LookupHost(ctx context.Context, hostname string) ([]string, error) { func (r *resolverShortCircuitIPAddr) LookupHost(ctx context.Context, hostname string) ([]string, error) {
if net.ParseIP(hostname) != nil { if net.ParseIP(hostname) != nil {
return []string{hostname}, nil return []string{hostname}, nil
@ -222,6 +254,18 @@ func (r *resolverShortCircuitIPAddr) LookupHTTPS(ctx context.Context, hostname s
return r.Resolver.LookupHTTPS(ctx, hostname) return r.Resolver.LookupHTTPS(ctx, hostname)
} }
func (r *resolverShortCircuitIPAddr) Network() string {
return r.Resolver.Network()
}
func (r *resolverShortCircuitIPAddr) Address() string {
return r.Resolver.Address()
}
func (r *resolverShortCircuitIPAddr) CloseIdleConnections() {
r.Resolver.CloseIdleConnections()
}
// IsIPv6 returns true if the given candidate is a valid IP address // IsIPv6 returns true if the given candidate is a valid IP address
// representation and such representation is IPv6. // representation and such representation is IPv6.
func IsIPv6(candidate string) (bool, error) { func IsIPv6(candidate string) (bool, error) {
@ -271,7 +315,7 @@ func (r *nullResolver) LookupHTTPS(
// resolverErrWrapper is a Resolver that knows about wrapping errors. // resolverErrWrapper is a Resolver that knows about wrapping errors.
type resolverErrWrapper struct { type resolverErrWrapper struct {
model.Resolver Resolver model.Resolver
} }
var _ model.Resolver = &resolverErrWrapper{} var _ model.Resolver = &resolverErrWrapper{}
@ -279,7 +323,7 @@ var _ model.Resolver = &resolverErrWrapper{}
func (r *resolverErrWrapper) LookupHost(ctx context.Context, hostname string) ([]string, error) { func (r *resolverErrWrapper) LookupHost(ctx context.Context, hostname string) ([]string, error) {
addrs, err := r.Resolver.LookupHost(ctx, hostname) addrs, err := r.Resolver.LookupHost(ctx, hostname)
if err != nil { if err != nil {
return nil, NewErrWrapper(classifyResolverError, ResolveOperation, err) return nil, newErrWrapper(classifyResolverError, ResolveOperation, err)
} }
return addrs, nil return addrs, nil
} }
@ -288,7 +332,19 @@ func (r *resolverErrWrapper) LookupHTTPS(
ctx context.Context, domain string) (*model.HTTPSSvc, error) { ctx context.Context, domain string) (*model.HTTPSSvc, error) {
out, err := r.Resolver.LookupHTTPS(ctx, domain) out, err := r.Resolver.LookupHTTPS(ctx, domain)
if err != nil { if err != nil {
return nil, NewErrWrapper(classifyResolverError, ResolveOperation, err) return nil, newErrWrapper(classifyResolverError, ResolveOperation, err)
} }
return out, nil return out, nil
} }
func (r *resolverErrWrapper) Network() string {
return r.Resolver.Network()
}
func (r *resolverErrWrapper) Address() string {
return r.Resolver.Address()
}
func (r *resolverErrWrapper) CloseIdleConnections() {
r.Resolver.CloseIdleConnections()
}

View File

@ -400,6 +400,30 @@ func TestResolverIDNA(t *testing.T) {
} }
}) })
}) })
t.Run("Network", func(t *testing.T) {
child := &mocks.Resolver{
MockNetwork: func() string {
return "x"
},
}
r := &resolverIDNA{child}
if r.Network() != "x" {
t.Fatal("invalid network")
}
})
t.Run("Address", func(t *testing.T) {
child := &mocks.Resolver{
MockAddress: func() string {
return "x"
},
}
r := &resolverIDNA{child}
if r.Address() != "x" {
t.Fatal("invalid address")
}
})
} }
func TestResolverShortCircuitIPAddr(t *testing.T) { func TestResolverShortCircuitIPAddr(t *testing.T) {

View File

@ -1,7 +1,7 @@
package netxlite package netxlite
// //
// Serial resolver implementation // Serial DNS resolver implementation
// //
import ( import (
@ -20,7 +20,11 @@ import (
// //
// You should probably use NewSerialResolver to create a new instance. // You should probably use NewSerialResolver to create a new instance.
// //
// Deprecated: please use ParallelResolver in new code. // Deprecated: please use ParallelResolver in new code. We cannot
// remove this code as long as we use tracing for measuring.
//
// QUIRK: unlike the ParallelResolver, this resolver retries each
// query three times for soft errors.
type SerialResolver struct { type SerialResolver struct {
// Encoder is the MANDATORY encoder to use. // Encoder is the MANDATORY encoder to use.
Encoder model.DNSEncoder Encoder model.DNSEncoder
@ -98,6 +102,8 @@ func (r *SerialResolver) LookupHTTPS(
func (r *SerialResolver) lookupHostWithRetry( func (r *SerialResolver) lookupHostWithRetry(
ctx context.Context, hostname string, qtype uint16) ([]string, error) { ctx context.Context, hostname string, qtype uint16) ([]string, error) {
// QUIRK: retrying has been there since the beginning so we need to
// keep it as long as we're using tracing for measuring.
var errorslist []error var errorslist []error
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
replies, err := r.lookupHostWithoutRetry(ctx, hostname, qtype) replies, err := r.lookupHostWithoutRetry(ctx, hostname, qtype)
@ -116,9 +122,9 @@ func (r *SerialResolver) lookupHostWithRetry(
} }
r.NumTimeouts.Add(1) r.NumTimeouts.Add(1)
} }
// bugfix: we MUST return one of the errors otherwise we confuse the // QUIRK: we MUST return one of the errors otherwise we confuse the
// mechanism in errwrap that classifies the root cause operation, since // mechanism in errwrap that classifies the root cause operation, since
// it would not be able to find a child with a major operation error // it would not be able to find a child with a major operation error.
return nil, errorslist[0] return nil, errorslist[0]
} }

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// TLS implementation
//
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
@ -11,8 +15,12 @@ import (
oohttp "github.com/ooni/oohttp" oohttp "github.com/ooni/oohttp"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/runtimex"
) )
// TODO(bassosimone): check whether there's now equivalent functionality
// inside the standard library allowing us to map numbers to names.
var ( var (
tlsVersionString = map[uint16]string{ tlsVersionString = map[uint16]string{
tls.VersionTLS10: "TLSv1", tls.VersionTLS10: "TLSv1",
@ -81,7 +89,8 @@ func NewDefaultCertPool() *x509.CertPool {
pool := x509.NewCertPool() pool := x509.NewCertPool()
// Assumption: AppendCertsFromPEM cannot fail because we // Assumption: AppendCertsFromPEM cannot fail because we
// have a test in certify_test.go that guarantees that // have a test in certify_test.go that guarantees that
pool.AppendCertsFromPEM([]byte(pemcerts)) ok := pool.AppendCertsFromPEM([]byte(pemcerts))
runtimex.PanicIfFalse(ok, "pool.AppendCertsFromPEM failed")
return pool return pool
} }
@ -201,8 +210,8 @@ var defaultTLSHandshaker = &tlsHandshakerConfigurable{}
// tlsHandshakerLogger is a TLSHandshaker with logging. // tlsHandshakerLogger is a TLSHandshaker with logging.
type tlsHandshakerLogger struct { type tlsHandshakerLogger struct {
model.TLSHandshaker TLSHandshaker model.TLSHandshaker
model.DebugLogger DebugLogger model.DebugLogger
} }
var _ model.TLSHandshaker = &tlsHandshakerLogger{} var _ model.TLSHandshaker = &tlsHandshakerLogger{}
@ -313,7 +322,7 @@ func NewSingleUseTLSDialer(conn TLSConn) model.TLSDialer {
// tlsDialerSingleUseAdapter adapts dialerSingleUse to // tlsDialerSingleUseAdapter adapts dialerSingleUse to
// be a TLSDialer type rather than a Dialer type. // be a TLSDialer type rather than a Dialer type.
type tlsDialerSingleUseAdapter struct { type tlsDialerSingleUseAdapter struct {
model.Dialer Dialer model.Dialer
} }
var _ model.TLSDialer = &tlsDialerSingleUseAdapter{} var _ model.TLSDialer = &tlsDialerSingleUseAdapter{}
@ -323,9 +332,13 @@ func (d *tlsDialerSingleUseAdapter) DialTLSContext(ctx context.Context, network,
return d.Dialer.DialContext(ctx, network, address) return d.Dialer.DialContext(ctx, network, address)
} }
func (d *tlsDialerSingleUseAdapter) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
}
// tlsHandshakerErrWrapper wraps the returned error to be an OONI error // tlsHandshakerErrWrapper wraps the returned error to be an OONI error
type tlsHandshakerErrWrapper struct { type tlsHandshakerErrWrapper struct {
model.TLSHandshaker TLSHandshaker model.TLSHandshaker
} }
// Handshake implements TLSHandshaker.Handshake // Handshake implements TLSHandshaker.Handshake
@ -334,7 +347,7 @@ func (h *tlsHandshakerErrWrapper) Handshake(
) (net.Conn, tls.ConnectionState, error) { ) (net.Conn, tls.ConnectionState, error) {
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config) tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
if err != nil { if err != nil {
return nil, tls.ConnectionState{}, NewErrWrapper( return nil, tls.ConnectionState{}, newErrWrapper(
classifyTLSHandshakeError, TLSHandshakeOperation, err) classifyTLSHandshakeError, TLSHandshakeOperation, err)
} }
return tlsconn, state, nil return tlsconn, state, nil

View File

@ -487,6 +487,7 @@ func TestTLSDialer(t *testing.T) {
func TestNewSingleUseTLSDialer(t *testing.T) { func TestNewSingleUseTLSDialer(t *testing.T) {
conn := &mocks.TLSConn{} conn := &mocks.TLSConn{}
d := NewSingleUseTLSDialer(conn) d := NewSingleUseTLSDialer(conn)
defer d.CloseIdleConnections()
outconn, err := d.DialTLSContext(context.Background(), "", "") outconn, err := d.DialTLSContext(context.Background(), "", "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Transparent proxy (for integration testing)
//
import ( import (
"context" "context"
"net" "net"

View File

@ -1,5 +1,9 @@
package netxlite package netxlite
//
// Code to use yawning/utls or refraction-networking/utls
//
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"