399d2f65da
This commit introduce a measurement library that consists of refactored code from earlier websteps experiments. I am not going to add tests for the time being, because this library is still a bit in flux, as we finalize websteps. I will soon though commit documentation explaining in detail how to use it, which currrently is at https://github.com/ooni/probe-cli/pull/506 and adds a new directory to internal/tutorial. The core idea of this measurement library is to allow two measurement modes: 1. tracing, which is what we're currently doing now, and the tutorial shows how we can rewrite the measurement part of web connectivity with measurex using less code. Under a tracing approach, we construct a normal http.Client that however has tracing configured, we gather events for resolve, connect, TLS handshake, QUIC handshake, HTTP round trip, etc. and then we try to make sense of what happened from the events stream; 2. step-by-step, which is what websteps does, and basically means that after each operation you immediately write into a Measurement structure its results and immediately draw the conclusions on what seems odd (which later may become an anomaly if we see what the test helper measured). This library is also such that it produces a data format compatible with the current OONI spec. This work is part of https://github.com/ooni/probe/issues/1733.
176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
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
|
|
}
|