ooni-probe-cli/internal/measurex/quic.go

176 lines
4.7 KiB
Go
Raw Normal View History

package measurex
//
// QUIC
//
// Wrappers for QUIC to store events into a WritableDB.
//
import (
"context"
"crypto/tls"
"net"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/netxlite/quicx"
)
// QUICConn is the kind of conn used by QUIC.
type QUICConn = quicx.UDPLikeConn
// QUICDialer creates QUICSesssions.
type QUICDialer = netxlite.QUICDialer
// QUICListener creates listening connections for QUIC.
type QUICListener = netxlite.QUICListener
type quicListenerDB struct {
netxlite.QUICListener
begin time.Time
db WritableDB
}
func (ql *quicListenerDB) Listen(addr *net.UDPAddr) (QUICConn, error) {
pconn, err := ql.QUICListener.Listen(addr)
if err != nil {
return nil, err
}
return &udpLikeConnDB{
UDPLikeConn: pconn,
begin: ql.begin,
db: ql.db,
}, nil
}
type udpLikeConnDB struct {
quicx.UDPLikeConn
begin time.Time
db WritableDB
}
func (c *udpLikeConnDB) WriteTo(p []byte, addr net.Addr) (int, error) {
started := time.Since(c.begin).Seconds()
count, err := c.UDPLikeConn.WriteTo(p, addr)
finished := time.Since(c.begin).Seconds()
c.db.InsertIntoReadWrite(&NetworkEvent{
Operation: "write_to",
Network: "quic",
RemoteAddr: addr.String(),
Started: started,
Finished: finished,
Failure: NewArchivalFailure(err),
Count: count,
})
return count, err
}
func (c *udpLikeConnDB) ReadFrom(b []byte) (int, net.Addr, error) {
started := time.Since(c.begin).Seconds()
count, addr, err := c.UDPLikeConn.ReadFrom(b)
finished := time.Since(c.begin).Seconds()
c.db.InsertIntoReadWrite(&NetworkEvent{
Operation: "read_from",
Network: "quic",
RemoteAddr: addrStringIfNotNil(addr),
Started: started,
Finished: finished,
Failure: NewArchivalFailure(err),
Count: count,
})
return count, addr, err
}
func (c *udpLikeConnDB) Close() error {
started := time.Since(c.begin).Seconds()
err := c.UDPLikeConn.Close()
finished := time.Since(c.begin).Seconds()
c.db.InsertIntoClose(&NetworkEvent{
Operation: "close",
Network: "quic",
RemoteAddr: "",
Started: started,
Finished: finished,
Failure: NewArchivalFailure(err),
Count: 0,
})
return err
}
// QUICHandshakeEvent is the result of QUICHandshake.
type QUICHandshakeEvent = TLSHandshakeEvent
// NewQUICDialerWithoutResolver creates a new QUICDialer that is not
// attached to any resolver. This means that every attempt to dial any
// address containing a domain name will fail. This QUICDialer will
// save any event into the WritableDB. Any QUICConn created by it will
// likewise save any event into the WritableDB.
func (mx *Measurer) NewQUICDialerWithoutResolver(db WritableDB, logger Logger) QUICDialer {
return &quicDialerDB{db: db, logger: logger, begin: mx.Begin}
}
type quicDialerDB struct {
netxlite.QUICDialer
begin time.Time
db WritableDB
logger Logger
}
func (qh *quicDialerDB) DialContext(ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
started := time.Since(qh.begin).Seconds()
var state tls.ConnectionState
listener := &quicListenerDB{
QUICListener: netxlite.NewQUICListener(),
begin: qh.begin,
db: qh.db,
}
dialer := netxlite.NewQUICDialerWithoutResolver(listener, qh.logger)
defer dialer.CloseIdleConnections()
sess, err := dialer.DialContext(ctx, network, address, tlsConfig, quicConfig)
if err == nil {
select {
case <-sess.HandshakeComplete().Done():
state = sess.ConnectionState().TLS.ConnectionState
case <-ctx.Done():
sess, err = nil, ctx.Err()
}
}
finished := time.Since(qh.begin).Seconds()
qh.db.InsertIntoQUICHandshake(&QUICHandshakeEvent{
Network: "quic",
RemoteAddr: address,
SNI: tlsConfig.ServerName,
ALPN: tlsConfig.NextProtos,
SkipVerify: tlsConfig.InsecureSkipVerify,
Started: started,
Finished: finished,
Failure: NewArchivalFailure(err),
Oddity: qh.computeOddity(err),
TLSVersion: netxlite.TLSVersionString(state.Version),
CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite),
NegotiatedProto: state.NegotiatedProtocol,
PeerCerts: NewArchivalTLSCerts(peerCerts(nil, &state)),
})
return sess, err
}
func (qh *quicDialerDB) computeOddity(err error) Oddity {
if err == nil {
return ""
}
switch err.Error() {
case netxlite.FailureGenericTimeoutError:
return OddityQUICHandshakeTimeout
case netxlite.FailureHostUnreachable:
return OddityQUICHandshakeHostUnreachable
default:
return OddityQUICHandshakeOther
}
}
func (qh *quicDialerDB) CloseIdleConnections() {
// nothing to do
}