fix(netxlite): map additional GetAddrInfoW errors (#521)

On Windows, GetAddrInfoW is a syscall and the Go resolver does
not attempt to map errors beyond WSA_HOST_NOT_FOUND, which becomes
"no such host", which we map to "dns_nxdomain_error".

See https://github.com/golang/go/blob/go1.17.1/src/net/lookup_windows.go#L16.

To map more GetAddrInfoW errors, thus, we need to enhance our
error classifier to have system specific errors.

Then, we need to filter for the WSA errors that are most likely
to pop up and map them to OONI failures. Those are three:

- WSANO_DATA which we have from our own UDP resolver as well
and which we can map to `dns_no_answer`

- WSANO_RECOVERY which we don't have but existed for MK so
we will use `dns_non_recoverable_failure`, which was an MK error

- WSATRY_AGAIN which likewise we map to the error that MK
used to emit, so `dns_temporary_failure`

This diff should address https://github.com/ooni/probe/issues/1467.
This commit is contained in:
Simone Basso 2021-09-29 11:21:28 +02:00 committed by GitHub
commit 9967803c31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1648 additions and 221 deletions

View file

@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
"sort"
"time"
"github.com/iancoleman/strcase"
@ -17,15 +18,53 @@ type ErrorSpec struct {
// failure is the error name according to OONI (e.g., FailureConnectionRefused).
failure string
// system specifies for which system this error is valid. If
// this value is empty then the spec is valid for all systems.
system string
}
// IsForSystem returns true when the spec's system matches the
// given system or when the spec's system is "".
func (es *ErrorSpec) IsForSystem(system string) bool {
return es.system == system || es.system == ""
}
// AsErrnoName returns the name of the corresponding errno, if this
// is a system error, or panics otherwise.
func (es *ErrorSpec) AsErrnoName() string {
func (es *ErrorSpec) AsErrnoName(system string) string {
if !es.IsSystemError() {
panic("not a system error")
}
return es.errno
s := es.errno
if system == "windows" {
s = "WSA" + s
}
return s
}
// AsCanonicalErrnoName attempts to canonicalize the errno name
// using the following algorithm:
//
// - if the error is present on all systems, use the unix name;
//
// - otherwise, use the system-name name for the error.
//
// So, for example, we will get:
//
// - EWOULDBLOCK because it's present on both Unix and Windows;
//
// - WSANO_DATA because it's Windows only.
func (es *ErrorSpec) AsCanonicalErrnoName() string {
if !es.IsSystemError() {
panic("not a system error")
}
switch es.system {
case "windows":
return es.AsErrnoName(es.system)
default:
return es.errno
}
}
// AsFailureVar returns the name of the failure var.
@ -41,7 +80,13 @@ func (es *ErrorSpec) AsFailureString() string {
// NewSystemError constructs a new ErrorSpec representing a system
// error, i.e., an error returned by a system call.
func NewSystemError(errno, failure string) *ErrorSpec {
return &ErrorSpec{errno: errno, failure: failure}
return &ErrorSpec{errno: errno, failure: failure, system: ""}
}
// NewWindowsError constructs a new ErrorSpec representing a
// Windows-only system error, i.e., an error returned by a system call.
func NewWindowsError(errno, failure string) *ErrorSpec {
return &ErrorSpec{errno: errno, failure: failure, system: "windows"}
}
// NewLibraryError constructs a new ErrorSpec representing a library
@ -87,6 +132,24 @@ var Specs = []*ErrorSpec{
NewSystemError("EPROTONOSUPPORT", "protocol_not_supported"),
NewSystemError("EPROTOTYPE", "wrong_protocol_type"),
// Windows-only system errors.
//
// Why do we have these extra errors here? Because on Windows
// GetAddrInfoW is a system call while it's a library call
// on Unix. Because of that, the Go stdlib treats Windows and
// Unix differently and allows more syscall errors to slip
// through when we're performing DNS resolutions.
//
// Because MK handled _some_ getaddrinfo return codes, I've
// marked names compatible with MK using [*].
//
// Implementation note: we need to specify acronyms we
// want to be upper case in uppercase here. For example,
// we must write "DNS" rather than writing "dns".
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
// Implementation note: we need to specify acronyms we
// want to be upper case in uppercase here. For example,
// we must write "DNS" rather than writing "dns".
@ -106,6 +169,19 @@ var Specs = []*ErrorSpec{
NewLibraryError("connection_already_closed"),
}
// mapSystemToLibrary maps the operating system name to the name
// of the related golang.org/x/sys/$name library.
func mapSystemToLibrary(system string) string {
switch system {
case "android", "darwin", "freebsd", "ios", "linux":
return "unix"
case "windows":
return "windows"
default:
panic(fmt.Sprintf("unsupported system: %s", system))
}
}
func fileCreate(filename string) *os.File {
filep, err := os.Create(filename)
if err != nil {
@ -137,74 +213,31 @@ func gofmt(filename string) {
}
}
func writeSystemSpecificFile(kind, library, prefix string) {
filename := "errno_" + kind + ".go"
func writeSystemSpecificFile(system string) {
filename := "errno_" + system + ".go"
filep := fileCreate(filename)
library := mapSystemToLibrary(system)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package netxlite\n\n")
filePrintf(filep, "import \"golang.org/x/sys/%s\"\n\n", library)
fileWrite(filep, "const (\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = %s.%s%s\n",
spec.AsErrnoName(), library, prefix, spec.AsErrnoName())
}
fileWrite(filep, ")\n\n")
fileClose(filep)
gofmt(filename)
}
func writeGenericFile() {
filename := "errno.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package netxlite\n\n")
fileWrite(filep, "//go:generate go run ./internal/generrno/\n\n")
fileWrite(filep, "import (\n")
fileWrite(filep, "\t\"errors\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "// This enumeration lists the failures defined at\n")
fileWrite(filep, "// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md\n")
fileWrite(filep, "const (\n")
fileWrite(filep, "//\n")
fileWrite(filep, "// System errors\n")
fileWrite(filep, "//\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = \"%s\"\n",
spec.AsFailureVar(),
spec.AsFailureString())
}
fileWrite(filep, "\n")
fileWrite(filep, "//\n")
fileWrite(filep, "// Library errors\n")
fileWrite(filep, "//\n")
for _, spec := range Specs {
if spec.IsSystemError() {
continue
}
filePrintf(filep, "\t%s = \"%s\"\n",
spec.AsFailureVar(),
spec.AsFailureString())
}
filePrintf(filep, "\t\"golang.org/x/sys/%s\"\n", library)
fileWrite(filep, ")\n\n")
fileWrite(filep, "// failureMap lists all failures so we can match them\n")
fileWrite(filep, "// when they are wrapped by quic.TransportError.\n")
fileWrite(filep, "var failuresMap = map[string]string{\n")
fileWrite(filep, "// This enumeration provides a canonical name for\n")
fileWrite(filep, "// every system-call error we support on this systems.\n")
fileWrite(filep, "const (\n")
for _, spec := range Specs {
filePrintf(filep, "\t\"%s\": \"%s\",\n",
spec.AsFailureString(), spec.AsFailureString())
if !spec.IsSystemError() || !spec.IsForSystem(system) {
continue
}
filePrintf(filep, "\t%s = %s.%s\n",
spec.AsCanonicalErrnoName(), library, spec.AsErrnoName(system))
}
fileWrite(filep, "}\n\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "// classifySyscallError converts a syscall error to the\n")
fileWrite(filep, "// proper OONI error. Returns the OONI error string\n")
@ -216,10 +249,10 @@ func writeGenericFile() {
fileWrite(filep, "\t}\n")
fileWrite(filep, "\tswitch errno {\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
if !spec.IsSystemError() || !spec.IsForSystem(library) {
continue
}
filePrintf(filep, "\tcase %s:\n", spec.AsErrnoName())
filePrintf(filep, "\tcase %s.%s:\n", library, spec.AsErrnoName(system))
filePrintf(filep, "\t\treturn %s\n", spec.AsFailureVar())
}
fileWrite(filep, "\t}\n")
@ -230,9 +263,56 @@ func writeGenericFile() {
gofmt(filename)
}
func writeGenericTestFile() {
filename := "errno_test.go"
func writeGenericFile() {
filename := "errno.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package netxlite\n\n")
fileWrite(filep, "//go:generate go run ./internal/generrno/\n\n")
fileWrite(filep, "// This enumeration lists the failures defined at\n")
fileWrite(filep, "// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md\n")
fileWrite(filep, "const (\n")
names := make(map[string]string)
for _, spec := range Specs {
names[spec.AsFailureVar()] = spec.AsFailureString()
}
var nameskeys []string
for key := range names {
nameskeys = append(nameskeys, key)
}
sort.Strings(nameskeys)
for _, key := range nameskeys {
filePrintf(filep, "\t%s = \"%s\"\n", key, names[key])
}
fileWrite(filep, ")\n\n")
fileWrite(filep, "// failureMap lists all failures so we can match them\n")
fileWrite(filep, "// when they are wrapped by quic.TransportError.\n")
fileWrite(filep, "var failuresMap = map[string]string{\n")
failures := make(map[string]string)
for _, spec := range Specs {
failures[spec.AsFailureString()] = spec.AsFailureString()
}
var failureskey []string
for key := range failures {
failureskey = append(failureskey, key)
}
sort.Strings(failureskey)
for _, key := range failureskey {
filePrintf(filep, "\t\"%s\": \"%s\",\n", key, failures[key])
}
fileWrite(filep, "}\n\n")
fileClose(filep)
gofmt(filename)
}
func writeSystemSpecificTestFile(system string) {
filename := fmt.Sprintf("errno_%s_test.go", system)
filep := fileCreate(filename)
library := mapSystemToLibrary(system)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
@ -241,6 +321,8 @@ func writeGenericTestFile() {
fileWrite(filep, "\t\"io\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, "\t\"testing\"\n")
fileWrite(filep, "\n")
filePrintf(filep, "\t\"golang.org/x/sys/%s\"\n", library)
fileWrite(filep, ")\n\n")
fileWrite(filep, "func TestClassifySyscallError(t *testing.T) {\n")
@ -251,13 +333,13 @@ func writeGenericTestFile() {
fileWrite(filep, "\t})\n\n")
for _, spec := range Specs {
if !spec.IsSystemError() {
if !spec.IsSystemError() || !spec.IsForSystem(library) {
continue
}
filePrintf(filep, "\tt.Run(\"for %s\", func (t *testing.T) {\n",
spec.AsErrnoName())
filePrintf(filep, "\t\tif v := classifySyscallError(%s); v != %s {\n",
spec.AsErrnoName(), spec.AsFailureVar())
spec.AsErrnoName(system))
filePrintf(filep, "\t\tif v := classifySyscallError(%s.%s); v != %s {\n",
library, spec.AsErrnoName(system), spec.AsFailureVar())
filePrintf(filep, "\t\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
spec.AsFailureVar())
fileWrite(filep, "\t\t}\n")
@ -275,13 +357,20 @@ func writeGenericTestFile() {
gofmt(filename)
}
func main() {
writeSystemSpecificFile("android", "unix", "")
writeSystemSpecificFile("darwin", "unix", "")
writeSystemSpecificFile("freebsd", "unix", "")
writeSystemSpecificFile("ios", "unix", "")
writeSystemSpecificFile("linux", "unix", "")
writeSystemSpecificFile("windows", "windows", "WSA")
writeGenericFile()
writeGenericTestFile()
// SupportedSystems contains the list of supported systems.
var SupportedSystems = []string{
"android",
"darwin",
"freebsd",
"ios",
"linux",
"windows",
}
func main() {
for _, system := range SupportedSystems {
writeSystemSpecificFile(system)
writeSystemSpecificTestFile(system)
}
writeGenericFile()
}