diff --git a/internal/enginenetx/beacons_test.go b/internal/enginenetx/beacons_test.go index 404b44cbf0..fadd041a55 100644 --- a/internal/enginenetx/beacons_test.go +++ b/internal/enginenetx/beacons_test.go @@ -13,7 +13,7 @@ func TestBeaconsPolicy(t *testing.T) { t.Run("for domains for which we don't have beacons and DNS failure", func(t *testing.T) { expected := errors.New("mocked error") policy := &beaconsPolicy{ - Fallback: &HTTPSDialerNullPolicy{ + Fallback: &dnsPolicy{ Logger: model.DiscardLogger, Resolver: &mocks.Resolver{ MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { @@ -38,7 +38,7 @@ func TestBeaconsPolicy(t *testing.T) { t.Run("for domains for which we don't have beacons and DNS success", func(t *testing.T) { policy := &beaconsPolicy{ - Fallback: &HTTPSDialerNullPolicy{ + Fallback: &dnsPolicy{ Logger: model.DiscardLogger, Resolver: &mocks.Resolver{ MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { @@ -79,7 +79,7 @@ func TestBeaconsPolicy(t *testing.T) { t.Run("for the api.ooni.io domain", func(t *testing.T) { expected := errors.New("mocked error") policy := &beaconsPolicy{ - Fallback: &HTTPSDialerNullPolicy{ + Fallback: &dnsPolicy{ Logger: model.DiscardLogger, Resolver: &mocks.Resolver{ MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { diff --git a/internal/enginenetx/httpsdialernull.go b/internal/enginenetx/dnspolicy.go similarity index 52% rename from internal/enginenetx/httpsdialernull.go rename to internal/enginenetx/dnspolicy.go index e47435cf5d..150820430d 100644 --- a/internal/enginenetx/httpsdialernull.go +++ b/internal/enginenetx/dnspolicy.go @@ -1,5 +1,10 @@ package enginenetx +// +// HTTPS dialing policy where we generate tactics in the usual way +// by using a DNS resolver and using SNI == VerifyHostname +// + import ( "context" @@ -7,17 +12,13 @@ import ( "github.com/ooni/probe-cli/v3/internal/netxlite" ) -// HTTPSDialerNullPolicy is the default "null" policy where we use the +// dnsPolicy is the default TLS dialing policy where we use the // given resolver and the domain as the SNI. // // The zero value is invalid; please, init all MANDATORY fields. // -// We say that this is the "null" policy because this is what you would get -// by default if you were not using any policy. -// -// This policy uses an Happy-Eyeballs-like algorithm. Dial attempts are -// staggered by httpsDialerHappyEyeballsDelay. -type HTTPSDialerNullPolicy struct { +// This policy uses an Happy-Eyeballs-like algorithm. +type dnsPolicy struct { // Logger is the MANDATORY logger. Logger model.Logger @@ -25,10 +26,10 @@ type HTTPSDialerNullPolicy struct { Resolver model.Resolver } -var _ HTTPSDialerPolicy = &HTTPSDialerNullPolicy{} +var _ HTTPSDialerPolicy = &dnsPolicy{} // LookupTactics implements HTTPSDialerPolicy. -func (p *HTTPSDialerNullPolicy) LookupTactics( +func (p *dnsPolicy) LookupTactics( ctx context.Context, domain, port string) <-chan *HTTPSDialerTactic { out := make(chan *HTTPSDialerTactic) @@ -67,33 +68,3 @@ func (p *HTTPSDialerNullPolicy) LookupTactics( return out } - -// HTTPSDialerNullStatsTracker is the "null" [HTTPSDialerStatsTracker]. -type HTTPSDialerNullStatsTracker struct{} - -var _ HTTPSDialerStatsTracker = &HTTPSDialerNullStatsTracker{} - -// OnStarting implements HTTPSDialerStatsTracker. -func (*HTTPSDialerNullStatsTracker) OnStarting(tactic *HTTPSDialerTactic) { - // nothing -} - -// OnSuccess implements HTTPSDialerStatsTracker. -func (*HTTPSDialerNullStatsTracker) OnSuccess(tactic *HTTPSDialerTactic) { - // nothing -} - -// OnTCPConnectError implements HTTPSDialerStatsTracker. -func (*HTTPSDialerNullStatsTracker) OnTCPConnectError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { - // nothing -} - -// OnTLSHandshakeError implements HTTPSDialerStatsTracker. -func (*HTTPSDialerNullStatsTracker) OnTLSHandshakeError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { - // nothing -} - -// OnTLSVerifyError implements HTTPSDialerStatsTracker. -func (*HTTPSDialerNullStatsTracker) OnTLSVerifyError(tactic *HTTPSDialerTactic, err error) { - // nothing -} diff --git a/internal/enginenetx/httpsdialernull_test.go b/internal/enginenetx/dnspolicy_test.go similarity index 95% rename from internal/enginenetx/httpsdialernull_test.go rename to internal/enginenetx/dnspolicy_test.go index ed9d4571f4..f6609fa097 100644 --- a/internal/enginenetx/httpsdialernull_test.go +++ b/internal/enginenetx/dnspolicy_test.go @@ -12,7 +12,7 @@ func TestHTTPSDialerNullPolicy(t *testing.T) { t.Run("LookupTactics with canceled context", func(t *testing.T) { var called int - policy := &HTTPSDialerNullPolicy{ + policy := &dnsPolicy{ Logger: &mocks.Logger{ MockDebugf: func(format string, v ...interface{}) { called++ @@ -40,7 +40,7 @@ func TestHTTPSDialerNullPolicy(t *testing.T) { }) t.Run("we short circuit IP addresses", func(t *testing.T) { - policy := &HTTPSDialerNullPolicy{ + policy := &dnsPolicy{ Logger: model.DiscardLogger, Resolver: &mocks.Resolver{}, // empty so we crash if we hit the resolver } diff --git a/internal/enginenetx/httpsdialercore.go b/internal/enginenetx/httpsdialer.go similarity index 100% rename from internal/enginenetx/httpsdialercore.go rename to internal/enginenetx/httpsdialer.go diff --git a/internal/enginenetx/httpsdialer_internal_test.go b/internal/enginenetx/httpsdialer_internal_test.go deleted file mode 100644 index e38742fc4e..0000000000 --- a/internal/enginenetx/httpsdialer_internal_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package enginenetx - -import ( - "crypto/tls" - "errors" - "testing" - - "github.com/ooni/probe-cli/v3/internal/mocks" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -func TestHTTPSDialerVerifyCertificateChain(t *testing.T) { - t.Run("without any peer certificate", func(t *testing.T) { - tlsConn := &mocks.TLSConn{ - MockConnectionState: func() tls.ConnectionState { - return tls.ConnectionState{} // empty! - }, - } - certPool := netxlite.NewMozillaCertPool() - err := httpsDialerVerifyCertificateChain("www.example.com", tlsConn, certPool) - if !errors.Is(err, errNoPeerCertificate) { - t.Fatal("unexpected error", err) - } - }) - - t.Run("with an empty hostname", func(t *testing.T) { - tlsConn := &mocks.TLSConn{ - MockConnectionState: func() tls.ConnectionState { - return tls.ConnectionState{} // empty but should not be an issue - }, - } - certPool := netxlite.NewMozillaCertPool() - err := httpsDialerVerifyCertificateChain("", tlsConn, certPool) - if !errors.Is(err, errEmptyVerifyHostname) { - t.Fatal("unexpected error", err) - } - }) -} - -func TestHTTPSDialerReduceResult(t *testing.T) { - t.Run("we return the first conn in a list of conns and close the other conns", func(t *testing.T) { - var closed int - expect := &mocks.TLSConn{} // empty - connv := []model.TLSConn{ - expect, - &mocks.TLSConn{ - Conn: mocks.Conn{ - MockClose: func() error { - closed++ - return nil - }, - }, - }, - &mocks.TLSConn{ - Conn: mocks.Conn{ - MockClose: func() error { - closed++ - return nil - }, - }, - }, - } - - conn, err := httpsDialerReduceResult(connv, nil) - if err != nil { - t.Fatal(err) - } - - if conn != expect { - t.Fatal("unexpected conn") - } - - if closed != 2 { - t.Fatal("did not call close") - } - }) - - t.Run("we join together a list of errors", func(t *testing.T) { - expectErr := "connection_refused\ninterrupted" - errorv := []error{errors.New("connection_refused"), errors.New("interrupted")} - - conn, err := httpsDialerReduceResult(nil, errorv) - if err == nil || err.Error() != expectErr { - t.Fatal("unexpected err", err) - } - - if conn != nil { - t.Fatal("expected nil conn") - } - }) - - t.Run("with a single error we return such an error", func(t *testing.T) { - expected := errors.New("connection_refused") - errorv := []error{expected} - - conn, err := httpsDialerReduceResult(nil, errorv) - if !errors.Is(err, expected) { - t.Fatal("unexpected err", err) - } - - if conn != nil { - t.Fatal("expected nil conn") - } - }) - - t.Run("we return errDNSNoAnswer if we don't have any conns or errors to return", func(t *testing.T) { - conn, err := httpsDialerReduceResult(nil, nil) - if !errors.Is(err, errDNSNoAnswer) { - t.Fatal("unexpected error", err) - } - - if conn != nil { - t.Fatal("expected nil conn") - } - }) -} diff --git a/internal/enginenetx/httpsdialer_test.go b/internal/enginenetx/httpsdialer_test.go index 0e8539d45a..ca3f39797e 100644 --- a/internal/enginenetx/httpsdialer_test.go +++ b/internal/enginenetx/httpsdialer_test.go @@ -1,8 +1,10 @@ -package enginenetx_test +package enginenetx import ( "context" + "crypto/tls" "crypto/x509" + "errors" "net/url" "testing" "time" @@ -10,8 +12,8 @@ import ( "github.com/apex/log" "github.com/google/go-cmp/cmp" "github.com/ooni/netem" - "github.com/ooni/probe-cli/v3/internal/enginenetx" "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netemx" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/runtimex" @@ -24,42 +26,42 @@ const ( httpsDialerCancelingContextStatsTrackerOnSuccess ) -// httpsDialerCancelingContextStatsTracker is an [enginenetx.HTTPSDialerStatsTracker] with a cancel +// httpsDialerCancelingContextStatsTracker is an [HTTPSDialerStatsTracker] with a cancel // function that causes the context to be canceled once we start dialing. // -// This struct helps with testing [enginenetx.HTTPSDialer] is WAI when the context +// This struct helps with testing [HTTPSDialer] is WAI when the context // has been canceled and we correctly shutdown all goroutines. type httpsDialerCancelingContextStatsTracker struct { cancel context.CancelFunc flags int } -var _ enginenetx.HTTPSDialerStatsTracker = &httpsDialerCancelingContextStatsTracker{} +var _ HTTPSDialerStatsTracker = &httpsDialerCancelingContextStatsTracker{} -// OnStarting implements enginenetx.HTTPSDialerStatsTracker. -func (st *httpsDialerCancelingContextStatsTracker) OnStarting(tactic *enginenetx.HTTPSDialerTactic) { +// OnStarting implements HTTPSDialerStatsTracker. +func (st *httpsDialerCancelingContextStatsTracker) OnStarting(tactic *HTTPSDialerTactic) { if (st.flags & httpsDialerCancelingContextStatsTrackerOnStarting) != 0 { st.cancel() } } -// OnTCPConnectError implements enginenetx.HTTPSDialerStatsTracker. -func (*httpsDialerCancelingContextStatsTracker) OnTCPConnectError(ctx context.Context, tactic *enginenetx.HTTPSDialerTactic, err error) { +// OnTCPConnectError implements HTTPSDialerStatsTracker. +func (*httpsDialerCancelingContextStatsTracker) OnTCPConnectError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { // nothing } -// OnTLSHandshakeError implements enginenetx.HTTPSDialerStatsTracker. -func (*httpsDialerCancelingContextStatsTracker) OnTLSHandshakeError(ctx context.Context, tactic *enginenetx.HTTPSDialerTactic, err error) { +// OnTLSHandshakeError implements HTTPSDialerStatsTracker. +func (*httpsDialerCancelingContextStatsTracker) OnTLSHandshakeError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { // nothing } -// OnTLSVerifyError implements enginenetx.HTTPSDialerStatsTracker. -func (*httpsDialerCancelingContextStatsTracker) OnTLSVerifyError(tactic *enginenetx.HTTPSDialerTactic, err error) { +// OnTLSVerifyError implements HTTPSDialerStatsTracker. +func (*httpsDialerCancelingContextStatsTracker) OnTLSVerifyError(tactic *HTTPSDialerTactic, err error) { // nothing } -// OnSuccess implements enginenetx.HTTPSDialerStatsTracker. -func (st *httpsDialerCancelingContextStatsTracker) OnSuccess(tactic *enginenetx.HTTPSDialerTactic) { +// OnSuccess implements HTTPSDialerStatsTracker. +func (st *httpsDialerCancelingContextStatsTracker) OnSuccess(tactic *HTTPSDialerTactic) { if (st.flags & httpsDialerCancelingContextStatsTrackerOnSuccess) != 0 { st.cancel() } @@ -75,7 +77,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { short bool // stats is the stats tracker to use. - stats enginenetx.HTTPSDialerStatsTracker + stats HTTPSDialerStatsTracker // endpoint is the endpoint to connect to consisting of a domain // name or IP address followed by a TCP port @@ -98,7 +100,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "net.SplitHostPort failure", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "www.example.com", // note: here the port is missing scenario: netemx.InternetScenario, configureDPI: func(dpi *netem.DPIEngine) { @@ -114,7 +116,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "hd.policy.LookupTactics failure", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "www.example.nonexistent:443", // note: the domain does not exist scenario: netemx.InternetScenario, configureDPI: func(dpi *netem.DPIEngine) { @@ -128,7 +130,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "successful dial with multiple addresses", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "www.example.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -154,7 +156,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "with TCP connect errors", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "www.example.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -188,7 +190,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "with TLS handshake errors", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "www.example.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -218,7 +220,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "with a TLS certificate valid for ANOTHER domain", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "wrong.host.badssl.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -243,7 +245,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "with TLS certificate signed by an unknown authority", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "untrusted-root.badssl.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -268,7 +270,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { { name: "with expired TLS certificate", short: true, - stats: &enginenetx.HTTPSDialerNullStatsTracker{}, + stats: &nullStatsTracker{}, endpoint: "expired.badssl.com:443", scenario: []*netemx.ScenarioDomainAddresses{{ Domains: []string{ @@ -373,13 +375,13 @@ func TestHTTPSDialerNetemQA(t *testing.T) { // create the getaddrinfo resolver resolver := netx.NewStdlibResolver(log.Log) - policy := &enginenetx.HTTPSDialerNullPolicy{ + policy := &dnsPolicy{ Logger: log.Log, Resolver: resolver, } // create the TLS dialer - dialer := enginenetx.NewHTTPSDialer( + dialer := NewHTTPSDialer( log.Log, netx, policy, @@ -436,7 +438,7 @@ func TestHTTPSDialerNetemQA(t *testing.T) { func TestHTTPSDialerTactic(t *testing.T) { t.Run("String", func(t *testing.T) { expected := `{"Address":"162.55.247.208","InitialDelay":150000000,"Port":"443","SNI":"www.example.com","VerifyHostname":"api.ooni.io"}` - ldt := &enginenetx.HTTPSDialerTactic{ + ldt := &HTTPSDialerTactic{ Address: "162.55.247.208", InitialDelay: 150 * time.Millisecond, Port: "443", @@ -451,7 +453,7 @@ func TestHTTPSDialerTactic(t *testing.T) { t.Run("Clone", func(t *testing.T) { ff := &testingx.FakeFiller{} - var expect enginenetx.HTTPSDialerTactic + var expect HTTPSDialerTactic ff.Fill(&expect) got := expect.Clone() if diff := cmp.Diff(expect.String(), got.String()); diff != "" { @@ -461,7 +463,7 @@ func TestHTTPSDialerTactic(t *testing.T) { t.Run("Summary", func(t *testing.T) { expected := `162.55.247.208:443 sni=www.example.com verify=api.ooni.io` - ldt := &enginenetx.HTTPSDialerTactic{ + ldt := &HTTPSDialerTactic{ Address: "162.55.247.208", InitialDelay: 150 * time.Millisecond, Port: "443", @@ -491,7 +493,7 @@ func TestHTTPSDialerHostNetworkQA(t *testing.T) { // https://github.com/ooni/probe-cli/pull/1295#issuecomment-1731243994 resolver := netxlite.MaybeWrapWithBogonResolver(true, netxlite.NewStdlibResolver(log.Log)) - httpsDialer := enginenetx.NewHTTPSDialer( + httpsDialer := NewHTTPSDialer( log.Log, &netxlite.Netx{Underlying: &mocks.UnderlyingNetwork{ MockDefaultCertPool: func() *x509.CertPool { @@ -504,11 +506,11 @@ func TestHTTPSDialerHostNetworkQA(t *testing.T) { MockGetaddrinfoLookupANY: tproxy.GetaddrinfoLookupANY, MockGetaddrinfoResolverNetwork: tproxy.GetaddrinfoResolverNetwork, }}, - &enginenetx.HTTPSDialerNullPolicy{ + &dnsPolicy{ Logger: log.Log, Resolver: resolver, }, - &enginenetx.HTTPSDialerNullStatsTracker{}, + &nullStatsTracker{}, ) URL := runtimex.Try1(url.Parse(server.URL)) @@ -521,3 +523,109 @@ func TestHTTPSDialerHostNetworkQA(t *testing.T) { tlsConn.Close() }) } + +func TestHTTPSDialerVerifyCertificateChain(t *testing.T) { + t.Run("without any peer certificate", func(t *testing.T) { + tlsConn := &mocks.TLSConn{ + MockConnectionState: func() tls.ConnectionState { + return tls.ConnectionState{} // empty! + }, + } + certPool := netxlite.NewMozillaCertPool() + err := httpsDialerVerifyCertificateChain("www.example.com", tlsConn, certPool) + if !errors.Is(err, errNoPeerCertificate) { + t.Fatal("unexpected error", err) + } + }) + + t.Run("with an empty hostname", func(t *testing.T) { + tlsConn := &mocks.TLSConn{ + MockConnectionState: func() tls.ConnectionState { + return tls.ConnectionState{} // empty but should not be an issue + }, + } + certPool := netxlite.NewMozillaCertPool() + err := httpsDialerVerifyCertificateChain("", tlsConn, certPool) + if !errors.Is(err, errEmptyVerifyHostname) { + t.Fatal("unexpected error", err) + } + }) +} + +func TestHTTPSDialerReduceResult(t *testing.T) { + t.Run("we return the first conn in a list of conns and close the other conns", func(t *testing.T) { + var closed int + expect := &mocks.TLSConn{} // empty + connv := []model.TLSConn{ + expect, + &mocks.TLSConn{ + Conn: mocks.Conn{ + MockClose: func() error { + closed++ + return nil + }, + }, + }, + &mocks.TLSConn{ + Conn: mocks.Conn{ + MockClose: func() error { + closed++ + return nil + }, + }, + }, + } + + conn, err := httpsDialerReduceResult(connv, nil) + if err != nil { + t.Fatal(err) + } + + if conn != expect { + t.Fatal("unexpected conn") + } + + if closed != 2 { + t.Fatal("did not call close") + } + }) + + t.Run("we join together a list of errors", func(t *testing.T) { + expectErr := "connection_refused\ninterrupted" + errorv := []error{errors.New("connection_refused"), errors.New("interrupted")} + + conn, err := httpsDialerReduceResult(nil, errorv) + if err == nil || err.Error() != expectErr { + t.Fatal("unexpected err", err) + } + + if conn != nil { + t.Fatal("expected nil conn") + } + }) + + t.Run("with a single error we return such an error", func(t *testing.T) { + expected := errors.New("connection_refused") + errorv := []error{expected} + + conn, err := httpsDialerReduceResult(nil, errorv) + if !errors.Is(err, expected) { + t.Fatal("unexpected err", err) + } + + if conn != nil { + t.Fatal("expected nil conn") + } + }) + + t.Run("we return errDNSNoAnswer if we don't have any conns or errors to return", func(t *testing.T) { + conn, err := httpsDialerReduceResult(nil, nil) + if !errors.Is(err, errDNSNoAnswer) { + t.Fatal("unexpected error", err) + } + + if conn != nil { + t.Fatal("expected nil conn") + } + }) +} diff --git a/internal/enginenetx/network.go b/internal/enginenetx/network.go index 2914799a35..e2a27e47a5 100644 --- a/internal/enginenetx/network.go +++ b/internal/enginenetx/network.go @@ -142,7 +142,7 @@ func NewNetwork( func newHTTPSDialerPolicy(kvStore model.KeyValueStore, logger model.Logger, resolver model.Resolver) HTTPSDialerPolicy { // create a composed fallback TLS dialer policy fallback := &beaconsPolicy{ - Fallback: &HTTPSDialerNullPolicy{logger, resolver}, + Fallback: &dnsPolicy{logger, resolver}, } // make sure we honor a user-provided policy diff --git a/internal/enginenetx/static_test.go b/internal/enginenetx/static_test.go index 0a1c03cbca..a33ff9b315 100644 --- a/internal/enginenetx/static_test.go +++ b/internal/enginenetx/static_test.go @@ -33,7 +33,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) { expectedPolicy *staticPolicy } - fallback := &HTTPSDialerNullPolicy{} + fallback := &dnsPolicy{} cases := []testcase{{ name: "when there is no key in the kvstore", @@ -217,7 +217,7 @@ func TestHTTPSDialerStaticPolicy(t *testing.T) { t.Run("we fallback if needed", func(t *testing.T) { ctx := context.Background() - fallback := &HTTPSDialerNullPolicy{ + fallback := &dnsPolicy{ Logger: log.Log, Resolver: &mocks.Resolver{ MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { diff --git a/internal/enginenetx/stats.go b/internal/enginenetx/stats.go index b4067bb7b9..13ad4fa781 100644 --- a/internal/enginenetx/stats.go +++ b/internal/enginenetx/stats.go @@ -17,6 +17,36 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) +// nullStatsTracker is the "null" [HTTPSDialerStatsTracker]. +type nullStatsTracker struct{} + +var _ HTTPSDialerStatsTracker = &nullStatsTracker{} + +// OnStarting implements HTTPSDialerStatsTracker. +func (*nullStatsTracker) OnStarting(tactic *HTTPSDialerTactic) { + // nothing +} + +// OnSuccess implements HTTPSDialerStatsTracker. +func (*nullStatsTracker) OnSuccess(tactic *HTTPSDialerTactic) { + // nothing +} + +// OnTCPConnectError implements HTTPSDialerStatsTracker. +func (*nullStatsTracker) OnTCPConnectError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { + // nothing +} + +// OnTLSHandshakeError implements HTTPSDialerStatsTracker. +func (*nullStatsTracker) OnTLSHandshakeError(ctx context.Context, tactic *HTTPSDialerTactic, err error) { + // nothing +} + +// OnTLSVerifyError implements HTTPSDialerStatsTracker. +func (*nullStatsTracker) OnTLSVerifyError(tactic *HTTPSDialerTactic, err error) { + // nothing +} + // statsTactic keeps stats about an [*HTTPSDialerTactic]. type statsTactic struct { // CountStarted counts the number of operations we started.