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"`
|
Address string `json:"address"`
|
||||||
CipherSuite string `json:"cipher_suite"`
|
CipherSuite string `json:"cipher_suite"`
|
||||||
Failure *string `json:"failure"`
|
Failure *string `json:"failure"`
|
||||||
|
SoError *string `json:"so_error,omitempty"`
|
||||||
NegotiatedProtocol string `json:"negotiated_protocol"`
|
NegotiatedProtocol string `json:"negotiated_protocol"`
|
||||||
NoTLSVerify bool `json:"no_tls_verify"`
|
NoTLSVerify bool `json:"no_tls_verify"`
|
||||||
PeerCertificates []ArchivalMaybeBinaryData `json:"peer_certificates"`
|
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