diff --git a/internal/netxlite/errorsx/classify.go b/internal/netxlite/errorsx/classify.go index ea833f9..7af75da 100644 --- a/internal/netxlite/errorsx/classify.go +++ b/internal/netxlite/errorsx/classify.go @@ -179,6 +179,16 @@ func ClassifyQUICHandshakeError(err error) string { if errCode == quicTLSUnrecognizedName { return FailureSSLInvalidHostname } + + // quic.TransportError wraps OONI errors using the error + // code quic.InternalError. So, if the error code is + // an internal error, search for a OONI error and, if + // found, just return such an error. + if transportError.ErrorCode == quic.InternalError { + if s := failuresMap[transportError.ErrorMessage]; s != "" { + return s + } + } } return ClassifyGenericError(err) } diff --git a/internal/netxlite/errorsx/classify_test.go b/internal/netxlite/errorsx/classify_test.go index bcee1a5..3759336 100644 --- a/internal/netxlite/errorsx/classify_test.go +++ b/internal/netxlite/errorsx/classify_test.go @@ -210,6 +210,16 @@ func TestClassifyQUICHandshakeError(t *testing.T) { } }) + t.Run("for a TransportError wrapping an OONI error", func(t *testing.T) { + err := &quic.TransportError{ + ErrorCode: quic.InternalError, + ErrorMessage: FailureHostUnreachable, + } + if ClassifyQUICHandshakeError(err) != FailureHostUnreachable { + t.Fatal("unexpected results") + } + }) + t.Run("for another kind of error", func(t *testing.T) { if ClassifyQUICHandshakeError(io.EOF) != FailureEOFError { t.Fatal("unexpected result") diff --git a/internal/netxlite/errorsx/errno.go b/internal/netxlite/errorsx/errno.go index 9bdb1d0..aa0bc1e 100644 --- a/internal/netxlite/errorsx/errno.go +++ b/internal/netxlite/errorsx/errno.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// Generated: 2021-09-07 20:42:20.172357 +0200 CEST m=+0.135433792 +// Generated: 2021-09-08 23:09:33.336763 +0200 CEST m=+0.192836793 package errorsx @@ -60,6 +60,49 @@ const ( FailureJSONParseError = "json_parse_error" ) +// failureMap lists all failures so we can match them +// when they are wrapped by quic.TransportError. +var failuresMap = map[string]string{ + "operation_canceled": "operation_canceled", + "connection_refused": "connection_refused", + "connection_reset": "connection_reset", + "host_unreachable": "host_unreachable", + "timed_out": "timed_out", + "address_family_not_supported": "address_family_not_supported", + "address_in_use": "address_in_use", + "address_not_available": "address_not_available", + "already_connected": "already_connected", + "bad_address": "bad_address", + "bad_file_descriptor": "bad_file_descriptor", + "connection_aborted": "connection_aborted", + "connection_already_in_progress": "connection_already_in_progress", + "destination_address_required": "destination_address_required", + "interrupted": "interrupted", + "invalid_argument": "invalid_argument", + "message_size": "message_size", + "network_down": "network_down", + "network_reset": "network_reset", + "network_unreachable": "network_unreachable", + "no_buffer_space": "no_buffer_space", + "no_protocol_option": "no_protocol_option", + "not_a_socket": "not_a_socket", + "not_connected": "not_connected", + "operation_would_block": "operation_would_block", + "permission_denied": "permission_denied", + "protocol_not_supported": "protocol_not_supported", + "wrong_protocol_type": "wrong_protocol_type", + "dns_bogon_error": "dns_bogon_error", + "dns_nxdomain_error": "dns_nxdomain_error", + "eof_error": "eof_error", + "generic_timeout_error": "generic_timeout_error", + "quic_incompatible_version": "quic_incompatible_version", + "ssl_failed_handshake": "ssl_failed_handshake", + "ssl_invalid_hostname": "ssl_invalid_hostname", + "ssl_unknown_authority": "ssl_unknown_authority", + "ssl_invalid_certificate": "ssl_invalid_certificate", + "json_parse_error": "json_parse_error", +} + // classifySyscallError converts a syscall error to the // proper OONI error. Returns the OONI error string // on success, an empty string otherwise. diff --git a/internal/netxlite/errorsx/errno_test.go b/internal/netxlite/errorsx/errno_test.go index 9e14921..b5f5709 100644 --- a/internal/netxlite/errorsx/errno_test.go +++ b/internal/netxlite/errorsx/errno_test.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// Generated: 2021-09-07 20:42:20.217324 +0200 CEST m=+0.180405501 +// Generated: 2021-09-08 23:09:33.382965 +0200 CEST m=+0.239039834 package errorsx diff --git a/internal/netxlite/errorsx/errno_unix.go b/internal/netxlite/errorsx/errno_unix.go index 8bd90a2..8a6ad8c 100644 --- a/internal/netxlite/errorsx/errno_unix.go +++ b/internal/netxlite/errorsx/errno_unix.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// Generated: 2021-09-07 20:42:20.037485 +0200 CEST m=+0.000558667 +// Generated: 2021-09-08 23:09:33.144513 +0200 CEST m=+0.000582543 package errorsx diff --git a/internal/netxlite/errorsx/errno_windows.go b/internal/netxlite/errorsx/errno_windows.go index 713ef75..78fde75 100644 --- a/internal/netxlite/errorsx/errno_windows.go +++ b/internal/netxlite/errorsx/errno_windows.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// Generated: 2021-09-07 20:42:20.147482 +0200 CEST m=+0.110557751 +// Generated: 2021-09-08 23:09:33.310337 +0200 CEST m=+0.166410043 package errorsx diff --git a/internal/netxlite/errorsx/internal/generrno/main.go b/internal/netxlite/errorsx/internal/generrno/main.go index 578f0ea..dd0ab97 100644 --- a/internal/netxlite/errorsx/internal/generrno/main.go +++ b/internal/netxlite/errorsx/internal/generrno/main.go @@ -194,6 +194,15 @@ func writeGenericFile() { } 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") + for _, spec := range Specs { + filePrintf(filep, "\t\"%s\": \"%s\",\n", + spec.AsFailureString(), spec.AsFailureString()) + } + 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") fileWrite(filep, "// on success, an empty string otherwise.\n") diff --git a/internal/netxlite/integration_test.go b/internal/netxlite/integration_test.go index 36e6a37..514ab40 100644 --- a/internal/netxlite/integration_test.go +++ b/internal/netxlite/integration_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/apex/log" + "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/netxlite" utls "gitlab.com/yawning/utls.git" ) @@ -90,3 +91,45 @@ func TestUTLSHandshaker(t *testing.T) { } }) } + +func TestQUICDialer(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") + } + + t.Run("works as intended", func(t *testing.T) { + tlsConfig := &tls.Config{ + ServerName: "dns.google", + } + d := netxlite.NewQUICDialerWithoutResolver( + netxlite.NewQUICListener(), log.Log, + ) + ctx := context.Background() + sess, err := d.DialContext( + ctx, "udp", "8.8.8.8:443", tlsConfig, &quic.Config{}) + if err != nil { + t.Fatal("not the error we expected", err) + } + <-sess.HandshakeComplete().Done() + if err := sess.CloseWithError(0, ""); err != nil { + t.Fatal(err) + } + }) + + t.Run("can guess the SNI and ALPN when using a domain name for web", func(t *testing.T) { + d := netxlite.NewQUICDialerWithResolver( + netxlite.NewQUICListener(), log.Log, + netxlite.NewResolverSystem(log.Log), + ) + ctx := context.Background() + sess, err := d.DialContext( + ctx, "udp", "dns.google:443", &tls.Config{}, &quic.Config{}) + if err != nil { + t.Fatal("not the error we expected", err) + } + <-sess.HandshakeComplete().Done() + if err := sess.CloseWithError(0, ""); err != nil { + t.Fatal(err) + } + }) +} diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index 862bcf1..ebb3182 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -67,7 +67,15 @@ type QUICDialer interface { // // 6. if a dialer wraps a resolver, the dialer will forward // the CloseIdleConnection call to its resolver (which is -// instrumental to manage a DoH resolver connections properly). +// instrumental to manage a DoH resolver connections properly); +// +// 7. will use the bundled CA unless you provide another CA; +// +// 8. will attempt to guess SNI when resolving domain names +// and otherwise will not set the SNI; +// +// 9. will attempt to guess ALPN when the port is known and +// otherwise will not set the ALPN. func NewQUICDialerWithResolver(listener QUICListener, logger Logger, resolver Resolver) QUICDialer { return &quicDialerLogger{ diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index 5fa349e..2f89203 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -144,28 +144,6 @@ func TestQUICDialerQUICGo(t *testing.T) { } }) - t.Run("works as intended", func(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - tlsConfig := &tls.Config{ - ServerName: "dns.google", - } - systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, - } - ctx := context.Background() - sess, err := systemdialer.DialContext( - ctx, "udp", "8.8.8.8:443", tlsConfig, &quic.Config{}) - if err != nil { - t.Fatal("not the error we expected", err) - } - <-sess.HandshakeComplete().Done() - if err := sess.CloseWithError(0, ""); err != nil { - t.Fatal(err) - } - }) - t.Run("TLS defaults for web", func(t *testing.T) { expected := errors.New("mocked error") var gotTLSConfig *tls.Config