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
|
||
|
}
|