ooni-probe-cli/internal/engine/experiment/tlsmiddlebox/tracing.go

146 lines
4.8 KiB
Go
Raw Normal View History

package tlsmiddlebox
//
// Iterative network tracing
//
import (
"context"
"crypto/tls"
"errors"
"net"
"sort"
"sync"
"syscall"
"time"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
utls "gitlab.com/yawning/utls.git"
)
// ClientIDs to map configurable inputs to uTLS fingerprints
// We use a non-zero index to map to each ClientID
var ClientIDs = map[int]*utls.ClientHelloID{
1: &utls.HelloGolang,
2: &utls.HelloChrome_Auto,
3: &utls.HelloFirefox_Auto,
4: &utls.HelloIOS_Auto,
}
// TLSTrace performs tracing using control and target SNI
func (m *Measurer) TLSTrace(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
address string, targetSNI string, trace *CompleteTrace) {
// perform an iterative trace with the control SNI
trace.ControlTrace = m.startIterativeTrace(ctx, index, zeroTime, logger, address, m.config.snicontrol())
// perform an iterative trace with the target SNI
trace.TargetTrace = m.startIterativeTrace(ctx, index, zeroTime, logger, address, targetSNI)
}
// startIterativeTrace creates a Trace and calls iterativeTrace
func (m *Measurer) startIterativeTrace(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
address string, sni string) (tr *IterativeTrace) {
tr = &IterativeTrace{
SNI: sni,
Iterations: []*Iteration{},
}
maxTTL := m.config.maxttl()
m.traceWithIncreasingTTLs(ctx, index, zeroTime, logger, address, sni, maxTTL, tr)
tr.Iterations = alignIterations(tr.Iterations)
return
}
// traceWithIncreasingTTLs performs iterative tracing with increasing TTL values
func (m *Measurer) traceWithIncreasingTTLs(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
address string, sni string, maxTTL int64, trace *IterativeTrace) {
ticker := time.NewTicker(m.config.delay())
wg := new(sync.WaitGroup)
for i := int64(1); i <= maxTTL; i++ {
wg.Add(1)
go m.handshakeWithTTL(ctx, index, zeroTime, logger, address, sni, int(i), trace, wg)
<-ticker.C
}
wg.Wait()
}
// handshakeWithTTL performs the TLS Handshake using the passed ttl value
func (m *Measurer) handshakeWithTTL(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
address string, sni string, ttl int, tr *IterativeTrace, wg *sync.WaitGroup) {
defer wg.Done()
trace := measurexlite.NewTrace(index, zeroTime)
// 1. Connect to the target IP
// TODO(DecFox, bassosimone): Do we need a trace for this TCP connect?
d := NewDialerTTLWrapper()
ol := measurexlite.NewOperationLogger(logger, "Handshake Trace #%d TTL %d %s %s", index, ttl, address, sni)
conn, err := d.DialContext(ctx, "tcp", address)
if err != nil {
iteration := newIterationFromHandshake(ttl, err, nil, nil)
tr.addIterations(iteration)
ol.Stop(err)
return
}
defer conn.Close()
// 2. Set the TTL to the passed value
err = setConnTTL(conn, ttl)
if err != nil {
iteration := newIterationFromHandshake(ttl, err, nil, nil)
tr.addIterations(iteration)
ol.Stop(err)
return
}
// 3. Perform the handshake and extract the SO_ERROR value (if any)
// Note: we switch to a uTLS Handshaker if the configured ClientID is non-zero
thx := trace.NewTLSHandshakerStdlib(logger)
clientId := m.config.clientid()
if clientId > 0 {
thx = trace.NewTLSHandshakerUTLS(logger, ClientIDs[clientId])
}
_, _, err = thx.Handshake(ctx, conn, genTLSConfig(sni))
ol.Stop(err)
soErr := extractSoError(conn)
// 4. reset the TTL value to ensure that conn closes successfully
// Note: Do not check for errors here
_ = setConnTTL(conn, 64)
iteration := newIterationFromHandshake(ttl, nil, soErr, trace.FirstTLSHandshakeOrNil())
tr.addIterations(iteration)
}
// extractSoError fetches the SO_ERROR value and returns a non-nil error if
// it qualifies as a valid ICMP soft error
// Note: The passed conn must be of type dialerTTLWrapperConn
func extractSoError(conn net.Conn) error {
soErrno, err := getSoErr(conn)
if err != nil || errors.Is(soErrno, syscall.Errno(0)) {
return nil
}
soErr := netxlite.MaybeNewErrWrapper(netxlite.ClassifyGenericError, netxlite.TLSHandshakeOperation, soErrno)
return soErr
}
// genTLSConfig generates tls.Config from a given SNI
func genTLSConfig(sni string) *tls.Config {
return &tls.Config{
RootCAs: netxlite.NewDefaultCertPool(),
ServerName: sni,
NextProtos: []string{"h2", "http/1.1"},
InsecureSkipVerify: true,
}
}
// alignIterEvents sorts the iterEvents according to increasing TTL
// and stops when we receive a nil or connection_reset
func alignIterations(in []*Iteration) (out []*Iteration) {
out = []*Iteration{}
sort.Slice(in, func(i int, j int) bool {
return in[i].TTL < in[j].TTL
})
for _, iter := range in {
out = append(out, iter)
if iter.Handshake.Failure == nil || *iter.Handshake.Failure == netxlite.FailureConnectionReset {
break
}
}
return out
}