refactor: move quic dns dialing to netxlite (#408)
Part of https://github.com/ooni/probe/issues/1505
This commit is contained in:
parent
a4d61a4be4
commit
f1f5ed342e
|
@ -174,7 +174,7 @@ func NewQUICDialer(config Config) QUICDialer {
|
||||||
if config.TLSSaver != nil {
|
if config.TLSSaver != nil {
|
||||||
d = quicdialer.HandshakeSaver{Saver: config.TLSSaver, Dialer: d}
|
d = quicdialer.HandshakeSaver{Saver: config.TLSSaver, Dialer: d}
|
||||||
}
|
}
|
||||||
d = &quicdialer.DNSDialer{Resolver: config.FullResolver, Dialer: d}
|
d = &netxlite.QUICDialerResolver{Resolver: config.FullResolver, Dialer: d}
|
||||||
var dialer QUICDialer = &httptransport.QUICWrapperDialer{Dialer: d}
|
var dialer QUICDialer = &httptransport.QUICWrapperDialer{Dialer: d}
|
||||||
return dialer
|
return dialer
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
package quicdialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNSDialer is a dialer that uses the configured Resolver to resolve a
|
|
||||||
// domain name to IP addresses
|
|
||||||
type DNSDialer struct {
|
|
||||||
Dialer ContextDialer
|
|
||||||
Resolver Resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext implements ContextDialer.DialContext
|
|
||||||
func (d DNSDialer) DialContext(
|
|
||||||
ctx context.Context, network, host string,
|
|
||||||
tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
|
|
||||||
onlyhost, onlyport, err := net.SplitHostPort(host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// TODO(kelmenhorst): Should this be somewhere else?
|
|
||||||
// failure if tlsCfg is nil but that should not happen
|
|
||||||
if tlsCfg.ServerName == "" {
|
|
||||||
tlsCfg.ServerName = onlyhost
|
|
||||||
}
|
|
||||||
var addrs []string
|
|
||||||
addrs, err = d.LookupHost(ctx, onlyhost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var errorslist []error
|
|
||||||
for _, addr := range addrs {
|
|
||||||
target := net.JoinHostPort(addr, onlyport)
|
|
||||||
sess, err := d.Dialer.DialContext(
|
|
||||||
ctx, network, target, tlsCfg, cfg)
|
|
||||||
if err == nil {
|
|
||||||
return sess, nil
|
|
||||||
}
|
|
||||||
errorslist = append(errorslist, err)
|
|
||||||
}
|
|
||||||
// TODO(bassosimone): maybe ReduceErrors could be in netx/internal.
|
|
||||||
return nil, netxlite.ReduceErrors(errorslist)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupHost implements Resolver.LookupHost
|
|
||||||
func (d DNSDialer) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
||||||
if net.ParseIP(hostname) != nil {
|
|
||||||
return []string{hostname}, nil
|
|
||||||
}
|
|
||||||
return d.Resolver.LookupHost(ctx, hostname)
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
package quicdialer_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/quicdialer"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockableResolver struct {
|
|
||||||
Addresses []string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockableResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
|
|
||||||
return r.Addresses, r.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerSuccess(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
dialer := quicdialer.DNSDialer{
|
|
||||||
Resolver: new(net.Resolver), Dialer: &netxlite.QUICDialerQUICGo{
|
|
||||||
QUICListener: &netxlite.QUICListenerStdlib{},
|
|
||||||
}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "www.google.com:443",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error", err)
|
|
||||||
}
|
|
||||||
if sess == nil {
|
|
||||||
t.Fatal("non nil sess expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerNoPort(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
dialer := quicdialer.DNSDialer{
|
|
||||||
Resolver: new(net.Resolver), Dialer: &netxlite.QUICDialerQUICGo{}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "www.google.com",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if sess != nil {
|
|
||||||
t.Fatal("expected a nil sess here")
|
|
||||||
}
|
|
||||||
if err.Error() != "address www.google.com: missing port in address" {
|
|
||||||
t.Fatal("not the error we expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerLookupHostAddress(t *testing.T) {
|
|
||||||
dialer := quicdialer.DNSDialer{Resolver: MockableResolver{
|
|
||||||
Err: errors.New("mocked error"),
|
|
||||||
}}
|
|
||||||
addrs, err := dialer.LookupHost(context.Background(), "1.1.1.1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(addrs) != 1 || addrs[0] != "1.1.1.1" {
|
|
||||||
t.Fatal("not the result we expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerLookupHostFailure(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
expected := errors.New("mocked error")
|
|
||||||
dialer := quicdialer.DNSDialer{Resolver: MockableResolver{
|
|
||||||
Err: expected,
|
|
||||||
}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "dns.google.com:853",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if !errors.Is(err, expected) {
|
|
||||||
t.Fatal("not the error we expected")
|
|
||||||
}
|
|
||||||
if sess != nil {
|
|
||||||
t.Fatal("expected nil sess")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerInvalidPort(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
dialer := quicdialer.DNSDialer{
|
|
||||||
Resolver: new(net.Resolver), Dialer: &netxlite.QUICDialerQUICGo{
|
|
||||||
QUICListener: &netxlite.QUICListenerStdlib{},
|
|
||||||
}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "www.google.com:0",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if sess != nil {
|
|
||||||
t.Fatal("expected nil sess")
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(err.Error(), "sendto: invalid argument") &&
|
|
||||||
!strings.HasSuffix(err.Error(), "sendto: can't assign requested address") {
|
|
||||||
t.Fatal("not the error we expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerInvalidPortSyntax(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
dialer := quicdialer.DNSDialer{
|
|
||||||
Resolver: new(net.Resolver), Dialer: &netxlite.QUICDialerQUICGo{
|
|
||||||
QUICListener: &netxlite.QUICListenerStdlib{},
|
|
||||||
}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "www.google.com:port",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if sess != nil {
|
|
||||||
t.Fatal("expected nil sess")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, strconv.ErrSyntax) {
|
|
||||||
t.Fatal("not the error we expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSDialerDialEarlyFails(t *testing.T) {
|
|
||||||
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
|
||||||
expected := errors.New("mocked DialEarly error")
|
|
||||||
dialer := quicdialer.DNSDialer{
|
|
||||||
Resolver: new(net.Resolver), Dialer: MockDialer{Err: expected}}
|
|
||||||
sess, err := dialer.DialContext(
|
|
||||||
context.Background(), "udp", "www.google.com:443",
|
|
||||||
tlsConf, &quic.Config{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if sess != nil {
|
|
||||||
t.Fatal("expected nil sess")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, expected) {
|
|
||||||
t.Fatal("not the error we expected")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -55,7 +55,7 @@ func (d *DialerResolver) DialContext(ctx context.Context, network, address strin
|
||||||
}
|
}
|
||||||
errorslist = append(errorslist, err)
|
errorslist = append(errorslist, err)
|
||||||
}
|
}
|
||||||
return nil, ReduceErrors(errorslist)
|
return nil, reduceErrors(errorslist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupHost performs a domain name resolution.
|
// lookupHost performs a domain name resolution.
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReduceErrors finds a known error in a list of errors since
|
// reduceErrors finds a known error in a list of errors since
|
||||||
// it's probably most relevant.
|
// it's probably most relevant.
|
||||||
//
|
//
|
||||||
// Deprecation warning
|
// Deprecation warning
|
||||||
|
@ -16,7 +16,7 @@ import (
|
||||||
//
|
//
|
||||||
// In perspective, we would like to transition to a scenario where
|
// In perspective, we would like to transition to a scenario where
|
||||||
// full dialing is NOT used for measurements and we return a multierror here.
|
// full dialing is NOT used for measurements and we return a multierror here.
|
||||||
func ReduceErrors(errorslist []error) error {
|
func reduceErrors(errorslist []error) error {
|
||||||
if len(errorslist) == 0 {
|
if len(errorslist) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,14 @@ import (
|
||||||
|
|
||||||
func TestReduceErrors(t *testing.T) {
|
func TestReduceErrors(t *testing.T) {
|
||||||
t.Run("no errors", func(t *testing.T) {
|
t.Run("no errors", func(t *testing.T) {
|
||||||
result := ReduceErrors(nil)
|
result := reduceErrors(nil)
|
||||||
if result != nil {
|
if result != nil {
|
||||||
t.Fatal("wrong result")
|
t.Fatal("wrong result")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("single error", func(t *testing.T) {
|
t.Run("single error", func(t *testing.T) {
|
||||||
err := errors.New("mocked error")
|
err := errors.New("mocked error")
|
||||||
result := ReduceErrors([]error{err})
|
result := reduceErrors([]error{err})
|
||||||
if result != err {
|
if result != err {
|
||||||
t.Fatal("wrong result")
|
t.Fatal("wrong result")
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func TestReduceErrors(t *testing.T) {
|
||||||
t.Run("multiple errors", func(t *testing.T) {
|
t.Run("multiple errors", func(t *testing.T) {
|
||||||
err1 := errors.New("mocked error #1")
|
err1 := errors.New("mocked error #1")
|
||||||
err2 := errors.New("mocked error #2")
|
err2 := errors.New("mocked error #2")
|
||||||
result := ReduceErrors([]error{err1, err2})
|
result := reduceErrors([]error{err1, err2})
|
||||||
if result.Error() != "mocked error #1" {
|
if result.Error() != "mocked error #1" {
|
||||||
t.Fatal("wrong result")
|
t.Fatal("wrong result")
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func TestReduceErrors(t *testing.T) {
|
||||||
Failure: errorx.FailureConnectionRefused,
|
Failure: errorx.FailureConnectionRefused,
|
||||||
}
|
}
|
||||||
err4 := errors.New("mocked error #3")
|
err4 := errors.New("mocked error #3")
|
||||||
result := ReduceErrors([]error{err1, err2, err3, err4})
|
result := reduceErrors([]error{err1, err2, err3, err4})
|
||||||
if result.Error() != errorx.FailureConnectionRefused {
|
if result.Error() != errorx.FailureConnectionRefused {
|
||||||
t.Fatal("wrong result")
|
t.Fatal("wrong result")
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,6 @@ type QUICContextDialer interface {
|
||||||
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QUICDialer dials QUIC connections.
|
|
||||||
type QUICDialer interface {
|
|
||||||
// DialContext establishes a new QUIC session using the given
|
|
||||||
// network and address. The tlsConfig and the quicConfig arguments
|
|
||||||
// MUST NOT be nil. Returns either the session or an error.
|
|
||||||
Dial(network, address string, tlsConfig *tls.Config,
|
|
||||||
quicConfig *quic.Config) (quic.EarlySession, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUICListener listens for QUIC connections.
|
// QUICListener listens for QUIC connections.
|
||||||
type QUICListener interface {
|
type QUICListener interface {
|
||||||
// Listen creates a new listening PacketConn.
|
// Listen creates a new listening PacketConn.
|
||||||
|
@ -100,3 +91,55 @@ func (sess *quicSessionOwnsConn) CloseWithError(
|
||||||
sess.conn.Close()
|
sess.conn.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QUICDialerResolver is a dialer that uses the configured Resolver
|
||||||
|
// to resolve a domain name to IP addrs.
|
||||||
|
type QUICDialerResolver struct {
|
||||||
|
// Dialer is the underlying QUIC dialer.
|
||||||
|
Dialer QUICContextDialer
|
||||||
|
|
||||||
|
// Resolver is the underlying resolver.
|
||||||
|
Resolver Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements QUICContextDialer.DialContext
|
||||||
|
func (d *QUICDialerResolver) DialContext(
|
||||||
|
ctx context.Context, network, address string,
|
||||||
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
|
||||||
|
onlyhost, onlyport, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO(kelmenhorst): Should this be somewhere else?
|
||||||
|
// failure if tlsCfg is nil but that should not happen
|
||||||
|
if tlsConfig.ServerName == "" {
|
||||||
|
tlsConfig.ServerName = onlyhost
|
||||||
|
}
|
||||||
|
addrs, err := d.lookupHost(ctx, onlyhost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO(bassosimone): here we should be using multierror rather
|
||||||
|
// than just calling ReduceErrors. We are not ready to do that
|
||||||
|
// yet, though. To do that, we need first to modify nettests so
|
||||||
|
// that we actually avoid dialing when measuring.
|
||||||
|
var errorslist []error
|
||||||
|
for _, addr := range addrs {
|
||||||
|
target := net.JoinHostPort(addr, onlyport)
|
||||||
|
sess, err := d.Dialer.DialContext(
|
||||||
|
ctx, network, target, tlsConfig, quicConfig)
|
||||||
|
if err == nil {
|
||||||
|
return sess, nil
|
||||||
|
}
|
||||||
|
errorslist = append(errorslist, err)
|
||||||
|
}
|
||||||
|
return nil, reduceErrors(errorslist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupHost performs a domain name resolution.
|
||||||
|
func (d *QUICDialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
|
||||||
|
if net.ParseIP(hostname) != nil {
|
||||||
|
return []string{hostname}, nil
|
||||||
|
}
|
||||||
|
return d.Resolver.LookupHost(ctx, hostname)
|
||||||
|
}
|
||||||
|
|
|
@ -133,3 +133,95 @@ func TestQUICDialerWorksAsIntended(t *testing.T) {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQUICDialerResolverSuccess(t *testing.T) {
|
||||||
|
tlsConfig := &tls.Config{NextProtos: []string{"h3"}}
|
||||||
|
dialer := &QUICDialerResolver{
|
||||||
|
Resolver: &net.Resolver{}, Dialer: &QUICDialerQUICGo{
|
||||||
|
QUICListener: &QUICListenerStdlib{},
|
||||||
|
}}
|
||||||
|
sess, err := dialer.DialContext(
|
||||||
|
context.Background(), "udp", "www.google.com:443",
|
||||||
|
tlsConfig, &quic.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
<-sess.HandshakeComplete().Done()
|
||||||
|
if err := sess.CloseWithError(0, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQUICDialerResolverNoPort(t *testing.T) {
|
||||||
|
tlsConfig := &tls.Config{NextProtos: []string{"h3"}}
|
||||||
|
dialer := &QUICDialerResolver{
|
||||||
|
Resolver: new(net.Resolver), Dialer: &QUICDialerQUICGo{}}
|
||||||
|
sess, err := dialer.DialContext(
|
||||||
|
context.Background(), "udp", "www.google.com",
|
||||||
|
tlsConfig, &quic.Config{})
|
||||||
|
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
|
||||||
|
t.Fatal("not the error we expected")
|
||||||
|
}
|
||||||
|
if sess != nil {
|
||||||
|
t.Fatal("expected a nil sess here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQUICDialerResolverLookupHostAddress(t *testing.T) {
|
||||||
|
dialer := &QUICDialerResolver{Resolver: &netxmocks.Resolver{
|
||||||
|
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
|
||||||
|
// We should not arrive here and call this function but if we do then
|
||||||
|
// there is going to be an error that fails this test.
|
||||||
|
return nil, errors.New("mocked error")
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
addrs, err := dialer.lookupHost(context.Background(), "1.1.1.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(addrs) != 1 || addrs[0] != "1.1.1.1" {
|
||||||
|
t.Fatal("not the result we expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQUICDialerResolverLookupHostFailure(t *testing.T) {
|
||||||
|
tlsConfig := &tls.Config{NextProtos: []string{"h3"}}
|
||||||
|
expected := errors.New("mocked error")
|
||||||
|
dialer := &QUICDialerResolver{Resolver: &netxmocks.Resolver{
|
||||||
|
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
|
||||||
|
return nil, expected
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sess, err := dialer.DialContext(
|
||||||
|
context.Background(), "udp", "dns.google.com:853",
|
||||||
|
tlsConfig, &quic.Config{})
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatal("not the error we expected")
|
||||||
|
}
|
||||||
|
if sess != nil {
|
||||||
|
t.Fatal("expected nil sess")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQUICDialerResolverInvalidPort(t *testing.T) {
|
||||||
|
// This test allows us to check for the case where every attempt
|
||||||
|
// to establish a connection leads to a failure
|
||||||
|
tlsConf := &tls.Config{NextProtos: []string{"h3"}}
|
||||||
|
dialer := &QUICDialerResolver{
|
||||||
|
Resolver: new(net.Resolver), Dialer: &QUICDialerQUICGo{
|
||||||
|
QUICListener: &QUICListenerStdlib{},
|
||||||
|
}}
|
||||||
|
sess, err := dialer.DialContext(
|
||||||
|
context.Background(), "udp", "www.google.com:0",
|
||||||
|
tlsConf, &quic.Config{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(err.Error(), "sendto: invalid argument") &&
|
||||||
|
!strings.HasSuffix(err.Error(), "sendto: can't assign requested address") {
|
||||||
|
t.Fatal("not the error we expected", err)
|
||||||
|
}
|
||||||
|
if sess != nil {
|
||||||
|
t.Fatal("expected nil sess")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user