feat: tlsmiddlebox experiment (#817)
See https://github.com/ooni/probe/issues/2124
This commit is contained in:
parent
b78b9aca51
commit
f2b88ddb4a
79
internal/engine/experiment/tlsmiddlebox/config.go
Normal file
79
internal/engine/experiment/tlsmiddlebox/config.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Config for the tlsmiddlebox experiment
|
||||
//
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config contains the experiment configuration.
|
||||
type Config struct {
|
||||
// ResolverURL is the default DoH resolver
|
||||
ResolverURL string `ooni:"URL for DoH resolver"`
|
||||
|
||||
// SNIPass is the SNI value we don't expect to be blocked
|
||||
SNIControl string `ooni:"control SNI value for testhelper"`
|
||||
|
||||
// Delay is the delay between each iteration (in milliseconds).
|
||||
Delay int64 `ooni:"delay between consecutive iterations"`
|
||||
|
||||
// MaxTTL is the default number of interations we trace
|
||||
MaxTTL int64 `ooni:"maximum TTL value to iterate upto"`
|
||||
|
||||
// TestHelper is the testhelper host for iterative tracing
|
||||
TestHelper string `ooni:"testhelper URL to use for tracing"`
|
||||
|
||||
// ClientId is the client fingerprint to use
|
||||
ClientId int `ooni:"ClientHello fingerprint to use"`
|
||||
}
|
||||
|
||||
func (c Config) resolverURL() string {
|
||||
if c.ResolverURL != "" {
|
||||
return c.ResolverURL
|
||||
}
|
||||
return "https://mozilla.cloudflare-dns.com/dns-query"
|
||||
}
|
||||
|
||||
func (c Config) snicontrol() string {
|
||||
if c.SNIControl != "" {
|
||||
return c.SNIControl
|
||||
}
|
||||
return "example.com"
|
||||
}
|
||||
|
||||
func (c Config) delay() time.Duration {
|
||||
if c.Delay > 0 {
|
||||
return time.Duration(c.Delay) * time.Millisecond
|
||||
}
|
||||
return 100 * time.Millisecond
|
||||
}
|
||||
|
||||
func (c Config) maxttl() int64 {
|
||||
if c.MaxTTL > 0 {
|
||||
return c.MaxTTL
|
||||
}
|
||||
return 20
|
||||
}
|
||||
|
||||
func (c Config) testhelper(address string) (URL *url.URL, err error) {
|
||||
// TODO(DecFox, bassosimone): We want to replace this with a generic input parser
|
||||
// Issue: https://github.com/ooni/probe/issues/2239
|
||||
if c.TestHelper != "" {
|
||||
return url.Parse(c.TestHelper)
|
||||
}
|
||||
URL = &url.URL{
|
||||
Host: address,
|
||||
Scheme: "tlshandshake",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c Config) clientid() int {
|
||||
if c.ClientId > 0 {
|
||||
return c.ClientId
|
||||
}
|
||||
return 0
|
||||
}
|
83
internal/engine/experiment/tlsmiddlebox/config_test.go
Normal file
83
internal/engine/experiment/tlsmiddlebox/config_test.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfig_maxttl(t *testing.T) {
|
||||
c := Config{}
|
||||
if c.maxttl() != 20 {
|
||||
t.Fatal("invalid default number of repetitions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_delay(t *testing.T) {
|
||||
c := Config{}
|
||||
if c.delay() != 100*time.Millisecond {
|
||||
t.Fatal("invalid default delay")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_resolver(t *testing.T) {
|
||||
c := Config{}
|
||||
if c.resolverURL() != "https://mozilla.cloudflare-dns.com/dns-query" {
|
||||
t.Fatal("invalid resolver URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_snipass(t *testing.T) {
|
||||
c := Config{}
|
||||
if c.snicontrol() != "example.com" {
|
||||
t.Fatal("invalid pass SNI")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_testhelper(t *testing.T) {
|
||||
t.Run("without config", func(t *testing.T) {
|
||||
c := Config{}
|
||||
th, err := c.testhelper("example.com")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
if th.Scheme != "tlshandshake" {
|
||||
t.Fatal("unexpected scheme")
|
||||
}
|
||||
if th.Host != "example.com" {
|
||||
t.Fatal("unexpected host")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with config", func(t *testing.T) {
|
||||
c := Config{
|
||||
TestHelper: "tlshandshake://example.com:80",
|
||||
}
|
||||
th, err := c.testhelper("google.com")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
if th.Scheme != "tlshandshake" {
|
||||
t.Fatal("unexpected scheme")
|
||||
}
|
||||
if th.Host != "example.com:80" {
|
||||
t.Fatal("unexpected host")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failure case", func(t *testing.T) {
|
||||
c := Config{
|
||||
TestHelper: "\t",
|
||||
}
|
||||
th, _ := c.testhelper("google.com")
|
||||
if th != nil {
|
||||
t.Fatal("expected nil url")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig_clientid(t *testing.T) {
|
||||
c := Config{}
|
||||
if c.clientid() != 0 {
|
||||
t.Fatal("invalid default ClientHello ID")
|
||||
}
|
||||
}
|
62
internal/engine/experiment/tlsmiddlebox/conn.go
Normal file
62
internal/engine/experiment/tlsmiddlebox/conn.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Wrapped TTL conn
|
||||
//
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
var errInvalidConnWrapper = errors.New("invalid conn wrapper")
|
||||
|
||||
// setConnTTL calls SetTTL to set the TTL for a dialerTTLWrapperConn
|
||||
func setConnTTL(conn net.Conn, ttl int) error {
|
||||
ttlWrapper, ok := conn.(*dialerTTLWrapperConn)
|
||||
if !ok {
|
||||
return errInvalidConnWrapper
|
||||
}
|
||||
return ttlWrapper.SetTTL(ttl)
|
||||
}
|
||||
|
||||
// getSoErr calls GetSoErr to fetch the SO_ERROR value
|
||||
func getSoErr(conn net.Conn) (soErr error, err error) {
|
||||
ttlWrapper, ok := conn.(*dialerTTLWrapperConn)
|
||||
if !ok {
|
||||
return nil, errInvalidConnWrapper
|
||||
}
|
||||
errno, err := ttlWrapper.GetSoErr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return syscall.Errno(errno), nil
|
||||
}
|
||||
|
||||
// dialerTTLWrapperConn wraps errors as well as allows us to set the TTL
|
||||
type dialerTTLWrapperConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
var _ net.Conn = &dialerTTLWrapperConn{}
|
||||
|
||||
// Read implements net.Conn.Read
|
||||
func (c *dialerTTLWrapperConn) Read(b []byte) (int, error) {
|
||||
count, err := c.Conn.Read(b)
|
||||
if err != nil {
|
||||
return 0, netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ReadOperation, err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Write implements net.Conn.Write
|
||||
func (c *dialerTTLWrapperConn) Write(b []byte) (int, error) {
|
||||
count, err := c.Conn.Write(b)
|
||||
if err != nil {
|
||||
return 0, netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.WriteOperation, err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
163
internal/engine/experiment/tlsmiddlebox/conn_test.go
Normal file
163
internal/engine/experiment/tlsmiddlebox/conn_test.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
func TestDialerTTLWrapperConn(t *testing.T) {
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
b := make([]byte, 128)
|
||||
conn := &dialerTTLWrapperConn{
|
||||
Conn: &mocks.Conn{
|
||||
MockRead: func(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
count, err := conn.Read(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(b) {
|
||||
t.Fatal("unexpected count")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("on failure", func(t *testing.T) {
|
||||
b := make([]byte, 128)
|
||||
expectedErr := io.EOF
|
||||
conn := &dialerTTLWrapperConn{
|
||||
Conn: &mocks.Conn{
|
||||
MockRead: func(b []byte) (int, error) {
|
||||
return 0, expectedErr
|
||||
},
|
||||
},
|
||||
}
|
||||
count, err := conn.Read(b)
|
||||
if err == nil || err.Error() != netxlite.FailureEOFError {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("unexpected count")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
b := make([]byte, 128)
|
||||
conn := &dialerTTLWrapperConn{
|
||||
Conn: &mocks.Conn{
|
||||
MockWrite: func(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
count, err := conn.Write(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(b) {
|
||||
t.Fatal("unexpected count")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("on failure", func(t *testing.T) {
|
||||
b := make([]byte, 128)
|
||||
expectedErr := io.EOF
|
||||
conn := &dialerTTLWrapperConn{
|
||||
Conn: &mocks.Conn{
|
||||
MockWrite: func(b []byte) (int, error) {
|
||||
return 0, expectedErr
|
||||
},
|
||||
},
|
||||
}
|
||||
count, err := conn.Write(b)
|
||||
if err == nil || err.Error() != netxlite.FailureEOFError {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("unexpected count")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetTTL(t *testing.T) {
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
d := NewDialerTTLWrapper()
|
||||
ctx := context.Background()
|
||||
conn, err := d.DialContext(ctx, "tcp", "1.1.1.1:80")
|
||||
if err != nil {
|
||||
t.Fatal("expected non-nil conn")
|
||||
}
|
||||
// test TTL set
|
||||
err = setConnTTL(conn, 1)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error in setting TTL", err)
|
||||
}
|
||||
var buf [512]byte
|
||||
_, err = conn.Write([]byte("1111"))
|
||||
if err != nil {
|
||||
t.Fatal("error writing", err)
|
||||
}
|
||||
r, _ := conn.Read(buf[:])
|
||||
if r != 0 {
|
||||
t.Fatal("unexpected output size", r)
|
||||
}
|
||||
setConnTTL(conn, 64) // reset TTL to ensure conn closes successfully
|
||||
conn.Close()
|
||||
_, err = conn.Read(buf[:])
|
||||
if err == nil || err.Error() != netxlite.FailureConnectionAlreadyClosed {
|
||||
t.Fatal("failed to reset TTL")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failure case", func(t *testing.T) {
|
||||
conn := &mocks.Conn{}
|
||||
err := setConnTTL(conn, 1)
|
||||
if !errors.Is(err, errInvalidConnWrapper) {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSoErr(t *testing.T) {
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
d := NewDialerTTLWrapper()
|
||||
ctx := context.Background()
|
||||
conn, err := d.DialContext(ctx, "tcp", "1.1.1.1:80")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errno, err := getSoErr(conn)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if !errors.Is(errno, syscall.Errno(0)) {
|
||||
t.Fatal("unexpected errno")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failure case", func(t *testing.T) {
|
||||
conn := &mocks.Conn{}
|
||||
errno, err := getSoErr(conn)
|
||||
if !errors.Is(err, errInvalidConnWrapper) {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
if errno != nil {
|
||||
t.Fatal("expected nil errorno")
|
||||
}
|
||||
})
|
||||
}
|
27
internal/engine/experiment/tlsmiddlebox/connect.go
Normal file
27
internal/engine/experiment/tlsmiddlebox/connect.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// TCP Connect for tlsmiddlebox
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/measurexlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// TCPConnect performs a TCP connect to filter working addresses
|
||||
func (m *Measurer) TCPConnect(ctx context.Context, index int64, zeroTime time.Time,
|
||||
logger model.Logger, address string, tk *TestKeys) error {
|
||||
trace := measurexlite.NewTrace(index, zeroTime)
|
||||
dialer := trace.NewDialerWithoutResolver(logger)
|
||||
ol := measurexlite.NewOperationLogger(logger, "TCPConnect #%d %s", index, address)
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
ol.Stop(err)
|
||||
measurexlite.MaybeClose(conn)
|
||||
tcpEvents := trace.TCPConnects()
|
||||
tk.addTCPConnect(tcpEvents)
|
||||
return err
|
||||
}
|
45
internal/engine/experiment/tlsmiddlebox/dialer.go
Normal file
45
internal/engine/experiment/tlsmiddlebox/dialer.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Custom TTL dialer
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
const timeout time.Duration = 15 * time.Second
|
||||
|
||||
func NewDialerTTLWrapper() model.Dialer {
|
||||
return &dialerTTLWrapper{
|
||||
Dialer: &net.Dialer{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
// dialerTTLWrapper wraps errors and also returns a TTL wrapped conn
|
||||
type dialerTTLWrapper struct {
|
||||
Dialer model.SimpleDialer
|
||||
}
|
||||
|
||||
var _ model.Dialer = &dialerTTLWrapper{}
|
||||
|
||||
// DialContext implements model.Dialer.DialContext
|
||||
func (d *dialerTTLWrapper) DialContext(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
conn, err := d.Dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err)
|
||||
}
|
||||
return &dialerTTLWrapperConn{
|
||||
Conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CloseIdleConnections implements model.Dialer.CloseIdleConnections
|
||||
func (d *dialerTTLWrapper) CloseIdleConnections() {
|
||||
// nothing to do here
|
||||
}
|
54
internal/engine/experiment/tlsmiddlebox/dialer_test.go
Normal file
54
internal/engine/experiment/tlsmiddlebox/dialer_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
func TestDialerWithTTL(t *testing.T) {
|
||||
t.Run("DialContext on success", func(t *testing.T) {
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
expectedConn := &mocks.Conn{}
|
||||
d := &dialerTTLWrapper{
|
||||
Dialer: &mocks.Dialer{
|
||||
MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return expectedConn, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn, err := d.DialContext(ctx, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errWrapperConn := conn.(*dialerTTLWrapperConn)
|
||||
if errWrapperConn.Conn != expectedConn {
|
||||
t.Fatal("unexpected conn")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("on failure", func(t *testing.T) {
|
||||
expectedErr := io.EOF
|
||||
d := &dialerTTLWrapper{
|
||||
Dialer: &mocks.Dialer{
|
||||
MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return nil, expectedErr
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn, err := d.DialContext(ctx, "", "")
|
||||
if err == nil || err.Error() != netxlite.FailureEOFError {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil conn")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
29
internal/engine/experiment/tlsmiddlebox/dns.go
Normal file
29
internal/engine/experiment/tlsmiddlebox/dns.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// DNS Lookup for tlsmiddlebox
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/measurexlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// DNSLookup performs a DNS Lookup for the passed domain
|
||||
func (m *Measurer) DNSLookup(ctx context.Context, index int64, zeroTime time.Time,
|
||||
logger model.Logger, domain string, tk *TestKeys) ([]string, error) {
|
||||
url := m.config.resolverURL()
|
||||
trace := measurexlite.NewTrace(index, zeroTime)
|
||||
ol := measurexlite.NewOperationLogger(logger, "DNSLookup #%d, %s, %s", index, url, domain)
|
||||
// TODO(DecFox, bassosimone): We are currently using the DoH resolver, we will
|
||||
// switch to the TRR2 resolver once we have it in measurexlite
|
||||
// Issue: https://github.com/ooni/probe/issues/2185
|
||||
resolver := trace.NewParallelDNSOverHTTPSResolver(logger, url)
|
||||
addrs, err := resolver.LookupHost(ctx, domain)
|
||||
ol.Stop(err)
|
||||
tk.addQueries(trace.DNSLookupsFromRoundTrip())
|
||||
return addrs, err
|
||||
}
|
4
internal/engine/experiment/tlsmiddlebox/doc.go
Normal file
4
internal/engine/experiment/tlsmiddlebox/doc.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
// Package tlsmiddlebox implements the tlsmiddlebox experiment
|
||||
//
|
||||
// Spec: https://github.com/ooni/spec/blob/master/nettests/ts-037-tlsmiddlebox.md.
|
||||
package tlsmiddlebox
|
115
internal/engine/experiment/tlsmiddlebox/measurer.go
Normal file
115
internal/engine/experiment/tlsmiddlebox/measurer.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Measurer
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "tlsmiddlebox"
|
||||
testVersion = "0.1.0"
|
||||
)
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName.
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
var (
|
||||
// errNoInputProvided indicates you didn't provide any input
|
||||
errNoInputProvided = errors.New("no input provided")
|
||||
|
||||
// errInputIsNotAnURL indicates that input is not an URL
|
||||
errInputIsNotAnURL = errors.New("input is not an URL")
|
||||
|
||||
// errInvalidInputScheme indicates that the input scheme is invalid
|
||||
errInvalidInputScheme = errors.New("input scheme must be tlstrace")
|
||||
|
||||
// errInvalidTestHelper indicates that the testhelper is invalid
|
||||
errInvalidTestHelper = errors.New("invalid testhelper")
|
||||
|
||||
// errInvalidTHScheme indicates that the TH scheme is invalid
|
||||
errInvalidTHScheme = errors.New("th scheme must be tlshandshake")
|
||||
)
|
||||
|
||||
// // Run implements ExperimentMeasurer.Run.
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context,
|
||||
sess model.ExperimentSession,
|
||||
measurement *model.Measurement,
|
||||
callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
if measurement.Input == "" {
|
||||
return errNoInputProvided
|
||||
}
|
||||
parsed, err := url.Parse(string(measurement.Input))
|
||||
if err != nil {
|
||||
return errInputIsNotAnURL
|
||||
}
|
||||
if parsed.Scheme != "tlstrace" {
|
||||
return errInvalidInputScheme
|
||||
}
|
||||
th, err := m.config.testhelper(parsed.Host)
|
||||
if err != nil {
|
||||
return errInvalidTestHelper
|
||||
}
|
||||
if th.Scheme != "tlshandshake" {
|
||||
return errInvalidTHScheme
|
||||
}
|
||||
tk := NewTestKeys()
|
||||
measurement.TestKeys = tk
|
||||
wg := new(sync.WaitGroup)
|
||||
// 1. perform a DNSLookup
|
||||
addrs, err := m.DNSLookup(ctx, 0, measurement.MeasurementStartTimeSaved, sess.Logger(), th.Hostname(), tk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. measure addresses
|
||||
addrs = prepareAddrs(addrs, th.Port())
|
||||
for i, addr := range addrs {
|
||||
wg.Add(1)
|
||||
go m.TraceAddress(ctx, int64(i), measurement.MeasurementStartTimeSaved, sess.Logger(), addr, parsed.Hostname(), tk, wg)
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// TraceAddress measures a single address after the DNSLookup
|
||||
func (m *Measurer) TraceAddress(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
|
||||
address string, sni string, tk *TestKeys, wg *sync.WaitGroup) error {
|
||||
defer wg.Done()
|
||||
trace := &CompleteTrace{
|
||||
Address: address,
|
||||
}
|
||||
tk.addTrace(trace)
|
||||
err := m.TCPConnect(ctx, index, zeroTime, logger, address, tk)
|
||||
if err != nil {
|
||||
return err // skip tracing if we cannot connect with default TTL
|
||||
}
|
||||
m.TLSTrace(ctx, index, zeroTime, logger, address, sni, trace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) *Measurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
246
internal/engine/experiment/tlsmiddlebox/measurer_test.go
Normal file
246
internal/engine/experiment/tlsmiddlebox/measurer_test.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
||||
)
|
||||
|
||||
func TestMeasurerExperimentNameVersion(t *testing.T) {
|
||||
measurer := NewExperimentMeasurer(Config{})
|
||||
if measurer.ExperimentName() != "tlsmiddlebox" {
|
||||
t.Fatal("unexpected ExperimentName")
|
||||
}
|
||||
if measurer.ExperimentVersion() != "0.1.0" {
|
||||
t.Fatal("unexpected ExperimentVersion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurer_input_failure(t *testing.T) {
|
||||
runHelper := func(ctx context.Context, input string, th string, sniControl string) (*model.Measurement, model.ExperimentMeasurer, error) {
|
||||
m := NewExperimentMeasurer(Config{
|
||||
TestHelper: th,
|
||||
SNIControl: sniControl,
|
||||
})
|
||||
meas := &model.Measurement{
|
||||
Input: model.MeasurementTarget(input),
|
||||
}
|
||||
sess := &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
}
|
||||
callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
|
||||
err := m.Run(ctx, sess, meas, callbacks)
|
||||
return meas, m, err
|
||||
}
|
||||
|
||||
t.Run("with empty input", func(t *testing.T) {
|
||||
_, _, err := runHelper(context.Background(), "", "", "")
|
||||
if !errors.Is(err, errNoInputProvided) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid URL", func(t *testing.T) {
|
||||
_, _, err := runHelper(context.Background(), "\t", "", "")
|
||||
if !errors.Is(err, errInputIsNotAnURL) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid scheme", func(t *testing.T) {
|
||||
_, _, err := runHelper(context.Background(), "http://8.8.8.8:443/", "", "")
|
||||
if !errors.Is(err, errInvalidInputScheme) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid testhelper", func(t *testing.T) {
|
||||
_, _, err := runHelper(context.Background(), "tlstrace://example.com", "\t", "")
|
||||
if !errors.Is(err, errInvalidTestHelper) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid TH scheme", func(t *testing.T) {
|
||||
_, _, err := runHelper(context.Background(), "tlstrace://example.com", "http://google.com", "")
|
||||
if !errors.Is(err, errInvalidTHScheme) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with local listener and successful outcome", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
URL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
URL.Scheme = "tlshandshake"
|
||||
meas, m, err := runHelper(context.Background(), "tlstrace://google.com", URL.String(), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask, err := m.GetSummaryKeys(meas)
|
||||
if err != nil {
|
||||
t.Fatal("cannot obtain summary")
|
||||
}
|
||||
summary := ask.(SummaryKeys)
|
||||
if summary.IsAnomaly {
|
||||
t.Fatal("expected no anomaly")
|
||||
}
|
||||
|
||||
t.Run("testkeys", func(t *testing.T) {
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
tr := tk.IterativeTrace
|
||||
if len(tr) != 1 {
|
||||
t.Fatal("unexpected number of trace")
|
||||
}
|
||||
trace := tr[0]
|
||||
if trace.Address != URL.Host {
|
||||
t.Fatal("unexpected trace address")
|
||||
}
|
||||
|
||||
t.Run("control trace", func(t *testing.T) {
|
||||
if trace.ControlTrace == nil || trace.ControlTrace.SNI != "example.com" {
|
||||
t.Fatal("unexpected control trace for url")
|
||||
}
|
||||
if len(trace.ControlTrace.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("target trace", func(t *testing.T) {
|
||||
if trace.TargetTrace == nil || trace.TargetTrace.SNI != "google.com" {
|
||||
t.Fatal("unexpected target trace for url")
|
||||
}
|
||||
if len(trace.TargetTrace.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with local listener and timeout", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
server := filtering.NewTLSServer(filtering.TLSActionTimeout)
|
||||
defer server.Close()
|
||||
th := "tlshandshake://" + server.Endpoint()
|
||||
URL, err := url.Parse(th)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meas, m, err := runHelper(context.Background(), "tlstrace://google.com", URL.String(), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask, err := m.GetSummaryKeys(meas)
|
||||
if err != nil {
|
||||
t.Fatal("cannot obtain summary")
|
||||
}
|
||||
summary := ask.(SummaryKeys)
|
||||
if summary.IsAnomaly {
|
||||
t.Fatal("expected no anomaly")
|
||||
}
|
||||
|
||||
t.Run("testkeys", func(t *testing.T) {
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
tr := tk.IterativeTrace
|
||||
if len(tr) != 1 {
|
||||
t.Fatal("unexpected number of trace")
|
||||
}
|
||||
trace := tr[0]
|
||||
if trace.Address != URL.Host {
|
||||
t.Fatal("unexpected trace address")
|
||||
}
|
||||
|
||||
t.Run("control trace", func(t *testing.T) {
|
||||
if trace.ControlTrace == nil || trace.ControlTrace.SNI != "example.com" {
|
||||
t.Fatal("unexpected control trace for url")
|
||||
}
|
||||
if len(trace.ControlTrace.Iterations) != 20 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("target trace", func(t *testing.T) {
|
||||
if trace.TargetTrace == nil || trace.TargetTrace.SNI != "google.com" {
|
||||
t.Fatal("unexpected target trace for url")
|
||||
}
|
||||
if len(trace.TargetTrace.Iterations) != 20 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with local listener and connect issues", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
server := filtering.NewTLSServer(filtering.TLSActionReset)
|
||||
defer server.Close()
|
||||
th := "tlshandshake://" + server.Endpoint()
|
||||
URL, err := url.Parse(th)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meas, m, err := runHelper(context.Background(), "tlstrace://google.com", URL.String(), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask, err := m.GetSummaryKeys(meas)
|
||||
if err != nil {
|
||||
t.Fatal("cannot obtain summary")
|
||||
}
|
||||
summary := ask.(SummaryKeys)
|
||||
if summary.IsAnomaly {
|
||||
t.Fatal("expected no anomaly")
|
||||
}
|
||||
|
||||
t.Run("testkeys", func(t *testing.T) {
|
||||
tk := meas.TestKeys.(*TestKeys)
|
||||
tr := tk.IterativeTrace
|
||||
if len(tr) != 1 {
|
||||
t.Fatal("unexpected number of trace")
|
||||
}
|
||||
trace := tr[0]
|
||||
if trace.Address != URL.Host {
|
||||
t.Fatal("unexpected trace address")
|
||||
}
|
||||
|
||||
t.Run("control trace", func(t *testing.T) {
|
||||
if trace.ControlTrace == nil || trace.ControlTrace.SNI != "example.com" {
|
||||
t.Fatal("unexpected control trace for url")
|
||||
}
|
||||
if len(trace.ControlTrace.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("target trace", func(t *testing.T) {
|
||||
if trace.TargetTrace == nil || trace.TargetTrace.SNI != "google.com" {
|
||||
t.Fatal("unexpected target trace for url")
|
||||
}
|
||||
if len(trace.TargetTrace.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
18
internal/engine/experiment/tlsmiddlebox/summary.go
Normal file
18
internal/engine/experiment/tlsmiddlebox/summary.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Summary
|
||||
//
|
||||
|
||||
import "github.com/ooni/probe-cli/v3/internal/model"
|
||||
|
||||
// Summary contains the summary results
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
// TODO(DecFox, bassosimone): Add anomaly logic to generate summary keys for the experiment
|
||||
return SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
29
internal/engine/experiment/tlsmiddlebox/syscall_cgo.go
Normal file
29
internal/engine/experiment/tlsmiddlebox/syscall_cgo.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
//go:build cgo && windows
|
||||
|
||||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// CGO support for SO_ERROR
|
||||
//
|
||||
|
||||
/*
|
||||
#cgo windows LDFLAGS: -lws2_32
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// getErrFromSockOpt returns the errno of the SO_ERROR
|
||||
//
|
||||
// This is the CGO_ENABLED=1 implementation of this function, which
|
||||
// returns the errno obtained from the getsockopt call
|
||||
func getErrFromSockOpt(fd uintptr) int {
|
||||
var cErrno C.int
|
||||
szInt := C.sizeof_int
|
||||
C.getsockopt((C.SOCKET)(fd), (C.int)(C.SOL_SOCKET), (C.int)(C.SO_ERROR), (*C.char)(unsafe.Pointer(&cErrno)), (*C.int)(unsafe.Pointer(&szInt)))
|
||||
return int(cErrno)
|
||||
}
|
15
internal/engine/experiment/tlsmiddlebox/syscall_otherwise.go
Normal file
15
internal/engine/experiment/tlsmiddlebox/syscall_otherwise.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
//go:build !cgo
|
||||
|
||||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Disabled CGO for SO_ERROR
|
||||
//
|
||||
|
||||
// getErrFromSockOpt returns the errno of the SO_ERROR
|
||||
//
|
||||
// This is the CGO_ENABLED=0 implementation of this function, which
|
||||
// always returns errno=0 for SO_ERROR
|
||||
func getErrFromSockOpt(fd uintptr) int {
|
||||
return 0
|
||||
}
|
53
internal/engine/experiment/tlsmiddlebox/syscall_unix.go
Normal file
53
internal/engine/experiment/tlsmiddlebox/syscall_unix.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris
|
||||
|
||||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// syscall utilities for dialerTTLWrapperConn
|
||||
//
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// SetTTL sets the IP TTL field for the underlying net.TCPConn
|
||||
func (c *dialerTTLWrapperConn) SetTTL(ttl int) error {
|
||||
conn := c.Conn
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return errInvalidConnWrapper
|
||||
}
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawErr := rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl)
|
||||
})
|
||||
// The syscall err is given a higher priority and returned early if non-nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rawErr
|
||||
}
|
||||
|
||||
// GetSoErr fetches the SO_ERROR value to look for soft ICMP errors in TCP
|
||||
func (c *dialerTTLWrapperConn) GetSoErr() (errno int, err error) {
|
||||
conn := c.Conn
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return 0, errInvalidConnWrapper
|
||||
}
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, errInvalidConnWrapper
|
||||
}
|
||||
rawErr := rawConn.Control(func(fd uintptr) {
|
||||
errno, err = syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_ERROR)
|
||||
})
|
||||
if rawErr != nil {
|
||||
return 0, rawErr
|
||||
}
|
||||
return
|
||||
}
|
54
internal/engine/experiment/tlsmiddlebox/syscall_windows.go
Normal file
54
internal/engine/experiment/tlsmiddlebox/syscall_windows.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// go:build windows
|
||||
|
||||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// syscall utilities for dialerTTLWrapperConn
|
||||
//
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// SetTTL sets the IP TTL field for the underlying net.TCPConn
|
||||
func (c *dialerTTLWrapperConn) SetTTL(ttl int) error {
|
||||
conn := c.Conn
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return errInvalidConnWrapper
|
||||
}
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawErr := rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl)
|
||||
})
|
||||
// The syscall err is given a higher priority and returned early if non-nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rawErr
|
||||
}
|
||||
|
||||
// GetSoErr fetches the SO_ERROR value at look for soft ICMP errors in TCP
|
||||
func (c *dialerTTLWrapperConn) GetSoErr() (int, error) {
|
||||
var cErrno int
|
||||
conn := c.Conn
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return 0, errInvalidConnWrapper
|
||||
}
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, errInvalidConnWrapper
|
||||
}
|
||||
rawErr := rawConn.Control(func(fd uintptr) {
|
||||
cErrno = getErrFromSockOpt(fd)
|
||||
})
|
||||
if rawErr != nil {
|
||||
return 0, rawErr
|
||||
}
|
||||
return cErrno, nil
|
||||
}
|
46
internal/engine/experiment/tlsmiddlebox/testkeys.go
Normal file
46
internal/engine/experiment/tlsmiddlebox/testkeys.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// TestKeys contains the experiment results
|
||||
type TestKeys struct {
|
||||
Queries []*model.ArchivalDNSLookupResult `json:"queries"`
|
||||
TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"`
|
||||
IterativeTrace []*CompleteTrace `json:"iterative_trace"`
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTestKeys creates new tlsmiddlebox TestKeys
|
||||
func NewTestKeys() *TestKeys {
|
||||
return &TestKeys{
|
||||
Queries: []*model.ArchivalDNSLookupResult{},
|
||||
TCPConnect: []*model.ArchivalTCPConnectResult{},
|
||||
IterativeTrace: []*CompleteTrace{},
|
||||
}
|
||||
}
|
||||
|
||||
// addqueries adds []*model.ArchivalDNSLookupResut to the test keys queries
|
||||
func (tk *TestKeys) addQueries(ev []*model.ArchivalDNSLookupResult) {
|
||||
tk.mu.Lock()
|
||||
tk.Queries = append(tk.Queries, ev...)
|
||||
tk.mu.Unlock()
|
||||
}
|
||||
|
||||
// addTCPConnect adds []*model.ArchivalTCPConnectResult to the test keys TCPConnect
|
||||
func (tk *TestKeys) addTCPConnect(ev []*model.ArchivalTCPConnectResult) {
|
||||
tk.mu.Lock()
|
||||
tk.TCPConnect = append(tk.TCPConnect, ev...)
|
||||
tk.mu.Unlock()
|
||||
}
|
||||
|
||||
// addTrace adds []*CompleteTrace to the test keys Trace
|
||||
func (tk *TestKeys) addTrace(ev ...*CompleteTrace) {
|
||||
tk.mu.Lock()
|
||||
tk.IterativeTrace = append(tk.IterativeTrace, ev...)
|
||||
tk.mu.Unlock()
|
||||
}
|
54
internal/engine/experiment/tlsmiddlebox/trace.go
Normal file
54
internal/engine/experiment/tlsmiddlebox/trace.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/tracex"
|
||||
)
|
||||
|
||||
// CompleteTrace records the result of the network trace
|
||||
// using a control SNI and a target SNI
|
||||
type CompleteTrace struct {
|
||||
Address string `json:"address"`
|
||||
ControlTrace *IterativeTrace `json:"control_trace"`
|
||||
TargetTrace *IterativeTrace `json:"target_trace"`
|
||||
}
|
||||
|
||||
// Trace is an iterative trace for the corresponding servername and address
|
||||
type IterativeTrace struct {
|
||||
SNI string `json:"server_name"`
|
||||
Iterations []*Iteration `json:"iterations"`
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Iteration is a single network iteration with variable TTL
|
||||
type Iteration struct {
|
||||
TTL int `json:"ttl"`
|
||||
Handshake *model.ArchivalTLSOrQUICHandshakeResult `json:"handshake"`
|
||||
}
|
||||
|
||||
// NewIterationFromHandshake returns a new iteration from a model.ArchivalTLSOrQUICHandshakeResult
|
||||
func newIterationFromHandshake(ttl int, err error, soErr error, handshake *model.ArchivalTLSOrQUICHandshakeResult) *Iteration {
|
||||
if err != nil {
|
||||
return &Iteration{
|
||||
TTL: ttl,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: tracex.NewFailure(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
handshake.SoError = tracex.NewFailure(soErr)
|
||||
return &Iteration{
|
||||
TTL: ttl,
|
||||
Handshake: handshake,
|
||||
}
|
||||
}
|
||||
|
||||
// addIterations adds iterations to the trace
|
||||
func (t *IterativeTrace) addIterations(ev ...*Iteration) {
|
||||
t.mu.Lock()
|
||||
t.Iterations = append(t.Iterations, ev...)
|
||||
t.mu.Unlock()
|
||||
}
|
145
internal/engine/experiment/tlsmiddlebox/tracing.go
Normal file
145
internal/engine/experiment/tlsmiddlebox/tracing.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
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
|
||||
}
|
255
internal/engine/experiment/tlsmiddlebox/tracing_test.go
Normal file
255
internal/engine/experiment/tlsmiddlebox/tracing_test.go
Normal file
|
@ -0,0 +1,255 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
||||
)
|
||||
|
||||
func TestStartIterativeTrace(t *testing.T) {
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
URL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
zeroTime := time.Now()
|
||||
ctx := context.Background()
|
||||
trace := m.startIterativeTrace(ctx, 0, zeroTime, model.DiscardLogger, URL.Host, "example.com")
|
||||
if trace.SNI != "example.com" {
|
||||
t.Fatal("unexpected servername")
|
||||
}
|
||||
if len(trace.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
for i, ev := range trace.Iterations {
|
||||
if ev.TTL != i+1 {
|
||||
t.Fatal("unexpected TTL value")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failure case", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
server := filtering.NewTLSServer(filtering.TLSActionTimeout)
|
||||
defer server.Close()
|
||||
th := "tlshandshake://" + server.Endpoint()
|
||||
URL, err := url.Parse(th)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
URL.Scheme = "tlshandshake"
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
zeroTime := time.Now()
|
||||
ctx := context.Background()
|
||||
trace := m.startIterativeTrace(ctx, 0, zeroTime, model.DiscardLogger, URL.Host, "example.com")
|
||||
if trace.SNI != "example.com" {
|
||||
t.Fatal("unexpected servername")
|
||||
}
|
||||
if len(trace.Iterations) != 20 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
for i, ev := range trace.Iterations {
|
||||
if ev.TTL != i+1 {
|
||||
t.Fatal("unexpected TTL value")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandshakeWithTTL(t *testing.T) {
|
||||
t.Run("on success", func(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer server.Close()
|
||||
URL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
tr := &IterativeTrace{}
|
||||
zeroTime := time.Now()
|
||||
ctx := context.Background()
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
m.handshakeWithTTL(ctx, 0, zeroTime, model.DiscardLogger, URL.Host, "example.com", 3, tr, wg)
|
||||
if len(tr.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
iter := tr.Iterations[0]
|
||||
if iter.TTL != 3 {
|
||||
t.Fatal("unexpected TTL value")
|
||||
}
|
||||
if iter.Handshake == nil || iter.Handshake.ServerName != "example.com" {
|
||||
t.Fatal("unexpected servername")
|
||||
}
|
||||
if iter.Handshake.Failure != nil {
|
||||
t.Fatal("unexpected error", *iter.Handshake.Failure)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("on failure", func(t *testing.T) {
|
||||
server := filtering.NewTLSServer(filtering.TLSActionReset)
|
||||
defer server.Close()
|
||||
th := "tlshandshake://" + server.Endpoint()
|
||||
URL, err := url.Parse(th)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
URL.Scheme = "tlshandshake"
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
tr := &IterativeTrace{}
|
||||
zeroTime := time.Now()
|
||||
ctx := context.Background()
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
m.handshakeWithTTL(ctx, 0, zeroTime, model.DiscardLogger, URL.Host, "example.com", 3, tr, wg)
|
||||
if len(tr.Iterations) != 1 {
|
||||
t.Fatal("unexpected number of iterations")
|
||||
}
|
||||
iter := tr.Iterations[0]
|
||||
if iter.TTL != 3 {
|
||||
t.Fatal("unexpected TTL value")
|
||||
}
|
||||
if iter.Handshake == nil || iter.Handshake.ServerName != "example.com" {
|
||||
t.Fatal("unexpected servername")
|
||||
}
|
||||
if *iter.Handshake.Failure != netxlite.FailureConnectionReset {
|
||||
t.Fatal("unexpected error", *iter.Handshake.Failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlignIterations(t *testing.T) {
|
||||
var (
|
||||
failureTimeout = "generic_timeout_err"
|
||||
failureConnectionReset = "connection_reset"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
input []*Iteration
|
||||
want []*Iteration
|
||||
}{{
|
||||
name: "with failure",
|
||||
input: []*Iteration{{
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 3,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}},
|
||||
want: []*Iteration{{
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 3,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}},
|
||||
}, {
|
||||
name: "without failure",
|
||||
input: []*Iteration{{
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: nil,
|
||||
},
|
||||
}, {
|
||||
TTL: 3,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}},
|
||||
want: []*Iteration{{
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: nil,
|
||||
},
|
||||
}},
|
||||
}, {
|
||||
name: "with connection reset",
|
||||
input: []*Iteration{{
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureConnectionReset,
|
||||
},
|
||||
}, {
|
||||
TTL: 3,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureConnectionReset,
|
||||
},
|
||||
}, {
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}},
|
||||
want: []*Iteration{{
|
||||
TTL: 1,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureTimeout,
|
||||
},
|
||||
}, {
|
||||
TTL: 2,
|
||||
Handshake: &model.ArchivalTLSOrQUICHandshakeResult{
|
||||
Failure: &failureConnectionReset,
|
||||
},
|
||||
}},
|
||||
}, {
|
||||
name: "empty input",
|
||||
input: []*Iteration{},
|
||||
want: []*Iteration{},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
out := alignIterations(tt.input)
|
||||
if diff := cmp.Diff(out, tt.want); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
}
|
25
internal/engine/experiment/tlsmiddlebox/utils.go
Normal file
25
internal/engine/experiment/tlsmiddlebox/utils.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
//
|
||||
// Utility functions for tlsmiddlebox
|
||||
//
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// prepareAddrs prepares the resolved IP addresses by
|
||||
// adding the configured port as a prefix
|
||||
func prepareAddrs(addrs []string, port string) (out []string) {
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if net.ParseIP(addr) == nil {
|
||||
continue
|
||||
}
|
||||
addr = net.JoinHostPort(addr, port)
|
||||
out = append(out, addr)
|
||||
}
|
||||
return
|
||||
}
|
46
internal/engine/experiment/tlsmiddlebox/utils_test.go
Normal file
46
internal/engine/experiment/tlsmiddlebox/utils_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package tlsmiddlebox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestPrepareAddrs(t *testing.T) {
|
||||
type arg struct {
|
||||
addrs []string
|
||||
port string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args arg
|
||||
want []string
|
||||
}{{
|
||||
name: "with valid input",
|
||||
args: arg{
|
||||
addrs: []string{"1.1.1.1", "2001:4860:4860::8844"},
|
||||
port: "",
|
||||
},
|
||||
want: []string{"1.1.1.1:443", "[2001:4860:4860::8844]:443"},
|
||||
}, {
|
||||
name: "with invalid input",
|
||||
args: arg{
|
||||
addrs: []string{"1.1.1.1.1", "2001:4860:4860::8844"},
|
||||
port: "",
|
||||
},
|
||||
want: []string{"[2001:4860:4860::8844]:443"},
|
||||
}, {
|
||||
name: "with custom port",
|
||||
args: arg{
|
||||
addrs: []string{"1.1.1.1", "2001:4860:4860::8844"},
|
||||
port: "80",
|
||||
},
|
||||
want: []string{"1.1.1.1:80", "[2001:4860:4860::8844]:80"},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
out := prepareAddrs(tt.args.addrs, tt.args.port)
|
||||
if diff := cmp.Diff(out, tt.want); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -174,6 +174,7 @@ type ArchivalTLSOrQUICHandshakeResult struct {
|
|||
Address string `json:"address"`
|
||||
CipherSuite string `json:"cipher_suite"`
|
||||
Failure *string `json:"failure"`
|
||||
SoError *string `json:"so_error,omitempty"`
|
||||
NegotiatedProtocol string `json:"negotiated_protocol"`
|
||||
NoTLSVerify bool `json:"no_tls_verify"`
|
||||
PeerCertificates []ArchivalMaybeBinaryData `json:"peer_certificates"`
|
||||
|
|
22
internal/registry/tlsmiddlebox.go
Normal file
22
internal/registry/tlsmiddlebox.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package registry
|
||||
|
||||
//
|
||||
// Registers the `tlsmiddlebox' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlsmiddlebox"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllExperiments["tlsmiddlebox"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return tlsmiddlebox.NewExperimentMeasurer(
|
||||
*config.(*tlsmiddlebox.Config),
|
||||
)
|
||||
},
|
||||
config: &tlsmiddlebox.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user