feat: tlsmiddlebox experiment (#817)

See https://github.com/ooni/probe/issues/2124
This commit is contained in:
DecFox 2022-09-08 21:01:03 +05:30 committed by GitHub
parent b78b9aca51
commit f2b88ddb4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1670 additions and 0 deletions

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

View 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")
}
}

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

View 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")
}
})
}

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

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

View 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")
}
})
})
}

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

View 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

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

View 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")
}
})
})
})
}

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

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

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

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

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

View 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()
}

View 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()
}

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

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

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

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

View File

@ -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"`

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