diff --git a/.github/workflows/netxlite.yml b/.github/workflows/netxlite.yml index 683720b..df57129 100644 --- a/.github/workflows/netxlite.yml +++ b/.github/workflows/netxlite.yml @@ -21,4 +21,11 @@ jobs: go-version: "${{ matrix.go }}" cache-key-suffix: "-coverage-${{ matrix.go }}" - uses: actions/checkout@v2 + + # The first test compiles and links against libc and uses getaddrinfo - run: go test -race ./internal/netxlite/... + + # The second test instead uses netgo (we can't use -race with CGO_ENABLED=0) + - run: go test ./internal/netxlite/... + env: + CGO_ENABLED: 0 diff --git a/internal/engine/experiment/quicping/quicping_test.go b/internal/engine/experiment/quicping/quicping_test.go index 9095e4a..725d1ea 100644 --- a/internal/engine/experiment/quicping/quicping_test.go +++ b/internal/engine/experiment/quicping/quicping_test.go @@ -23,7 +23,7 @@ type FailStdLib struct { readErr error } -// ListenUDP implements UnderlyingNetworkLibrary.ListenUDP. +// ListenUDP implements model.UnderlyingNetworkLibrary.ListenUDP. func (f *FailStdLib) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) { conn, _ := net.ListenUDP(network, laddr) f.conn = model.UDPLikeConn(conn) @@ -65,11 +65,21 @@ func (f *FailStdLib) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLik return &mocks.UDPLikeConn{}, nil } -// LookupHost implements UnderlyingNetworkLibrary.LookupHost. +// DefaultResolver implements model.UnderlyingNetworkLibrary.DefaultResolver. +func (f *FailStdLib) DefaultResolver() model.SimpleResolver { + return f +} + +// LookupHost implements model.SimpleResolver.LookupHost. func (f *FailStdLib) LookupHost(ctx context.Context, domain string) ([]string, error) { return nil, f.err } +// Network implements model.SimpleResolver.Network. +func (f *FailStdLib) Network() string { + return "fail_stdlib" +} + // NewSimpleDialer implements UnderlyingNetworkLibrary.NewSimpleDialer. func (f *FailStdLib) NewSimpleDialer(timeout time.Duration) model.SimpleDialer { return nil diff --git a/internal/model/netx.go b/internal/model/netx.go index 4fc7cbf..01438e2 100644 --- a/internal/model/netx.go +++ b/internal/model/netx.go @@ -193,13 +193,35 @@ type QUICDialer interface { CloseIdleConnections() } -// Resolver performs domain name resolutions. -type Resolver interface { +// SimpleResolver is a simplified resolver that only allows to perform +// an ordinary lookup operation and to know the resolver's name. +type SimpleResolver interface { // LookupHost behaves like net.Resolver.LookupHost. LookupHost(ctx context.Context, hostname string) (addrs []string, err error) - // Network returns the resolver type (e.g., system, dot, doh). + // Network returns the resolver type. It should be one of: + // + // - netgo: means we're using golang's "netgo" UDP resolver, which + // reads /etc/resolv.conf and only works on Unix systems; + // + // - system: means we're calling getaddrinfo; + // + // - udp: is a custom DNS-over-UDP resolver; + // + // - tcp: is a custom DNS-over-TCP resolver; + // + // - dot: is a custom DNS-over-TLS resolver; + // + // - doh: is a custom DNS-over-HTTPS resolver; + // + // - doh3: is a custom DNS-over-HTTP3 resolver. Network() string +} + +// Resolver performs domain name resolutions. +type Resolver interface { + // A Resolver is also a SimpleResolver. + SimpleResolver // Address returns the resolver address (e.g., 8.8.8.8:53). Address() string @@ -283,8 +305,8 @@ type UnderlyingNetworkLibrary interface { // ListenUDP creates a new model.UDPLikeConn conn. ListenUDP(network string, laddr *net.UDPAddr) (UDPLikeConn, error) - // LookupHost lookups a domain using the stdlib resolver. - LookupHost(ctx context.Context, domain string) ([]string, error) + // DefaultResolver returns the default resolver. + DefaultResolver() SimpleResolver // NewSimpleDialer returns a new SimpleDialer. NewSimpleDialer(timeout time.Duration) SimpleDialer diff --git a/internal/netxlite/getaddrinfo_bsd_test.go b/internal/netxlite/getaddrinfo_bsd_test.go index a768984..c755f72 100644 --- a/internal/netxlite/getaddrinfo_bsd_test.go +++ b/internal/netxlite/getaddrinfo_bsd_test.go @@ -9,8 +9,7 @@ import ( ) func TestGetaddrinfoAIFlags(t *testing.T) { - var wrong bool - wrong = getaddrinfoAIFlags != (aiCanonname|aiV4Mapped|aiAll)&aiMask + wrong := getaddrinfoAIFlags != (aiCanonname|aiV4Mapped|aiAll)&aiMask if wrong { t.Fatal("wrong flags for platform") } diff --git a/internal/netxlite/getaddrinfo_cgo.go b/internal/netxlite/getaddrinfo_cgo.go index 06c172f..0e7ace5 100644 --- a/internal/netxlite/getaddrinfo_cgo.go +++ b/internal/netxlite/getaddrinfo_cgo.go @@ -24,12 +24,37 @@ import ( "unsafe" ) +// getaddrinfoResolverNetwork returns the "network" that is actually +// been used to implement the getaddrinfo resolver. +// +// This is the CGO_ENABLED=1 implementation of this function, which +// always returns the string "system", because in this scenario +// we are actually calling the getaddrinfo libc function. +func getaddrinfoResolverNetwork() string { + return "system" +} + +// getaddrinfoLookupANY attempts to perform an ANY lookup using getaddrinfo. +// +// This is the CGO_ENABLED=1 implementation of this function. +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation +// +// - domain is the domain to lookup +// +// This function returns the list of looked up addresses, the CNAME, and +// the error that occurred. On error, the list of addresses is empty. The +// CNAME may be empty on success, if there's no CNAME, but may also be +// non-empty on failure, if the lookup result included a CNAME answer but +// did not include any A or AAAA answers. func getaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { - return getaddrinfoSingleton.LookupANY(ctx, domain) + return getaddrinfoStateSingleton.LookupANY(ctx, domain) } // getaddrinfoSingleton is the getaddrinfo singleton. -var getaddrinfoSingleton = newGetaddrinfoState(getaddrinfoNumSlots) +var getaddrinfoStateSingleton = newGetaddrinfoState(getaddrinfoNumSlots) // getaddrinfoSlot is a slot for calling getaddrinfo. The Go standard lib // limits the maximum number of parallel calls to getaddrinfo. They do that @@ -168,13 +193,13 @@ func (state *getaddrinfoState) addrinfoToString(r *C.struct_addrinfo) (string, e switch r.ai_family { case C.AF_INET: sa := (*syscall.RawSockaddrInet4)(unsafe.Pointer(r.ai_addr)) - addr := net.IPAddr{IP: state.copyIP(sa.Addr[:])} + addr := net.IPAddr{IP: getaddrinfoCopyIP(sa.Addr[:])} return addr.String(), nil case C.AF_INET6: sa := (*syscall.RawSockaddrInet6)(unsafe.Pointer(r.ai_addr)) addr := net.IPAddr{ - IP: state.copyIP(sa.Addr[:]), - Zone: state.ifnametoindex(int(sa.Scope_id)), + IP: getaddrinfoCopyIP(sa.Addr[:]), + Zone: getaddrinfoIfNametoindex(int(sa.Scope_id)), } return addr.String(), nil default: @@ -199,13 +224,13 @@ func staticAddrinfoWithInvalidSocketType() *C.struct_addrinfo { return &value } -// copyIP copies a net.IP. +// getaddrinfoCopyIP copies a net.IP. // // This function is adapted from copyIP // https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L344 // // SPDX-License-Identifier: BSD-3-Clause. -func (state *getaddrinfoState) copyIP(x net.IP) net.IP { +func getaddrinfoCopyIP(x net.IP) net.IP { if len(x) < 16 { return x.To16() } @@ -214,13 +239,13 @@ func (state *getaddrinfoState) copyIP(x net.IP) net.IP { return y } -// ifnametoindex converts an IPv6 scope index into an interface name. +// getaddrinfoIfNametotindex converts an IPv6 scope index into an interface name. // // This function is adapted from ipv6ZoneCache.update // https://github.com/golang/go/blob/go1.17.6/src/net/interface.go#L194 // // SPDX-License-Identifier: BSD-3-Clause. -func (state *getaddrinfoState) ifnametoindex(idx int) string { +func getaddrinfoIfNametoindex(idx int) string { iface, err := net.InterfaceByIndex(idx) // internally uses caching if err != nil { return "" diff --git a/internal/netxlite/getaddrinfo_cgo_test.go b/internal/netxlite/getaddrinfo_cgo_test.go deleted file mode 100644 index 0562072..0000000 --- a/internal/netxlite/getaddrinfo_cgo_test.go +++ /dev/null @@ -1,89 +0,0 @@ -//go:build: cgo - -package netxlite - -import ( - "context" - "errors" - "net" - "testing" - "time" -) - -func TestGetaddrinfoStateAddrinfoToStringWithInvalidFamily(t *testing.T) { - aip := staticAddrinfoWithInvalidFamily() - state := newGetaddrinfoState(getaddrinfoNumSlots) - addr, err := state.addrinfoToString(aip) - if !errors.Is(err, errGetaddrinfoUnknownFamily) { - t.Fatal("unexpected err", err) - } - if addr != "" { - t.Fatal("expected empty addr here") - } -} - -func TestGetaddrinfoStateIfnametoindex(t *testing.T) { - ifaces, err := net.Interfaces() - if err != nil { - t.Fatal(err) - } - state := newGetaddrinfoState(getaddrinfoNumSlots) - for _, iface := range ifaces { - name := state.ifnametoindex(iface.Index) - if name != iface.Name { - t.Fatal("unexpected name") - } - } -} - -func TestGetaddrinfoStateLookupANYWithNoSlots(t *testing.T) { - const ( - noslots = 0 - timeout = 10 * time.Millisecond - ) - state := newGetaddrinfoState(noslots) - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - addresses, cname, err := state.LookupANY(ctx, "dns.google") - if !errors.Is(err, context.DeadlineExceeded) { - t.Fatal("unexpected err", err) - } - if len(addresses) > 0 { - t.Fatal("expected no addresses", addresses) - } - if cname != "" { - t.Fatal("expected empty cname", cname) - } -} - -func TestGetaddrinfoStateToAddressList(t *testing.T) { - t.Run("with invalid sockety type", func(t *testing.T) { - state := newGetaddrinfoState(0) // number of slots not relevant - aip := staticAddrinfoWithInvalidSocketType() - addresses, cname, err := state.toAddressList(aip) - if !errors.Is(err, ErrOODNSNoAnswer) { - t.Fatal("unexpected err", err) - } - if len(addresses) > 0 { - t.Fatal("expected no addresses", addresses) - } - if cname != "" { - t.Fatal("expected empty cname", cname) - } - }) - - t.Run("with invalid family", func(t *testing.T) { - state := newGetaddrinfoState(0) // number of slots not relevant - aip := staticAddrinfoWithInvalidFamily() - addresses, cname, err := state.toAddressList(aip) - if !errors.Is(err, ErrOODNSNoAnswer) { - t.Fatal("unexpected err", err) - } - if len(addresses) > 0 { - t.Fatal("expected no addresses", addresses) - } - if cname != "" { - t.Fatal("expected empty cname", cname) - } - }) -} diff --git a/internal/netxlite/getaddrinfo_otherwise.go b/internal/netxlite/getaddrinfo_otherwise.go index ccd70be..b84b994 100644 --- a/internal/netxlite/getaddrinfo_otherwise.go +++ b/internal/netxlite/getaddrinfo_otherwise.go @@ -7,6 +7,29 @@ import ( "net" ) -func getaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { - return net.DefaultResolver.LookupHost(ctx, domain) +// getaddrinfoResolverNetwork returns the "network" that is actually +// been used to implement the getaddrinfo resolver. +// +// This is the CGO_ENABLED=0 implementation of this function, which +// always returns the string "netgo", because in this scenario we +// are actually using the netgo implementation of net.Resolver. +func getaddrinfoResolverNetwork() string { + return "netgo" +} + +// getaddrinfoLookupANY attempts to perform an ANY lookup using getaddrinfo. +// +// This is the CGO_ENABLED=0 implementation of this function. +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation +// +// - domain is the domain to lookup +// +// This function returns the list of looked up addresses, an always-empty +// CNAME, and the error that occurred. On error, the list of addresses is empty. +func getaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { + al, err := net.DefaultResolver.LookupHost(ctx, domain) + return al, "", err } diff --git a/internal/netxlite/getaddrinfo_windows_test.go b/internal/netxlite/getaddrinfo_windows_test.go index 3ed44f5..5071f56 100644 --- a/internal/netxlite/getaddrinfo_windows_test.go +++ b/internal/netxlite/getaddrinfo_windows_test.go @@ -9,8 +9,7 @@ import ( ) func TestGetaddrinfoAIFlags(t *testing.T) { - var wrong bool - wrong = getaddrinfoAIFlags != aiCanonname + wrong := getaddrinfoAIFlags != aiCanonname if wrong { t.Fatal("wrong flags for platform") } diff --git a/internal/netxlite/resolver.go b/internal/netxlite/resolver.go index dbb287a..c0898d7 100644 --- a/internal/netxlite/resolver.go +++ b/internal/netxlite/resolver.go @@ -116,11 +116,11 @@ func (r *resolverSystem) lookupHost() func(ctx context.Context, domain string) ( if r.testableLookupHost != nil { return r.testableLookupHost } - return TProxy.LookupHost + return TProxy.DefaultResolver().LookupHost } func (r *resolverSystem) Network() string { - return "system" + return TProxy.DefaultResolver().Network() } func (r *resolverSystem) Address() string { diff --git a/internal/netxlite/resolver_test.go b/internal/netxlite/resolver_test.go index 8a1dc8c..17230b8 100644 --- a/internal/netxlite/resolver_test.go +++ b/internal/netxlite/resolver_test.go @@ -48,7 +48,7 @@ func TestNewResolverUDP(t *testing.T) { func TestResolverSystem(t *testing.T) { t.Run("Network and Address", func(t *testing.T) { r := &resolverSystem{} - if r.Network() != "system" { + if r.Network() != getaddrinfoResolverNetwork() { t.Fatal("invalid Network") } if r.Address() != "" { diff --git a/internal/netxlite/tproxy.go b/internal/netxlite/tproxy.go index 1cbb281..98af596 100644 --- a/internal/netxlite/tproxy.go +++ b/internal/netxlite/tproxy.go @@ -28,17 +28,25 @@ func (*TProxyStdlib) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLik return net.ListenUDP(network, laddr) } -// LookupHost calls net.DefaultResolver.LookupHost. -func (*TProxyStdlib) LookupHost(ctx context.Context, domain string) ([]string, error) { - // Implementation note: if possible, we try to call getaddrinfo - // directly, which allows us to gather the underlying error. The - // specifics of whether "it's possible" depend on whether we've - // been compiled linking to libc as well as whether we think that - // a platform is ready for using getaddrinfo directly. - return getaddrinfoLookupHost(ctx, domain) +// DefaultResolver returns the default resolver. +func (*TProxyStdlib) DefaultResolver() model.SimpleResolver { + return &tproxyDefaultResolver{} } // NewSimpleDialer returns a &net.Dialer{Timeout: timeout} instance. func (*TProxyStdlib) NewSimpleDialer(timeout time.Duration) model.SimpleDialer { return &net.Dialer{Timeout: timeout} } + +// tproxyDefaultResolver is the resolver we use by default. +type tproxyDefaultResolver struct{} + +// LookupHost implements model.SimpleResolver.LookupHost. +func (r *tproxyDefaultResolver) LookupHost(ctx context.Context, domain string) ([]string, error) { + return getaddrinfoLookupHost(ctx, domain) +} + +// Network implements model.SimpleResolver.Network. +func (r *tproxyDefaultResolver) Network() string { + return getaddrinfoResolverNetwork() +}