diff --git a/internal/engine/netx/netx.go b/internal/engine/netx/netx.go index 2352066..a4db396 100644 --- a/internal/engine/netx/netx.go +++ b/internal/engine/netx/netx.go @@ -162,6 +162,7 @@ func NewQUICDialer(config Config) QUICDialer { config.FullResolver = NewResolver(config) } var ql quicdialer.QUICListener = &netxlite.QUICListenerStdlib{} + ql = &errorsx.ErrorWrapperQUICListener{QUICListener: ql} if config.ReadWriteSaver != nil { ql = &quicdialer.QUICListenerSaver{ QUICListener: ql, diff --git a/internal/errorsx/errorsx.go b/internal/errorsx/errorsx.go index 7d82a6c..df8d383 100644 --- a/internal/errorsx/errorsx.go +++ b/internal/errorsx/errorsx.go @@ -108,6 +108,9 @@ const ( // QUICHandshakeOperation is the handshake to setup a QUIC connection QUICHandshakeOperation = "quic_handshake" + // QUICListenOperation is when we open a listening UDP conn for QUIC + QUICListenOperation = "quic_listen" + // HTTPRoundTripOperation is the HTTP round trip HTTPRoundTripOperation = "http_round_trip" diff --git a/internal/errorsx/quic.go b/internal/errorsx/quic.go index 11a9f1f..b4f4ca3 100644 --- a/internal/errorsx/quic.go +++ b/internal/errorsx/quic.go @@ -3,8 +3,10 @@ package errorsx import ( "context" "crypto/tls" + "net" "github.com/lucas-clemente/quic-go" + "github.com/ooni/probe-cli/v3/internal/quicx" ) // QUICContextDialer is a dialer for QUIC using Context. @@ -16,6 +18,64 @@ type QUICContextDialer interface { tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) } +// QUICListener listens for QUIC connections. +type QUICListener interface { + // Listen creates a new listening UDPConn. + Listen(addr *net.UDPAddr) (quicx.UDPConn, error) +} + +// ErrorWrapperQUICListener is a QUICListener that wraps errors. +type ErrorWrapperQUICListener struct { + // QUICListener is the underlying listener. + QUICListener QUICListener +} + +var _ QUICListener = &ErrorWrapperQUICListener{} + +// Listen implements QUICListener.Listen. +func (qls *ErrorWrapperQUICListener) Listen(addr *net.UDPAddr) (quicx.UDPConn, error) { + pconn, err := qls.QUICListener.Listen(addr) + if err != nil { + return nil, SafeErrWrapperBuilder{ + Error: err, + Operation: QUICListenOperation, + }.MaybeBuild() + } + return &errorWrapperUDPConn{pconn}, nil +} + +// errorWrapperUDPConn is a quicx.UDPConn that wraps errors. +type errorWrapperUDPConn struct { + // UDPConn is the underlying conn. + quicx.UDPConn +} + +var _ quicx.UDPConn = &errorWrapperUDPConn{} + +// WriteTo implements quicx.UDPConn.WriteTo. +func (c *errorWrapperUDPConn) WriteTo(p []byte, addr net.Addr) (int, error) { + count, err := c.UDPConn.WriteTo(p, addr) + if err != nil { + return 0, SafeErrWrapperBuilder{ + Error: err, + Operation: WriteToOperation, + }.MaybeBuild() + } + return count, nil +} + +// ReadMsgUDP implements quicx.UDPConn.ReadMsgUDP. +func (c *errorWrapperUDPConn) ReadMsgUDP(b, oob []byte) (int, int, int, *net.UDPAddr, error) { + n, oobn, flags, addr, err := c.UDPConn.ReadMsgUDP(b, oob) + if err != nil { + return 0, 0, 0, nil, SafeErrWrapperBuilder{ + Error: err, + Operation: ReadFromOperation, + }.MaybeBuild() + } + return n, oobn, flags, addr, nil +} + // ErrorWrapperQUICDialer is a dialer that performs quic err wrapping type ErrorWrapperQUICDialer struct { Dialer QUICContextDialer diff --git a/internal/errorsx/quic_test.go b/internal/errorsx/quic_test.go index da6c8f0..c3add76 100644 --- a/internal/errorsx/quic_test.go +++ b/internal/errorsx/quic_test.go @@ -5,12 +5,142 @@ import ( "crypto/tls" "errors" "io" + "net" "testing" "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/netxmocks" + "github.com/ooni/probe-cli/v3/internal/quicx" ) +func TestErrorWrapperQUICListenerSuccess(t *testing.T) { + ql := &ErrorWrapperQUICListener{ + QUICListener: &netxmocks.QUICListener{ + MockListen: func(addr *net.UDPAddr) (quicx.UDPConn, error) { + return &net.UDPConn{}, nil + }, + }, + } + pconn, err := ql.Listen(&net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + pconn.Close() +} + +func TestErrorWrapperQUICListenerFailure(t *testing.T) { + ql := &ErrorWrapperQUICListener{ + QUICListener: &netxmocks.QUICListener{ + MockListen: func(addr *net.UDPAddr) (quicx.UDPConn, error) { + return nil, io.EOF + }, + }, + } + pconn, err := ql.Listen(&net.UDPAddr{}) + if err.Error() != "eof_error" { + t.Fatal("not the error we expected", err) + } + if pconn != nil { + t.Fatal("expected nil pconn here") + } +} + +func TestErrorWrapperUDPConnWriteToSuccess(t *testing.T) { + quc := &errorWrapperUDPConn{ + UDPConn: &netxmocks.QUICUDPConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + return 10, nil + }, + }, + } + pkt := make([]byte, 128) + addr := &net.UDPAddr{} + cnt, err := quc.WriteTo(pkt, addr) + if err != nil { + t.Fatal("not the error we expected", err) + } + if cnt != 10 { + t.Fatal("expected 10 here") + } +} + +func TestErrorWrapperUDPConnWriteToFailure(t *testing.T) { + expected := errors.New("mocked error") + quc := &errorWrapperUDPConn{ + UDPConn: &netxmocks.QUICUDPConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + return 0, expected + }, + }, + } + pkt := make([]byte, 128) + addr := &net.UDPAddr{} + cnt, err := quc.WriteTo(pkt, addr) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if cnt != 0 { + t.Fatal("expected 0 here") + } +} + +func TestErrorWrapperUDPConnReadMsgUDPSuccess(t *testing.T) { + expected := errors.New("mocked error") + quc := &errorWrapperUDPConn{ + UDPConn: &netxmocks.QUICUDPConn{ + MockReadMsgUDP: func(b, oob []byte) (int, int, int, *net.UDPAddr, error) { + return 0, 0, 0, nil, expected + }, + }, + } + b := make([]byte, 128) + oob := make([]byte, 128) + n, oobn, flags, addr, err := quc.ReadMsgUDP(b, oob) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if n != 0 { + t.Fatal("expected 0 here") + } + if oobn != 0 { + t.Fatal("expected 0 here") + } + if flags != 0 { + t.Fatal("expected 0 here") + } + if addr != nil { + t.Fatal("expected nil here") + } +} + +func TestErrorWrapperUDPConnReadMsgUDPFailure(t *testing.T) { + quc := &errorWrapperUDPConn{ + UDPConn: &netxmocks.QUICUDPConn{ + MockReadMsgUDP: func(b, oob []byte) (int, int, int, *net.UDPAddr, error) { + return 10, 1, 0, nil, nil + }, + }, + } + b := make([]byte, 128) + oob := make([]byte, 128) + n, oobn, flags, addr, err := quc.ReadMsgUDP(b, oob) + if err != nil { + t.Fatal("not the error we expected", err) + } + if n != 10 { + t.Fatal("expected 10 here") + } + if oobn != 1 { + t.Fatal("expected 1 here") + } + if flags != 0 { + t.Fatal("expected 0 here") + } + if addr != nil { + t.Fatal("expected nil here") + } +} + func TestErrorWrapperQUICDialerFailure(t *testing.T) { ctx := context.Background() d := &ErrorWrapperQUICDialer{Dialer: &netxmocks.QUICContextDialer{ diff --git a/internal/netxmocks/conn.go b/internal/netxmocks/conn.go index a8c62af..ff938f8 100644 --- a/internal/netxmocks/conn.go +++ b/internal/netxmocks/conn.go @@ -32,7 +32,7 @@ func (c *Conn) Close() error { return c.MockClose() } -// LocalAddr class MockLocalAddr. +// LocalAddr calls MockLocalAddr. func (c *Conn) LocalAddr() net.Addr { return c.MockLocalAddr() } diff --git a/internal/netxmocks/quic.go b/internal/netxmocks/quic.go index 10bc935..0e60213 100644 --- a/internal/netxmocks/quic.go +++ b/internal/netxmocks/quic.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net" + "time" "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/quicx" @@ -127,3 +128,63 @@ func (s *QUICEarlySession) SendMessage(b []byte) error { func (s *QUICEarlySession) ReceiveMessage() ([]byte, error) { return s.MockReceiveMessage() } + +// QUICUDPConn is an UDP conn used by QUIC. +type QUICUDPConn struct { + MockWriteTo func(p []byte, addr net.Addr) (int, error) + MockReadMsgUDP func(b, oob []byte) (int, int, int, *net.UDPAddr, error) + MockClose func() error + MockLocalAddr func() net.Addr + MockRemoteAddr func() net.Addr + MockSetDeadline func(t time.Time) error + MockSetReadDeadline func(t time.Time) error + MockSetWriteDeadline func(t time.Time) error + MockReadFrom func(p []byte) (n int, addr net.Addr, err error) +} + +var _ net.PacketConn = &QUICUDPConn{} + +// WriteTo calls MockWriteTo. +func (c *QUICUDPConn) WriteTo(p []byte, addr net.Addr) (int, error) { + return c.MockWriteTo(p, addr) +} + +// ReadMsgUDP calls MockReadMsgUDP. +func (c *QUICUDPConn) ReadMsgUDP(b, oob []byte) (int, int, int, *net.UDPAddr, error) { + return c.MockReadMsgUDP(b, oob) +} + +// Close calls MockClose. +func (c *QUICUDPConn) Close() error { + return c.MockClose() +} + +// LocalAddr calls MockLocalAddr. +func (c *QUICUDPConn) LocalAddr() net.Addr { + return c.MockLocalAddr() +} + +// RemoteAddr calls MockRemoteAddr. +func (c *QUICUDPConn) RemoteAddr() net.Addr { + return c.MockRemoteAddr() +} + +// SetDeadline calls MockSetDeadline. +func (c *QUICUDPConn) SetDeadline(t time.Time) error { + return c.MockSetDeadline(t) +} + +// SetReadDeadline calls MockSetReadDeadline. +func (c *QUICUDPConn) SetReadDeadline(t time.Time) error { + return c.MockSetReadDeadline(t) +} + +// SetWriteDeadline calls MockSetWriteDeadline. +func (c *QUICUDPConn) SetWriteDeadline(t time.Time) error { + return c.MockSetWriteDeadline(t) +} + +// ReadFrom calls MockReadFrom. +func (c *QUICUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { + return c.MockReadFrom(b) +} diff --git a/internal/netxmocks/quic_test.go b/internal/netxmocks/quic_test.go index dac90e2..c9103ee 100644 --- a/internal/netxmocks/quic_test.go +++ b/internal/netxmocks/quic_test.go @@ -7,7 +7,9 @@ import ( "net" "reflect" "testing" + "time" + "github.com/google/go-cmp/cmp" "github.com/lucas-clemente/quic-go" "github.com/ooni/probe-cli/v3/internal/quicx" ) @@ -266,3 +268,152 @@ func TestQUICEarlySessionReceiveMessage(t *testing.T) { t.Fatal("expected nil buffer here") } } + +func TestQUICUDPConnWriteTo(t *testing.T) { + expected := errors.New("mocked error") + quc := &QUICUDPConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + return 0, expected + }, + } + pkt := make([]byte, 128) + addr := &net.UDPAddr{} + cnt, err := quc.WriteTo(pkt, addr) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if cnt != 0 { + t.Fatal("expected zero here") + } +} + +func TestQUICUDPConnReadMsgUDP(t *testing.T) { + expected := errors.New("mocked error") + quc := &QUICUDPConn{ + MockReadMsgUDP: func(b, oob []byte) (int, int, int, *net.UDPAddr, error) { + return 0, 0, 0, nil, expected + }, + } + b := make([]byte, 128) + oob := make([]byte, 128) + n, oobn, flags, addr, err := quc.ReadMsgUDP(b, oob) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if n != 0 { + t.Fatal("expected zero here") + } + if oobn != 0 { + t.Fatal("expected zero here") + } + if flags != 0 { + t.Fatal("expected zero here") + } + if addr != nil { + t.Fatal("expected nil here") + } +} + +func TestQUICUDPConnClose(t *testing.T) { + expected := errors.New("mocked error") + quc := &QUICUDPConn{ + MockClose: func() error { + return expected + }, + } + err := quc.Close() + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } +} + +func TestQUICUDPConnLocalAddrWorks(t *testing.T) { + expected := &net.TCPAddr{ + IP: net.IPv6loopback, + Port: 1234, + } + c := &QUICUDPConn{ + MockLocalAddr: func() net.Addr { + return expected + }, + } + out := c.LocalAddr() + if diff := cmp.Diff(expected, out); diff != "" { + t.Fatal(diff) + } +} + +func TestQUICUDPConnRemoteAddrWorks(t *testing.T) { + expected := &net.TCPAddr{ + IP: net.IPv6loopback, + Port: 1234, + } + c := &QUICUDPConn{ + MockRemoteAddr: func() net.Addr { + return expected + }, + } + out := c.RemoteAddr() + if diff := cmp.Diff(expected, out); diff != "" { + t.Fatal(diff) + } +} + +func TestQUICUDPConnSetDeadline(t *testing.T) { + expected := errors.New("mocked error") + c := &QUICUDPConn{ + MockSetDeadline: func(t time.Time) error { + return expected + }, + } + err := c.SetDeadline(time.Time{}) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } +} + +func TestQUICUDPConnSetReadDeadline(t *testing.T) { + expected := errors.New("mocked error") + c := &QUICUDPConn{ + MockSetReadDeadline: func(t time.Time) error { + return expected + }, + } + err := c.SetReadDeadline(time.Time{}) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } +} + +func TestQUICUDPConnSetWriteDeadline(t *testing.T) { + expected := errors.New("mocked error") + c := &QUICUDPConn{ + MockSetWriteDeadline: func(t time.Time) error { + return expected + }, + } + err := c.SetWriteDeadline(time.Time{}) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } +} + +func TestQUICUDPConnReadFrom(t *testing.T) { + expected := errors.New("mocked error") + quc := &QUICUDPConn{ + MockReadFrom: func(b []byte) (int, net.Addr, error) { + return 0, nil, expected + }, + } + b := make([]byte, 128) + n, addr, err := quc.ReadFrom(b) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if n != 0 { + t.Fatal("expected zero here") + } + if addr != nil { + t.Fatal("expected nil here") + } +}