netxlite: call getaddrinfo and handle platform-specific oddities (#764)
This commit changes our system resolver to call getaddrinfo directly when CGO is enabled. This change allows us to: 1. obtain the CNAME easily 2. obtain the real getaddrinfo retval 3. handle platform specific oddities such as `EAI_NODATA` returned on Android devices See https://github.com/ooni/probe/issues/2029 and https://github.com/ooni/probe/issues/2029#issuecomment-1140258729 in particular. See https://github.com/ooni/probe/issues/2033 for documentation regarding the desire to see `getaddrinfo`'s retval. See https://github.com/ooni/probe/issues/2118 for possible follow-up changes.
This commit is contained in:
parent
62bd62ece1
commit
cf6dbe48e0
5
.github/workflows/netxlite.yml
vendored
5
.github/workflows/netxlite.yml
vendored
|
@ -1,6 +1,9 @@
|
|||
# netxlite runs unit and integration tests on our fundamental net library
|
||||
# Runs unit and integration tests for our fundamental networking library.
|
||||
name: netxlite
|
||||
on:
|
||||
# Because we link libc explicitly for getaddrinfo, we SHOULD run
|
||||
# these checks for every PR to ensure we still compile.
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
|
|
@ -68,7 +68,7 @@ func (tk *TestKeys) classify() string {
|
|||
return classAnomalyTestHelperUnreachable
|
||||
case netxlite.FailureConnectionReset:
|
||||
return classInterferenceReset
|
||||
case netxlite.FailureDNSNXDOMAINError:
|
||||
case netxlite.FailureDNSNXDOMAINError, netxlite.FailureAndroidDNSCacheNoData:
|
||||
return classAnomalyTestHelperUnreachable
|
||||
case netxlite.FailureEOFError:
|
||||
return classInterferenceClosed
|
||||
|
|
|
@ -12,11 +12,6 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
const (
|
||||
softwareName = "ooniprobe-example"
|
||||
softwareVersion = "0.0.1"
|
||||
)
|
||||
|
||||
func TestTestKeysClassify(t *testing.T) {
|
||||
asStringPtr := func(s string) *string {
|
||||
return &s
|
||||
|
@ -41,6 +36,13 @@ func TestTestKeysClassify(t *testing.T) {
|
|||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
t.Run("with tk.Target.Failure == android_dns_cache_no_data", func(t *testing.T) {
|
||||
tk := new(TestKeys)
|
||||
tk.Target.Failure = asStringPtr(netxlite.FailureAndroidDNSCacheNoData)
|
||||
if tk.classify() != classAnomalyTestHelperUnreachable {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
t.Run("with tk.Target.Failure == connection_reset", func(t *testing.T) {
|
||||
tk := new(TestKeys)
|
||||
tk.Target.Failure = asStringPtr(netxlite.FailureConnectionReset)
|
||||
|
|
|
@ -44,7 +44,13 @@ func DNSAnalysis(URL *url.URL, measurement DNSLookupResult,
|
|||
switch *control.DNS.Failure {
|
||||
case DNSNameError: // the control returns this on NXDOMAIN error
|
||||
switch *measurement.Failure {
|
||||
case netxlite.FailureDNSNXDOMAINError:
|
||||
// When the Android getaddrinfo cache says "no data" (meaning basically
|
||||
// "I don't know, mate") _and_ the test helper says NXDOMAIN, we can
|
||||
// be ~confident that there's also NXDOMAIN on the Android side.
|
||||
//
|
||||
// See also https://github.com/ooni/probe/issues/2029.
|
||||
case netxlite.FailureDNSNXDOMAINError,
|
||||
netxlite.FailureAndroidDNSCacheNoData:
|
||||
out.DNSConsistency = &DNSConsistent
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ func TestDNSAnalysis(t *testing.T) {
|
|||
measurementFailure := netxlite.FailureDNSNXDOMAINError
|
||||
controlFailure := webconnectivity.DNSNameError
|
||||
eofFailure := io.EOF.Error()
|
||||
androidEaiNoData := netxlite.FailureAndroidDNSCacheNoData
|
||||
type args struct {
|
||||
URL *url.URL
|
||||
measurement webconnectivity.DNSLookupResult
|
||||
|
@ -57,7 +58,7 @@ func TestDNSAnalysis(t *testing.T) {
|
|||
DNSConsistency: &webconnectivity.DNSInconsistent,
|
||||
},
|
||||
}, {
|
||||
name: "when the failures are compatible",
|
||||
name: "when the failures are compatible (NXDOMAIN case)",
|
||||
args: args{
|
||||
URL: &url.URL{
|
||||
Host: "www.kerneltrap.org",
|
||||
|
@ -74,6 +75,24 @@ func TestDNSAnalysis(t *testing.T) {
|
|||
wantOut: webconnectivity.DNSAnalysisResult{
|
||||
DNSConsistency: &webconnectivity.DNSConsistent,
|
||||
},
|
||||
}, {
|
||||
name: "when the failures are compatible (Android EAI_NODATA case)",
|
||||
args: args{
|
||||
URL: &url.URL{
|
||||
Host: "www.kerneltrap.org",
|
||||
},
|
||||
measurement: webconnectivity.DNSLookupResult{
|
||||
Failure: &androidEaiNoData,
|
||||
},
|
||||
control: webconnectivity.ControlResponse{
|
||||
DNS: webconnectivity.ControlDNSResult{
|
||||
Failure: &controlFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantOut: webconnectivity.DNSAnalysisResult{
|
||||
DNSConsistency: &webconnectivity.DNSConsistent,
|
||||
},
|
||||
}, {
|
||||
name: "when the ASNs are equal",
|
||||
args: args{
|
||||
|
|
|
@ -125,9 +125,14 @@ func Summarize(tk *TestKeys) (out Summary) {
|
|||
return
|
||||
}
|
||||
// If DNS failed with NXDOMAIN and the control DNS is consistent, then it
|
||||
// means this website does not exist anymore.
|
||||
// means this website does not exist anymore. We need to include the weird
|
||||
// cache failure on Android into this analysis because that failure means
|
||||
// NXDOMAIN (well, most likely) if the TH reported NXDOMAIN.
|
||||
//
|
||||
// See https://github.com/ooni/probe/issues/2029 for the Android issue.
|
||||
if tk.DNSExperimentFailure != nil &&
|
||||
*tk.DNSExperimentFailure == netxlite.FailureDNSNXDOMAINError &&
|
||||
(*tk.DNSExperimentFailure == netxlite.FailureDNSNXDOMAINError ||
|
||||
*tk.DNSExperimentFailure == netxlite.FailureAndroidDNSCacheNoData) &&
|
||||
tk.DNSConsistency != nil && *tk.DNSConsistency == DNSConsistent {
|
||||
// TODO(bassosimone): MK flags this as accessible. This result is debatable. We
|
||||
// are doing what MK does. But we most likely want to make it better later.
|
||||
|
|
|
@ -26,6 +26,7 @@ func TestSummarize(t *testing.T) {
|
|||
probeSSLInvalidHost = netxlite.FailureSSLInvalidHostname
|
||||
probeSSLInvalidCert = netxlite.FailureSSLInvalidCertificate
|
||||
probeSSLUnknownAuth = netxlite.FailureSSLUnknownAuthority
|
||||
probeAndroidEaiNoData = netxlite.FailureAndroidDNSCacheNoData
|
||||
tcpIP = "tcp_ip"
|
||||
trueValue = true
|
||||
)
|
||||
|
@ -68,7 +69,7 @@ func TestSummarize(t *testing.T) {
|
|||
Status: webconnectivity.StatusAnomalyControlUnreachable,
|
||||
},
|
||||
}, {
|
||||
name: "with non-existing website",
|
||||
name: "with non-existing website (NXDOMAIN case)",
|
||||
args: args{
|
||||
tk: &webconnectivity.TestKeys{
|
||||
DNSExperimentFailure: &probeNXDOMAIN,
|
||||
|
@ -84,6 +85,23 @@ func TestSummarize(t *testing.T) {
|
|||
Status: webconnectivity.StatusSuccessNXDOMAIN |
|
||||
webconnectivity.StatusExperimentDNS,
|
||||
},
|
||||
}, {
|
||||
name: "with non-existing website (Android EAI_NODATA case)",
|
||||
args: args{
|
||||
tk: &webconnectivity.TestKeys{
|
||||
DNSExperimentFailure: &probeAndroidEaiNoData,
|
||||
DNSAnalysisResult: webconnectivity.DNSAnalysisResult{
|
||||
DNSConsistency: &webconnectivity.DNSConsistent,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantOut: webconnectivity.Summary{
|
||||
BlockingReason: nil,
|
||||
Blocking: false,
|
||||
Accessible: &trueValue,
|
||||
Status: webconnectivity.StatusSuccessNXDOMAIN |
|
||||
webconnectivity.StatusExperimentDNS,
|
||||
},
|
||||
}, {
|
||||
name: "with NXDOMAIN measured only by the probe",
|
||||
args: args{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2022-05-19 20:30:44.840082 +0200 CEST m=+0.374984084
|
||||
// 2022-05-28 13:27:21.630174629 +0200 CEST m=+0.293627763
|
||||
// https://curl.haxx.se/ca/cacert.pem
|
||||
|
||||
package netxlite
|
||||
|
|
|
@ -249,18 +249,34 @@ const (
|
|||
// DNSOverHTTPSTransport and DNSOverUDPTransport). Their suffix matches the equivalent
|
||||
// unexported errors used by the Go standard library.
|
||||
var (
|
||||
// ErrOODNSNoSuchHost means NXDOMAIN.
|
||||
ErrOODNSNoSuchHost = fmt.Errorf("ooniresolver: %s", DNSNoSuchHostSuffix)
|
||||
|
||||
// ErrOODNSMisbehaving is the error typically returned by the `netgo`resolver
|
||||
// when it cannot really make sense of the error.
|
||||
ErrOODNSMisbehaving = fmt.Errorf("ooniresolver: %s", DNSServerMisbehavingSuffix)
|
||||
|
||||
// ErrOODNSNoAnswer means that we've got a valid DNS response that
|
||||
// did not contain any answer for the original query. This could happen
|
||||
// when we query for AAAA and the domain only has A records.
|
||||
ErrOODNSNoAnswer = fmt.Errorf("ooniresolver: %s", DNSNoAnswerSuffix)
|
||||
)
|
||||
|
||||
// These errors are not part of the Go standard library but we can
|
||||
// return them in our custom resolvers.
|
||||
var (
|
||||
// ErrOODNSRefused indicates that the response's Rcode was "refused"
|
||||
ErrOODNSRefused = errors.New("ooniresolver: refused")
|
||||
|
||||
// ErrOODNSServfail indicates that the response's Rcode was "servfail"
|
||||
ErrOODNSServfail = errors.New("ooniresolver: servfail")
|
||||
)
|
||||
|
||||
// ErrAndroidDNSCacheNoData is the kind of error returned by our getaddrinfo
|
||||
// code on Android when we see EAI_NODATA, an error condition that could mean
|
||||
// anything as explained in getaddrinfo_linux.go.
|
||||
var ErrAndroidDNSCacheNoData = errors.New(FailureAndroidDNSCacheNoData)
|
||||
|
||||
// classifyResolverError maps DNS resolution errors to
|
||||
// OONI failure strings.
|
||||
//
|
||||
|
@ -291,6 +307,9 @@ func classifyResolverError(err error) string {
|
|||
if errors.Is(err, ErrDNSReplyWithWrongQueryID) {
|
||||
return FailureDNSReplyWithWrongQueryID
|
||||
}
|
||||
if errors.Is(err, ErrAndroidDNSCacheNoData) {
|
||||
return FailureAndroidDNSCacheNoData
|
||||
}
|
||||
return classifyGenericError(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -281,6 +281,12 @@ func TestClassifyResolverError(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("for EAI_NODATA returned by Android's getaddrinfo", func(t *testing.T) {
|
||||
if classifyResolverError(ErrAndroidDNSCacheNoData) != FailureAndroidDNSCacheNoData {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for another kind of error", func(t *testing.T) {
|
||||
if classifyResolverError(io.EOF) != FailureEOFError {
|
||||
t.Fatal("unexpected result")
|
||||
|
|
|
@ -54,4 +54,21 @@
|
|||
//
|
||||
// Operations 1, 2, 3, and 4 are used when we perform measurements,
|
||||
// while 5 and 6 are mostly used when speaking with our backend.
|
||||
//
|
||||
// Getaddrinfo usage
|
||||
//
|
||||
// When compiled with CGO_ENABLED=1, this package will link with libc
|
||||
// and call getaddrinfo directly. While this design choice means we will
|
||||
// need to maintain more code, it also allows us to save the correct
|
||||
// getaddrinfo return value, which is hidden by the Go resolver. Also,
|
||||
// this strategy allows us to deal with the Android EAI_NODATA implementation
|
||||
// quirk (see https://github.com/ooni/probe/issues/2029).
|
||||
//
|
||||
// We currently use net.Resolver when CGO_ENABLED=0. A future version of
|
||||
// netxlite MIGHT change this and use a custom UDP resolver in such a
|
||||
// case, to avoid depending on the assumption that /etc/resolver.conf is
|
||||
// present on the target system. See https://github.com/ooni/probe/issues/2118
|
||||
// for more details regarding ongoing plans to bypass net.Resolver when
|
||||
// CGO_ENABLED=0. (If you're reading this piece of documentation and notice
|
||||
// it's not updated, please submit a pull request to update it :-).
|
||||
package netxlite
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.752543 +0200 CEST m=+0.582472918
|
||||
// Generated: 2022-05-28 13:27:22.097503116 +0200 CEST m=+0.338871155
|
||||
|
||||
package netxlite
|
||||
|
||||
|
@ -13,6 +13,7 @@ const (
|
|||
FailureAddressInUse = "address_in_use"
|
||||
FailureAddressNotAvailable = "address_not_available"
|
||||
FailureAlreadyConnected = "already_connected"
|
||||
FailureAndroidDNSCacheNoData = "android_dns_cache_no_data"
|
||||
FailureBadAddress = "bad_address"
|
||||
FailureBadFileDescriptor = "bad_file_descriptor"
|
||||
FailureConnectionAborted = "connection_aborted"
|
||||
|
@ -63,6 +64,7 @@ var failuresMap = map[string]string{
|
|||
"address_in_use": "address_in_use",
|
||||
"address_not_available": "address_not_available",
|
||||
"already_connected": "already_connected",
|
||||
"android_dns_cache_no_data": "android_dns_cache_no_data",
|
||||
"bad_address": "bad_address",
|
||||
"bad_file_descriptor": "bad_file_descriptor",
|
||||
"connection_aborted": "connection_aborted",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.170591 +0200 CEST m=+0.000503793
|
||||
// Generated: 2022-05-28 13:27:21.764075578 +0200 CEST m=+0.005443607
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.466378 +0200 CEST m=+0.296299710
|
||||
// Generated: 2022-05-28 13:27:21.820244729 +0200 CEST m=+0.061612769
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.509171 +0200 CEST m=+0.339094543
|
||||
// Generated: 2022-05-28 13:27:21.843034214 +0200 CEST m=+0.084402243
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.559112 +0200 CEST m=+0.389036168
|
||||
// Generated: 2022-05-28 13:27:21.881328637 +0200 CEST m=+0.122696672
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.642498 +0200 CEST m=+0.472425168
|
||||
// Generated: 2022-05-28 13:27:21.967785506 +0200 CEST m=+0.209153549
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.684349 +0200 CEST m=+0.514276960
|
||||
// Generated: 2022-05-28 13:27:22.010048884 +0200 CEST m=+0.251416941
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.579849 +0200 CEST m=+0.409773835
|
||||
// Generated: 2022-05-28 13:27:21.904104276 +0200 CEST m=+0.145472305
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.622731 +0200 CEST m=+0.452657918
|
||||
// Generated: 2022-05-28 13:27:21.942808293 +0200 CEST m=+0.184176336
|
||||
|
||||
package netxlite
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.704221 +0200 CEST m=+0.534149543
|
||||
// Generated: 2022-05-28 13:27:22.034720951 +0200 CEST m=+0.276088980
|
||||
|
||||
package netxlite
|
||||
|
||||
|
@ -45,6 +45,7 @@ const (
|
|||
WSANO_DATA = windows.WSANO_DATA
|
||||
WSANO_RECOVERY = windows.WSANO_RECOVERY
|
||||
WSATRY_AGAIN = windows.WSATRY_AGAIN
|
||||
WSAHOST_NOT_FOUND = windows.WSAHOST_NOT_FOUND
|
||||
)
|
||||
|
||||
// classifySyscallError converts a syscall error to the
|
||||
|
@ -116,6 +117,8 @@ func classifySyscallError(err error) string {
|
|||
return FailureDNSNonRecoverableFailure
|
||||
case windows.WSATRY_AGAIN:
|
||||
return FailureDNSTemporaryFailure
|
||||
case windows.WSAHOST_NOT_FOUND:
|
||||
return FailureDNSNXDOMAINError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// Generated: 2022-05-19 20:30:45.733431 +0200 CEST m=+0.563360501
|
||||
// Generated: 2022-05-28 13:27:22.067609692 +0200 CEST m=+0.308977732
|
||||
|
||||
package netxlite
|
||||
|
||||
|
@ -198,6 +198,12 @@ func TestClassifySyscallError(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("for WSAHOST_NOT_FOUND", func(t *testing.T) {
|
||||
if v := classifySyscallError(windows.WSAHOST_NOT_FOUND); v != FailureDNSNXDOMAINError {
|
||||
t.Fatalf("expected '%s', got '%s'", FailureDNSNXDOMAINError, v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for the zero errno value", func(t *testing.T) {
|
||||
if v := classifySyscallError(syscall.Errno(0)); v != "" {
|
||||
t.Fatalf("expected empty string, got '%s'", v)
|
||||
|
|
56
internal/netxlite/getaddrinfo.go
Normal file
56
internal/netxlite/getaddrinfo.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package netxlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// getaddrinfoLookupHost performs a DNS lookup and returns the
|
||||
// results. If we were compiled with CGO_ENABLED=0, then this
|
||||
// function calls net.DefaultResolver.LookupHost. Otherwise,
|
||||
// we call getaddrinfo. In such a case, if getaddrinfo returns a nonzero
|
||||
// return value, we'll return as error an instance of the
|
||||
// ErrGetaddrinfo error. This error will contain the specific
|
||||
// code returned by getaddrinfo in its .Code field.
|
||||
func getaddrinfoLookupHost(ctx context.Context, domain string) ([]string, error) {
|
||||
addrs, _, err := getaddrinfoLookupANY(ctx, domain)
|
||||
return addrs, err
|
||||
}
|
||||
|
||||
// ErrGetaddrinfo represents a getaddrinfo failure.
|
||||
type ErrGetaddrinfo struct {
|
||||
// Err is the error proper.
|
||||
Underlying error
|
||||
|
||||
// Code is getaddrinfo's return code.
|
||||
Code int64
|
||||
}
|
||||
|
||||
// newErrGetaddrinfo creates a new instance of the ErrGetaddrinfo type.
|
||||
func newErrGetaddrinfo(code int64, err error) *ErrGetaddrinfo {
|
||||
return &ErrGetaddrinfo{
|
||||
Underlying: err,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the underlying error's string.
|
||||
func (err *ErrGetaddrinfo) Error() string {
|
||||
return err.Underlying.Error()
|
||||
}
|
||||
|
||||
// Unwrap allows to get the underlying error value.
|
||||
func (err *ErrGetaddrinfo) Unwrap() error {
|
||||
return err.Underlying
|
||||
}
|
||||
|
||||
// ErrorToGetaddrinfoRetval converts an arbitrary error to
|
||||
// the return value of getaddrinfo. If err is nil or is not
|
||||
// an instance of ErrGetaddrinfo, we just return zero.
|
||||
func ErrorToGetaddrinfoRetval(err error) int64 {
|
||||
var aierr *ErrGetaddrinfo
|
||||
if err != nil && errors.As(err, &aierr) {
|
||||
return aierr.Code
|
||||
}
|
||||
return 0
|
||||
}
|
58
internal/netxlite/getaddrinfo_bsd.go
Normal file
58
internal/netxlite/getaddrinfo_bsd.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2011 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 cgo && (darwin || dragonfly || freebsd || openbsd)
|
||||
|
||||
package netxlite
|
||||
|
||||
/*
|
||||
#include <netdb.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const getaddrinfoAIFlags = (C.AI_CANONNAME | C.AI_V4MAPPED | C.AI_ALL) & C.AI_MASK
|
||||
|
||||
// Making constants available to Go code so we can run tests
|
||||
const (
|
||||
aiCanonname = C.AI_CANONNAME
|
||||
aiV4Mapped = C.AI_V4MAPPED
|
||||
aiAll = C.AI_ALL
|
||||
aiMask = C.AI_MASK
|
||||
eaiSystem = C.EAI_SYSTEM
|
||||
eaiNoName = C.EAI_NONAME
|
||||
eaiBadFlags = C.EAI_BADFLAGS
|
||||
)
|
||||
|
||||
// toError is the function that converts the return value from
|
||||
// the getaddrinfo function into a proper Go error.
|
||||
//
|
||||
// This function is adapted from cgoLookupIPCNAME
|
||||
// https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L145
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func (state *getaddrinfoState) toError(code int64, err error, goos string) error {
|
||||
switch code {
|
||||
case C.EAI_SYSTEM:
|
||||
if err == nil {
|
||||
// err should not be nil, but sometimes getaddrinfo returns
|
||||
// code == C.EAI_SYSTEM with err == nil on Linux.
|
||||
// The report claims that it happens when we have too many
|
||||
// open files, so use syscall.EMFILE (too many open files in system).
|
||||
// Most system calls would return ENFILE (too many open files),
|
||||
// so at the least EMFILE should be easy to recognize if this
|
||||
// comes up again. golang.org/issue/6232.
|
||||
err = syscall.EMFILE
|
||||
}
|
||||
return newErrGetaddrinfo(code, err)
|
||||
case C.EAI_NONAME:
|
||||
err = ErrOODNSNoSuchHost // so it becomes FailureDNSNXDOMAIN
|
||||
return newErrGetaddrinfo(code, err)
|
||||
default:
|
||||
err = ErrOODNSMisbehaving // so it becomes FailureDNSServerMisbehaving
|
||||
return newErrGetaddrinfo(code, err)
|
||||
}
|
||||
}
|
105
internal/netxlite/getaddrinfo_bsd_test.go
Normal file
105
internal/netxlite/getaddrinfo_bsd_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
//go:build cgo && (darwin || dragonfly || freebsd || openbsd)
|
||||
|
||||
package netxlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetaddrinfoAIFlags(t *testing.T) {
|
||||
var wrong bool
|
||||
wrong = getaddrinfoAIFlags != (aiCanonname|aiV4Mapped|aiAll)&aiMask
|
||||
if wrong {
|
||||
t.Fatal("wrong flags for platform")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetaddrinfoStateToError(t *testing.T) {
|
||||
type args struct {
|
||||
code int64
|
||||
err error
|
||||
goos string
|
||||
}
|
||||
type expects struct {
|
||||
message string // message obtained using .Error
|
||||
code int64
|
||||
err error
|
||||
}
|
||||
var inputs = []struct {
|
||||
name string
|
||||
args args
|
||||
expects expects
|
||||
}{{
|
||||
name: "with C.EAI_SYSTEM and non-nil error",
|
||||
args: args{
|
||||
code: eaiSystem,
|
||||
err: syscall.EAGAIN,
|
||||
goos: "darwin",
|
||||
},
|
||||
expects: expects{
|
||||
message: syscall.EAGAIN.Error(),
|
||||
code: eaiSystem,
|
||||
err: syscall.EAGAIN,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_SYSTEM and nil error",
|
||||
args: args{
|
||||
code: eaiSystem,
|
||||
err: nil,
|
||||
goos: "darwin",
|
||||
},
|
||||
expects: expects{
|
||||
message: syscall.EMFILE.Error(),
|
||||
code: eaiSystem,
|
||||
err: syscall.EMFILE,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_NONAME",
|
||||
args: args{
|
||||
code: eaiNoName,
|
||||
err: nil,
|
||||
goos: "darwin",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrOODNSNoSuchHost.Error(),
|
||||
code: eaiNoName,
|
||||
err: ErrOODNSNoSuchHost,
|
||||
},
|
||||
}, {
|
||||
name: "with an unhandled error",
|
||||
args: args{
|
||||
code: eaiBadFlags,
|
||||
err: nil,
|
||||
goos: "darwin",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrOODNSMisbehaving.Error(),
|
||||
code: eaiBadFlags,
|
||||
err: ErrOODNSMisbehaving,
|
||||
},
|
||||
}}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
state := newGetaddrinfoState(getaddrinfoNumSlots)
|
||||
err := state.toError(input.args.code, input.args.err, input.args.goos)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error here")
|
||||
}
|
||||
if err.Error() != input.expects.message {
|
||||
t.Fatal("unexpected error message")
|
||||
}
|
||||
var gaierr *ErrGetaddrinfo
|
||||
if !errors.As(err, &gaierr) {
|
||||
t.Fatal("cannot convert error to ErrGetaddrinfo")
|
||||
}
|
||||
if gaierr.Code != input.expects.code {
|
||||
t.Fatal("unexpected code")
|
||||
}
|
||||
if !errors.Is(gaierr.Underlying, input.expects.err) {
|
||||
t.Fatal("unexpected underlying error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
229
internal/netxlite/getaddrinfo_cgo.go
Normal file
229
internal/netxlite/getaddrinfo_cgo.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
//go:build: cgo
|
||||
|
||||
package netxlite
|
||||
|
||||
/*
|
||||
// On Unix systems, getaddrinfo is part of libc. On Windows,
|
||||
// instead, we need to explicitly link with winsock2.
|
||||
#cgo windows LDFLAGS: -lws2_32
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <netdb.h> // for getaddrinfo
|
||||
#else
|
||||
#include <ws2tcpip.h> // for getaddrinfo
|
||||
#endif
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func getaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) {
|
||||
return getaddrinfoSingleton.LookupANY(ctx, domain)
|
||||
}
|
||||
|
||||
// getaddrinfoSingleton is the getaddrinfo singleton.
|
||||
var getaddrinfoSingleton = 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
|
||||
// to avoid using too many threads if the system resolver for some
|
||||
// reason doesn't respond. We need to do the same. Because OONI does not
|
||||
// need to be as general as the Go stdlib, we'll use a small-enough number
|
||||
// of slots, rather than checking for rlimits, like the stdlib does,
|
||||
// e.g., on Unix. This struct represents one of these slots.
|
||||
type getaddrinfoSlot struct{}
|
||||
|
||||
// getaddrinfoState is the state associated to getaddrinfo.
|
||||
type getaddrinfoState struct {
|
||||
// sema is the semaphore that only allows a maximum number of
|
||||
// getaddrinfo slots to be active at any given time.
|
||||
sema chan *getaddrinfoSlot
|
||||
|
||||
// lookupANY is the function that actually implements
|
||||
// the lookup ANY lookup using getaddrinfo.
|
||||
lookupANY func(domain string) ([]string, string, error)
|
||||
}
|
||||
|
||||
// getaddrinfoNumSlots is the maximum number of parallel calls
|
||||
// to getaddrinfo we may have at any given time.
|
||||
const getaddrinfoNumSlots = 8
|
||||
|
||||
// newGetaddrinfoState creates the getaddrinfo state.
|
||||
func newGetaddrinfoState(numSlots int) *getaddrinfoState {
|
||||
state := &getaddrinfoState{
|
||||
sema: make(chan *getaddrinfoSlot, numSlots),
|
||||
lookupANY: nil,
|
||||
}
|
||||
state.lookupANY = state.doLookupANY
|
||||
return state
|
||||
}
|
||||
|
||||
// lookupANY invokes getaddrinfo and returns the results.
|
||||
func (state *getaddrinfoState) LookupANY(ctx context.Context, domain string) ([]string, string, error) {
|
||||
if err := state.grabSlot(ctx); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer state.releaseSlot()
|
||||
return state.doLookupANY(domain)
|
||||
}
|
||||
|
||||
// grabSlot grabs a slot for calling getaddrinfo. This function may block until
|
||||
// a slot becomes available (or until the context is done).
|
||||
func (state *getaddrinfoState) grabSlot(ctx context.Context) error {
|
||||
// Implementation note: the channel has getaddrinfoNumSlots capacity, hence
|
||||
// the first getaddrinfoNumSlots channel writes will succeed and all the
|
||||
// subsequent ones will block. To unblock a pending request, we release a
|
||||
// slot by reading from the channel.
|
||||
select {
|
||||
case state.sema <- &getaddrinfoSlot{}:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// releaseSlot releases a previously acquired slot.
|
||||
func (state *getaddrinfoState) releaseSlot() {
|
||||
<-state.sema
|
||||
}
|
||||
|
||||
// doLookupANY calls getaddrinfo. We assume that you've already grabbed a
|
||||
// slot and you're defer-releasing it when you're done.
|
||||
//
|
||||
// This function is adapted from cgoLookupIPCNAME
|
||||
// https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L145
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func (state *getaddrinfoState) doLookupANY(domain string) ([]string, string, error) {
|
||||
var hints C.struct_addrinfo // zero-initialized by Go
|
||||
hints.ai_flags = getaddrinfoAIFlags
|
||||
hints.ai_socktype = C.SOCK_STREAM
|
||||
hints.ai_family = C.AF_UNSPEC
|
||||
h := make([]byte, len(domain)+1)
|
||||
copy(h, domain)
|
||||
var res *C.struct_addrinfo
|
||||
// From https://pkg.go.dev/cmd/cgo:
|
||||
//
|
||||
// "Any C function (even void functions) may be called in a multiple
|
||||
// assignment context to retrieve both the return value (if any) and the
|
||||
// C errno variable as an error"
|
||||
code, err := C.getaddrinfo((*C.char)(unsafe.Pointer(&h[0])), nil, &hints, &res)
|
||||
if code != 0 {
|
||||
return nil, "", state.toError(int64(code), err, runtime.GOOS)
|
||||
}
|
||||
defer C.freeaddrinfo(res)
|
||||
return state.toAddressList(res)
|
||||
}
|
||||
|
||||
// toAddressList is the function that converts the return value from
|
||||
// the getaddrinfo function into a list of strings.
|
||||
//
|
||||
// This function is adapted from cgoLookupIPCNAME
|
||||
// https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L145
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func (state *getaddrinfoState) toAddressList(res *C.struct_addrinfo) ([]string, string, error) {
|
||||
var (
|
||||
addrs []string
|
||||
canonname string
|
||||
)
|
||||
for r := res; r != nil; r = r.ai_next {
|
||||
if r.ai_canonname != nil {
|
||||
canonname = C.GoString(r.ai_canonname)
|
||||
}
|
||||
// We only asked for SOCK_STREAM, but check anyhow.
|
||||
if r.ai_socktype != C.SOCK_STREAM {
|
||||
continue
|
||||
}
|
||||
addr, err := state.addrinfoToString(r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
if len(addrs) < 1 {
|
||||
return nil, canonname, ErrOODNSNoAnswer
|
||||
}
|
||||
return addrs, canonname, nil
|
||||
}
|
||||
|
||||
// errGetaddrinfoUnknownFamily indicates we don't know the address family.
|
||||
var errGetaddrinfoUnknownFamily = errors.New("unknown address family")
|
||||
|
||||
// addrinfoToString is the function that converts a single entry
|
||||
// in the struct_addrinfos linked list into a string.
|
||||
//
|
||||
// This function is adapted from cgoLookupIPCNAME
|
||||
// https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L145
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func (state *getaddrinfoState) addrinfoToString(r *C.struct_addrinfo) (string, error) {
|
||||
switch r.ai_family {
|
||||
case C.AF_INET:
|
||||
sa := (*syscall.RawSockaddrInet4)(unsafe.Pointer(r.ai_addr))
|
||||
addr := net.IPAddr{IP: state.copyIP(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)),
|
||||
}
|
||||
return addr.String(), nil
|
||||
default:
|
||||
return "", errGetaddrinfoUnknownFamily
|
||||
}
|
||||
}
|
||||
|
||||
// staticAddrinfoWithInvalidFamily is an helper to construct an addrinfo struct
|
||||
// that we use in testing. (We cannot call CGO directly from tests.)
|
||||
func staticAddrinfoWithInvalidFamily() *C.struct_addrinfo {
|
||||
var value C.struct_addrinfo // zeroed by Go
|
||||
value.ai_socktype = C.SOCK_STREAM // this is what the code expects
|
||||
value.ai_family = 0 // but 0 is not AF_INET{,6}
|
||||
return &value
|
||||
}
|
||||
|
||||
// staticAddrinfoWithInvalidSocketType is an helper to construct an addrinfo struct
|
||||
// that we use in testing. (We cannot call CGO directly from tests.)
|
||||
func staticAddrinfoWithInvalidSocketType() *C.struct_addrinfo {
|
||||
var value C.struct_addrinfo // zeroed by Go
|
||||
value.ai_socktype = C.SOCK_DGRAM // not SOCK_STREAM
|
||||
return &value
|
||||
}
|
||||
|
||||
// copyIP 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 {
|
||||
if len(x) < 16 {
|
||||
return x.To16()
|
||||
}
|
||||
y := make(net.IP, len(x))
|
||||
copy(y, x)
|
||||
return y
|
||||
}
|
||||
|
||||
// ifnametoindex 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 {
|
||||
iface, err := net.InterfaceByIndex(idx) // internally uses caching
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return iface.Name
|
||||
}
|
89
internal/netxlite/getaddrinfo_cgo_test.go
Normal file
89
internal/netxlite/getaddrinfo_cgo_test.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
//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)
|
||||
}
|
||||
})
|
||||
}
|
161
internal/netxlite/getaddrinfo_linux.go
Normal file
161
internal/netxlite/getaddrinfo_linux.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
// Copyright 2011 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 cgo && linux
|
||||
|
||||
package netxlite
|
||||
|
||||
/*
|
||||
// Both glibc and musl expose the EAI_NODATA error if we
|
||||
// ask them to expose it through this define. See below for
|
||||
// more details on how each of the supported libcs hides
|
||||
// (or does not hide) the EAI_NODATA define.
|
||||
#cgo CFLAGS: -D_GNU_SOURCE
|
||||
#include <netdb.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Implementation note: the original Go codebase separated linux and android
|
||||
// but we want them to be in the same file, so we can implement tests for both
|
||||
// operating system and increase our confidence that the behavior will be the
|
||||
// one we'd like to see on Android systems.
|
||||
|
||||
var getaddrinfoAIFlags = getaddrinfoGetPlatformSpecificAIFlags(runtime.GOOS)
|
||||
|
||||
// This function returns the platforms-specific AI flags that go1.17.6
|
||||
// used to set when we merged resolver's code into ooni/probe-cli
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func getaddrinfoGetPlatformSpecificAIFlags(goos string) C.int {
|
||||
switch goos {
|
||||
case "android":
|
||||
return C.AI_CANONNAME
|
||||
default:
|
||||
// NOTE(rsc): In theory there are approximately balanced
|
||||
// arguments for and against including AI_ADDRCONFIG
|
||||
// in the flags (it includes IPv4 results only on IPv4 systems,
|
||||
// and similarly for IPv6), but in practice setting it causes
|
||||
// getaddrinfo to return the wrong canonical name on Linux.
|
||||
// So definitely leave it out.
|
||||
return C.AI_CANONNAME | C.AI_V4MAPPED | C.AI_ALL
|
||||
}
|
||||
}
|
||||
|
||||
// Making constants available to Go code so we can run tests (it seems
|
||||
// it's not possible to import C directly in tests, sadly).
|
||||
const (
|
||||
aiCanonname = C.AI_CANONNAME
|
||||
aiV4Mapped = C.AI_V4MAPPED
|
||||
aiAll = C.AI_ALL
|
||||
eaiSystem = C.EAI_SYSTEM
|
||||
eaiNoName = C.EAI_NONAME
|
||||
eaiBadFlags = C.EAI_BADFLAGS
|
||||
eaiNoData = C.EAI_NODATA
|
||||
)
|
||||
|
||||
// toError is the function that converts the return value from
|
||||
// the getaddrinfo function into a proper Go error.
|
||||
//
|
||||
// This function is adapted from cgoLookupIPCNAME
|
||||
// https://github.com/golang/go/blob/go1.17.6/src/net/cgo_unix.go#L145
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause.
|
||||
func (state *getaddrinfoState) toError(code int64, err error, goos string) error {
|
||||
switch code {
|
||||
case C.EAI_SYSTEM:
|
||||
if err == nil {
|
||||
// err should not be nil, but sometimes getaddrinfo returns
|
||||
// code == C.EAI_SYSTEM with err == nil on Linux.
|
||||
// The report claims that it happens when we have too many
|
||||
// open files, so use syscall.EMFILE (too many open files in system).
|
||||
// Most system calls would return ENFILE (too many open files),
|
||||
// so at the least EMFILE should be easy to recognize if this
|
||||
// comes up again. golang.org/issue/6232.
|
||||
err = syscall.EMFILE
|
||||
}
|
||||
return newErrGetaddrinfo(code, err)
|
||||
case C.EAI_NONAME:
|
||||
return newErrGetaddrinfo(code, ErrOODNSNoSuchHost)
|
||||
case C.EAI_NODATA:
|
||||
return state.toErrorNODATA(err, goos)
|
||||
default:
|
||||
return newErrGetaddrinfo(code, ErrOODNSMisbehaving)
|
||||
}
|
||||
}
|
||||
|
||||
// toErrorNODATA maps the EAI_NODATA value to the proper return value
|
||||
// depending on the underlying operating system.
|
||||
//
|
||||
// As of 2022-05-28, this is the status of the major C libraries whose
|
||||
// getaddrinfo return value we may end up processing here:
|
||||
//
|
||||
// 1. musl libc (statically linked Linux builds for official OONI
|
||||
// Probe packages we build): EAI_NODATA is defined in netdb.h in a
|
||||
// section guarded by _GNU_SOURCE and _BSD_SOURCE and the code
|
||||
// does not otherwise ever use this definition.
|
||||
//
|
||||
// 2. GNU libc (which is what you would get if you compile OONI Probe
|
||||
// for yourself in a GNU/Linux system): the codebase defines EAI_NODATA
|
||||
// inside netdb.h protected by __USE_GNU, which is defined to 1 in
|
||||
// include/features.h if the user defines _GNU_SOURCE. Additionally,
|
||||
// the getaddrinfo implementation returns EAI_NODATA when a name
|
||||
// exists but there's no associated address for such a name. There
|
||||
// was a bug, fixed in glibc 2.27, were EAI_NONAME was returned
|
||||
// when EAI_NODATA would actually have been more proper:
|
||||
//
|
||||
// https://sourceware.org/bugzilla/show_bug.cgi?id=21922
|
||||
//
|
||||
// 3. Android libc: EAI_NODATA is defined in netdb.h and is not
|
||||
// protected by any feature flag. The getaddrinfo function (as
|
||||
// of 4ebdeebef74) calls android_getaddrinfofornet, which in turns
|
||||
// calls android_getaddrinfofornetcontext. This function will
|
||||
// eventually call android_getaddrinfo_proxy. If this function
|
||||
// returns any status code different from EAI_SYSTEM, then bionic
|
||||
// will return its return value. Otherwise, the code ends up
|
||||
// calling explore_fqdn, which in turn calls nsdispatch, which
|
||||
// is what NetBSD is still doing today.
|
||||
//
|
||||
// So, android_getaddrinfo_proxy was introduced a long time
|
||||
// ago on October 28, 2010 by this commit:
|
||||
//
|
||||
// https://github.com/aosp-mirror/platform_bionic/commit/a1dbf0b453801620565e5911f354f82706b0200d
|
||||
//
|
||||
// Then a subsequent commit changed android_getaddrinfo_proxy
|
||||
// to basically default to EAI_NODATA on proxy errors:
|
||||
//
|
||||
// https://github.com/aosp-mirror/platform_bionic/commit/c63e59039d28c352e3053bb81319e960c392dbd4
|
||||
//
|
||||
// As of today and 4ebdeebef74, android_getaddrinfo_proxy returns
|
||||
// one of the following possible return codes:
|
||||
//
|
||||
// a) 0 on success;
|
||||
//
|
||||
// b) EAI_SYSTEM if it cannot speak to the proxy (which causes the code
|
||||
// to fall through to the original NetBSD implementation);
|
||||
//
|
||||
// c) EAI_NODATA in all the other cases.
|
||||
//
|
||||
// The above discussion about Android provides us with a theory that explains the
|
||||
// https://github.com/ooni/probe/issues/2029 issue. That said, we are still missing
|
||||
// some bits, e.g., why some Android 6 phones did not experience this problem.
|
||||
//
|
||||
// We originally proposed to handle the EAI_NODATA error on Android like it was a
|
||||
// EAI_NONAME error. However, this mapping seems very inaccurate. Any error inside
|
||||
// the DNS proxy could cause EAI_NODATA (_unless_ we're "lucky" for some reason
|
||||
// and the original NetBSD code runs). Therefore, the sanest choice is to introduce
|
||||
// a new OONI error describing this error condition `android_dns_cache_no_data`
|
||||
// and handle this error as a special case when checking for NXDOMAIN.
|
||||
func (state *getaddrinfoState) toErrorNODATA(err error, goos string) error {
|
||||
switch goos {
|
||||
case "android":
|
||||
return newErrGetaddrinfo(C.EAI_NODATA, ErrAndroidDNSCacheNoData)
|
||||
default:
|
||||
return newErrGetaddrinfo(C.EAI_NODATA, ErrOODNSNoAnswer)
|
||||
}
|
||||
}
|
181
internal/netxlite/getaddrinfo_linux_test.go
Normal file
181
internal/netxlite/getaddrinfo_linux_test.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
//go:build cgo && linux
|
||||
|
||||
package netxlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetaddrinfoAIFlags(t *testing.T) {
|
||||
var wrong bool
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
wrong = getaddrinfoAIFlags != aiCanonname
|
||||
default:
|
||||
wrong = getaddrinfoAIFlags != (aiCanonname | aiV4Mapped | aiAll)
|
||||
}
|
||||
if wrong {
|
||||
t.Fatal("wrong flags for platform")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetaddrinfoGetPlatformSpecificAiFlags(t *testing.T) {
|
||||
type args struct {
|
||||
goos string
|
||||
}
|
||||
type expects struct {
|
||||
flags int64
|
||||
}
|
||||
var inputs = []struct {
|
||||
name string
|
||||
args args
|
||||
expects expects
|
||||
}{{
|
||||
name: "using the Android platform",
|
||||
args: args{
|
||||
goos: "android",
|
||||
},
|
||||
expects: expects{
|
||||
flags: aiCanonname,
|
||||
},
|
||||
}, {
|
||||
name: "using Linux",
|
||||
args: args{
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
flags: aiCanonname | aiV4Mapped | aiAll,
|
||||
},
|
||||
}, {
|
||||
name: "when the platform name is empty",
|
||||
args: args{
|
||||
goos: "",
|
||||
},
|
||||
expects: expects{
|
||||
flags: aiCanonname | aiV4Mapped | aiAll,
|
||||
},
|
||||
}}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
flags := getaddrinfoGetPlatformSpecificAIFlags(input.args.goos)
|
||||
if int64(flags) != input.expects.flags {
|
||||
t.Fatal("invalid flags")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetaddrinfoStateToError(t *testing.T) {
|
||||
type args struct {
|
||||
code int64
|
||||
err error
|
||||
goos string
|
||||
}
|
||||
type expects struct {
|
||||
message string // message obtained using .Error
|
||||
code int64
|
||||
err error
|
||||
}
|
||||
var inputs = []struct {
|
||||
name string
|
||||
args args
|
||||
expects expects
|
||||
}{{
|
||||
name: "with C.EAI_SYSTEM and non-nil error",
|
||||
args: args{
|
||||
code: eaiSystem,
|
||||
err: syscall.EAGAIN,
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
message: syscall.EAGAIN.Error(),
|
||||
code: eaiSystem,
|
||||
err: syscall.EAGAIN,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_SYSTEM and nil error",
|
||||
args: args{
|
||||
code: eaiSystem,
|
||||
err: nil,
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
message: syscall.EMFILE.Error(),
|
||||
code: eaiSystem,
|
||||
err: syscall.EMFILE,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_NONAME",
|
||||
args: args{
|
||||
code: eaiNoName,
|
||||
err: nil,
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrOODNSNoSuchHost.Error(),
|
||||
code: eaiNoName,
|
||||
err: ErrOODNSNoSuchHost,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_NODATA on Linux",
|
||||
args: args{
|
||||
code: eaiNoData,
|
||||
err: nil,
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrOODNSNoAnswer.Error(),
|
||||
code: eaiNoData,
|
||||
err: ErrOODNSNoAnswer,
|
||||
},
|
||||
}, {
|
||||
name: "with C.EAI_NODATA on Android",
|
||||
args: args{
|
||||
code: eaiNoData,
|
||||
err: nil,
|
||||
goos: "android",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrAndroidDNSCacheNoData.Error(),
|
||||
code: eaiNoData,
|
||||
err: ErrAndroidDNSCacheNoData,
|
||||
},
|
||||
}, {
|
||||
name: "with an unhandled error",
|
||||
args: args{
|
||||
code: eaiBadFlags,
|
||||
err: nil,
|
||||
goos: "linux",
|
||||
},
|
||||
expects: expects{
|
||||
message: ErrOODNSMisbehaving.Error(),
|
||||
code: eaiBadFlags,
|
||||
err: ErrOODNSMisbehaving,
|
||||
},
|
||||
}}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
state := newGetaddrinfoState(getaddrinfoNumSlots)
|
||||
err := state.toError(input.args.code, input.args.err, input.args.goos)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error here")
|
||||
}
|
||||
if err.Error() != input.expects.message {
|
||||
t.Fatal("unexpected error message")
|
||||
}
|
||||
var gaierr *ErrGetaddrinfo
|
||||
if !errors.As(err, &gaierr) {
|
||||
t.Fatal("cannot convert error to ErrGetaddrinfo")
|
||||
}
|
||||
if gaierr.Code != input.expects.code {
|
||||
t.Fatal("unexpected code")
|
||||
}
|
||||
if !errors.Is(gaierr.Underlying, input.expects.err) {
|
||||
t.Fatal("unexpected underlying error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
12
internal/netxlite/getaddrinfo_otherwise.go
Normal file
12
internal/netxlite/getaddrinfo_otherwise.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !cgo
|
||||
|
||||
package netxlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
func getaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) {
|
||||
return net.DefaultResolver.LookupHost(ctx, domain)
|
||||
}
|
80
internal/netxlite/getaddrinfo_test.go
Normal file
80
internal/netxlite/getaddrinfo_test.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package netxlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetaddrinfoLookupHost(t *testing.T) {
|
||||
addrs, err := getaddrinfoLookupHost(context.Background(), "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(addrs) != 1 || addrs[0] != "127.0.0.1" {
|
||||
t.Fatal("unexpected addrs", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorToGetaddrinfoRetval(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{{
|
||||
name: "with valid getaddrinfo error",
|
||||
args: args{
|
||||
newErrGetaddrinfo(144, nil),
|
||||
},
|
||||
want: 144,
|
||||
}, {
|
||||
name: "with another kind of error",
|
||||
args: args{io.EOF},
|
||||
want: 0,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ErrorToGetaddrinfoRetval(tt.args.err); got != tt.want {
|
||||
t.Errorf("ErrorToGetaddrinfoRetval() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newErrGetaddrinfo(t *testing.T) {
|
||||
type args struct {
|
||||
code int64
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{{
|
||||
name: "common case",
|
||||
args: args{
|
||||
code: 17,
|
||||
err: io.EOF,
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := newErrGetaddrinfo(tt.args.code, tt.args.err)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error")
|
||||
}
|
||||
if !errors.Is(err, tt.args.err) {
|
||||
t.Fatal("Unwrap() is not working correctly")
|
||||
}
|
||||
if err.Error() != tt.args.err.Error() {
|
||||
t.Fatal("Error() is not working correctly")
|
||||
}
|
||||
if err.Code != tt.args.code {
|
||||
t.Fatal("Code has not been copied correctly")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
28
internal/netxlite/getaddrinfo_windows.go
Normal file
28
internal/netxlite/getaddrinfo_windows.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
//go:build cgo && windows
|
||||
|
||||
package netxlite
|
||||
|
||||
//#include <ws2tcpip.h>
|
||||
import "C"
|
||||
|
||||
import "syscall"
|
||||
|
||||
const getaddrinfoAIFlags = C.AI_CANONNAME
|
||||
|
||||
// Making constants available to Go code so we can run tests (it seems
|
||||
// it's not possible to import C directly in tests, sadly).
|
||||
const (
|
||||
aiCanonname = C.AI_CANONNAME
|
||||
)
|
||||
|
||||
// toError is the function that converts the return value from
|
||||
// the getaddrinfo function into a proper Go error.
|
||||
func (state *getaddrinfoState) toError(code int64, err error, goos string) error {
|
||||
if err == nil {
|
||||
// Implementation note: on Windows getaddrinfo directly
|
||||
// returns what is basically a winsock2 error. So if there
|
||||
// is no other error, just cast code to a syscall err.
|
||||
err = syscall.Errno(code)
|
||||
}
|
||||
return newErrGetaddrinfo(int64(code), err)
|
||||
}
|
81
internal/netxlite/getaddrinfo_windows_test.go
Normal file
81
internal/netxlite/getaddrinfo_windows_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
//go:build cgo && windows
|
||||
|
||||
package netxlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetaddrinfoAIFlags(t *testing.T) {
|
||||
var wrong bool
|
||||
wrong = getaddrinfoAIFlags != aiCanonname
|
||||
if wrong {
|
||||
t.Fatal("wrong flags for platform")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetaddrinfoStateToError(t *testing.T) {
|
||||
type args struct {
|
||||
code int64
|
||||
err error
|
||||
goos string
|
||||
}
|
||||
type expects struct {
|
||||
message string // message obtained using .Error
|
||||
code int64
|
||||
err error
|
||||
}
|
||||
var inputs = []struct {
|
||||
name string
|
||||
args args
|
||||
expects expects
|
||||
}{{
|
||||
name: "with nonzero return code and error",
|
||||
args: args{
|
||||
code: int64(WSAHOST_NOT_FOUND),
|
||||
err: syscall.EAGAIN,
|
||||
goos: "windows",
|
||||
},
|
||||
expects: expects{
|
||||
message: syscall.EAGAIN.Error(),
|
||||
code: int64(WSAHOST_NOT_FOUND),
|
||||
err: syscall.EAGAIN,
|
||||
},
|
||||
}, {
|
||||
name: "with return code and nil error",
|
||||
args: args{
|
||||
code: int64(WSAHOST_NOT_FOUND),
|
||||
err: nil,
|
||||
goos: "windows",
|
||||
},
|
||||
expects: expects{
|
||||
message: WSAHOST_NOT_FOUND.Error(),
|
||||
code: int64(WSAHOST_NOT_FOUND),
|
||||
err: WSAHOST_NOT_FOUND,
|
||||
},
|
||||
}}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
state := newGetaddrinfoState(getaddrinfoNumSlots)
|
||||
err := state.toError(input.args.code, input.args.err, input.args.goos)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error here")
|
||||
}
|
||||
if err.Error() != input.expects.message {
|
||||
t.Fatal("unexpected error message")
|
||||
}
|
||||
var gaierr *ErrGetaddrinfo
|
||||
if !errors.As(err, &gaierr) {
|
||||
t.Fatal("cannot convert error to ErrGetaddrinfo")
|
||||
}
|
||||
if gaierr.Code != input.expects.code {
|
||||
t.Fatal("unexpected code")
|
||||
}
|
||||
if !errors.Is(gaierr.Underlying, input.expects.err) {
|
||||
t.Fatal("unexpected underlying error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite/quictesting"
|
||||
"github.com/ooni/probe-cli/v3/internal/randx"
|
||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||
utls "gitlab.com/yawning/utls.git"
|
||||
)
|
||||
|
@ -71,7 +72,10 @@ func TestMeasureWithSystemResolver(t *testing.T) {
|
|||
const timeout = time.Nanosecond
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
addrs, err := r.LookupHost(ctx, "ooni.org")
|
||||
// Implementation note: Windows' resolver has caching so back to back tests
|
||||
// will fail unless we query for something that could bypass the cache itself
|
||||
// e.g. a domain containing a few random letters
|
||||
addrs, err := r.LookupHost(ctx, randx.Letters(7)+".ooni.org")
|
||||
if err == nil || err.Error() != netxlite.FailureGenericTimeoutError {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
|
|
|
@ -149,6 +149,7 @@ var Specs = []*ErrorSpec{
|
|||
NewWindowsError("NO_DATA", "DNS_no_answer"), // [ ] WSANO_DATA
|
||||
NewWindowsError("NO_RECOVERY", "DNS_non_recoverable_failure"), // [*] WSANO_RECOVERY
|
||||
NewWindowsError("TRY_AGAIN", "DNS_temporary_failure"), // [*] WSATRY_AGAIN
|
||||
NewWindowsError("HOST_NOT_FOUND", "DNS_NXDOMAIN_error"), // [*] WSAHOST_NOT_FOUND
|
||||
|
||||
// Implementation note: we need to specify acronyms we
|
||||
// want to be upper case in uppercase here. For example,
|
||||
|
@ -169,6 +170,10 @@ var Specs = []*ErrorSpec{
|
|||
NewLibraryError("SSL_invalid_certificate"),
|
||||
NewLibraryError("JSON_parse_error"),
|
||||
NewLibraryError("connection_already_closed"),
|
||||
|
||||
// QUIRKS: the following errors exist to clearly flag strange
|
||||
// underlying behavior implemented by platforms.
|
||||
NewLibraryError("Android_DNS_cache_no_data"),
|
||||
}
|
||||
|
||||
// mapSystemToLibrary maps the operating system name to the name
|
||||
|
|
|
@ -30,7 +30,12 @@ func (*TProxyStdlib) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLik
|
|||
|
||||
// LookupHost calls net.DefaultResolver.LookupHost.
|
||||
func (*TProxyStdlib) LookupHost(ctx context.Context, domain string) ([]string, error) {
|
||||
return net.DefaultResolver.LookupHost(ctx, domain)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// NewSimpleDialer returns a &net.Dialer{Timeout: timeout} instance.
|
||||
|
|
Loading…
Reference in New Issue
Block a user