From 252dc807a9dc340916990847e907dc24784bc4d2 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Thu, 29 Jun 2023 21:47:50 +0200 Subject: [PATCH 01/25] (chore): export webconnectivitylte fields to use them in test package --- .../webconnectivitylte/cleartextflow.go | 2 +- .../webconnectivitylte/cleartextflow_test.go | 6 ++--- .../experiment/webconnectivitylte/control.go | 6 ++--- .../webconnectivitylte/dnsresolvers.go | 6 ++--- .../webconnectivitylte/ipfiltering.go | 8 +++---- .../webconnectivitylte/ipfiltering_test.go | 10 ++++----- .../experiment/webconnectivitylte/priority.go | 22 +++++++++---------- .../webconnectivitylte/secureflow.go | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/internal/experiment/webconnectivitylte/cleartextflow.go b/internal/experiment/webconnectivitylte/cleartextflow.go index fdb20e281b..02f20fec39 100644 --- a/internal/experiment/webconnectivitylte/cleartextflow.go +++ b/internal/experiment/webconnectivitylte/cleartextflow.go @@ -63,7 +63,7 @@ type CleartextFlow struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether this flow is allowed to fetch the webpage. - PrioSelector *prioritySelector + PrioSelector *PrioritySelector // Referer contains the OPTIONAL referer, used for redirects. Referer string diff --git a/internal/experiment/webconnectivitylte/cleartextflow_test.go b/internal/experiment/webconnectivitylte/cleartextflow_test.go index 39564a6b50..82bfa0586b 100644 --- a/internal/experiment/webconnectivitylte/cleartextflow_test.go +++ b/internal/experiment/webconnectivitylte/cleartextflow_test.go @@ -25,7 +25,7 @@ func TestCleartextFlow_Run(t *testing.T) { CookieJar http.CookieJar FollowRedirects bool HostHeader string - PrioSelector *prioritySelector + PrioSelector *PrioritySelector Referer string UDPAddress string URLPath string @@ -50,7 +50,7 @@ func TestCleartextFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "with loopback IPv6 endpoint", fields: fields{ @@ -61,7 +61,7 @@ func TestCleartextFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/experiment/webconnectivitylte/control.go b/internal/experiment/webconnectivitylte/control.go index d691532376..5c20eabf1a 100644 --- a/internal/experiment/webconnectivitylte/control.go +++ b/internal/experiment/webconnectivitylte/control.go @@ -21,10 +21,10 @@ import ( type EndpointMeasurementsStarter interface { // startCleartextFlows starts a TCP measurement flow for each IP addr. The [ps] // argument determines whether this flow will be allowed to fetch the webpage. - startCleartextFlows(ctx context.Context, ps *prioritySelector, addresses []DNSEntry) + startCleartextFlows(ctx context.Context, ps *PrioritySelector, addresses []DNSEntry) // startSecureFlows is like startCleartextFlows but for HTTPS. - startSecureFlows(ctx context.Context, ps *prioritySelector, addresses []DNSEntry) + startSecureFlows(ctx context.Context, ps *PrioritySelector, addresses []DNSEntry) } // Control issues a Control request and saves the results @@ -45,7 +45,7 @@ type Control struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether we will be allowed to fetch the webpage. - PrioSelector *prioritySelector + PrioSelector *PrioritySelector // TestKeys is MANDATORY and contains the TestKeys. TestKeys *TestKeys diff --git a/internal/experiment/webconnectivitylte/dnsresolvers.go b/internal/experiment/webconnectivitylte/dnsresolvers.go index 92e6000eb7..828b558da3 100644 --- a/internal/experiment/webconnectivitylte/dnsresolvers.go +++ b/internal/experiment/webconnectivitylte/dnsresolvers.go @@ -418,7 +418,7 @@ func (t *DNSResolvers) dohSplitQueries( // startCleartextFlows starts a TCP measurement flow for each IP addr. func (t *DNSResolvers) startCleartextFlows( ctx context.Context, - ps *prioritySelector, + ps *PrioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "http" { @@ -456,7 +456,7 @@ func (t *DNSResolvers) startCleartextFlows( // startSecureFlows starts a TCP+TLS measurement flow for each IP addr. func (t *DNSResolvers) startSecureFlows( ctx context.Context, - ps *prioritySelector, + ps *PrioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "https" { @@ -500,7 +500,7 @@ func (t *DNSResolvers) startSecureFlows( // maybeStartControlFlow starts the control flow iff .Session and .TestHelpers are set. func (t *DNSResolvers) maybeStartControlFlow( ctx context.Context, - ps *prioritySelector, + ps *PrioritySelector, addresses []DNSEntry, ) { // note: for subsequent requests we don't set .Session and .TestHelpers hence diff --git a/internal/experiment/webconnectivitylte/ipfiltering.go b/internal/experiment/webconnectivitylte/ipfiltering.go index 291b737367..0d0fe94083 100644 --- a/internal/experiment/webconnectivitylte/ipfiltering.go +++ b/internal/experiment/webconnectivitylte/ipfiltering.go @@ -12,15 +12,15 @@ import ( "github.com/ooni/probe-cli/v3/internal/netxlite" ) -// errNotAllowedToConnect indicates we're not allowed to connect. -var errNotAllowedToConnect = errors.New("webconnectivity: not allowed to connect") +// ErrNotAllowedToConnect indicates we're not allowed to connect. +var ErrNotAllowedToConnect = errors.New("webconnectivity: not allowed to connect") // allowedToConnect returns nil if we can connect to a given endpoint. Otherwise // it returns an error explaining why we cannot connect. func allowedToConnect(endpoint string) error { addr, _, err := net.SplitHostPort(endpoint) if err != nil { - return fmt.Errorf("%w: %s", errNotAllowedToConnect, err.Error()) + return fmt.Errorf("%w: %s", ErrNotAllowedToConnect, err.Error()) } // Implementation note: we don't remove bogons because accessing // them can lead us to discover block pages. This may change in @@ -29,7 +29,7 @@ func allowedToConnect(endpoint string) error { // We prevent connecting to localhost, however, as documented // inside https://github.com/ooni/probe/issues/2397. if netxlite.IsLoopback(addr) { - return fmt.Errorf("%w: is loopback", errNotAllowedToConnect) + return fmt.Errorf("%w: is loopback", ErrNotAllowedToConnect) } return nil } diff --git a/internal/experiment/webconnectivitylte/ipfiltering_test.go b/internal/experiment/webconnectivitylte/ipfiltering_test.go index af33761282..7036ff0b86 100644 --- a/internal/experiment/webconnectivitylte/ipfiltering_test.go +++ b/internal/experiment/webconnectivitylte/ipfiltering_test.go @@ -13,19 +13,19 @@ func Test_allowedToConnect(t *testing.T) { }{{ name: "we cannot connect when there's no port", endpoint: "8.8.4.4", - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "we cannot connect when address is a domain (should not happen)", endpoint: "dns.google:443", - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "we cannot connect for IPv4 loopback 127.0.0.1", endpoint: "127.0.0.1:443", - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "we cannot connect for IPv4 loopback 127.0.0.2", endpoint: "127.0.0.2:443", - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "we can connect to 10.0.0.1 (may change in the future)", endpoint: "10.0.0.1:443", @@ -33,7 +33,7 @@ func Test_allowedToConnect(t *testing.T) { }, { name: "we cannot connect for IPv6 loopback", endpoint: "::1", - want: errNotAllowedToConnect, + want: ErrNotAllowedToConnect, }, { name: "we can connect for public IPv4 address", endpoint: "8.8.8.8:443", diff --git a/internal/experiment/webconnectivitylte/priority.go b/internal/experiment/webconnectivitylte/priority.go index c86a4cce90..cf883a430d 100644 --- a/internal/experiment/webconnectivitylte/priority.go +++ b/internal/experiment/webconnectivitylte/priority.go @@ -41,8 +41,8 @@ import ( // because another goroutine have already fetched that webpage. var errNotPermittedToFetch = errors.New("webconnectivity: not permitted to fetch") -// prioritySelector selects the connection with the highest priority. -type prioritySelector struct { +// PrioritySelector selects the connection with the highest priority. +type PrioritySelector struct { // ch is the channel used to ask for priority ch chan *priorityRequest @@ -78,15 +78,15 @@ type priorityRequest struct { resp chan bool } -// newPrioritySelector creates a new prioritySelector instance. +// newPrioritySelector creates a new PrioritySelector instance. func newPrioritySelector( ctx context.Context, zeroTime time.Time, tk *TestKeys, logger model.Logger, addrs []DNSEntry, -) *prioritySelector { - ps := &prioritySelector{ +) *PrioritySelector { + ps := &PrioritySelector{ ch: make(chan *priorityRequest), logger: logger, m: map[string]int64{}, @@ -115,17 +115,17 @@ func newPrioritySelector( } // log formats and emits a ConnPriorityLogEntry -func (ps *prioritySelector) log(format string, v ...any) { +func (ps *PrioritySelector) log(format string, v ...any) { ps.tk.AppendConnPriorityLogEntry(&ConnPriorityLogEntry{ Msg: fmt.Sprintf(format, v...), T: time.Since(ps.zeroTime).Seconds(), }) - ps.logger.Infof("prioritySelector: "+format, v...) + ps.logger.Infof("PrioritySelector: "+format, v...) } // permissionToFetch returns whether this ready-to-use connection // is permitted to perform a round trip and fetch the webpage. -func (ps *prioritySelector) permissionToFetch(address string) bool { +func (ps *PrioritySelector) permissionToFetch(address string) bool { ipAddr, _, err := net.SplitHostPort(address) runtimex.PanicOnError(err, "net.SplitHostPort failed") r := &priorityRequest{ @@ -153,7 +153,7 @@ func (ps *prioritySelector) permissionToFetch(address string) bool { // background goroutine and terminates when [ctx] is done. // // This function implements https://github.com/ooni/probe/issues/2276. -func (ps *prioritySelector) selector(ctx context.Context) { +func (ps *PrioritySelector) selector(ctx context.Context) { // Implementation note: setting an arbitrary timeout here would // be ~an issue because we want this goroutine to be available in // case the only connections from which we could fetch a webpage @@ -213,7 +213,7 @@ Loop: } // findHighestPriority returns the highest priority request -func (ps *prioritySelector) findHighestPriority(reqs []*priorityRequest) *priorityRequest { +func (ps *PrioritySelector) findHighestPriority(reqs []*priorityRequest) *priorityRequest { runtimex.Assert(len(reqs) > 0, "findHighestPriority wants a non-empty reqs slice") for _, r := range reqs { if ps.isHighestPriority(r) { @@ -224,7 +224,7 @@ func (ps *prioritySelector) findHighestPriority(reqs []*priorityRequest) *priori } // isHighestPriority returns whether this request is highest priority -func (ps *prioritySelector) isHighestPriority(r *priorityRequest) bool { +func (ps *PrioritySelector) isHighestPriority(r *priorityRequest) bool { // See https://github.com/ooni/probe/issues/2276 flags := ps.m[r.addr] if ps.nsystem > 0 { diff --git a/internal/experiment/webconnectivitylte/secureflow.go b/internal/experiment/webconnectivitylte/secureflow.go index a1079aae11..f227555bfd 100644 --- a/internal/experiment/webconnectivitylte/secureflow.go +++ b/internal/experiment/webconnectivitylte/secureflow.go @@ -67,7 +67,7 @@ type SecureFlow struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether this flow is allowed to fetch the webpage. - PrioSelector *prioritySelector + PrioSelector *PrioritySelector // Referer contains the OPTIONAL referer, used for redirects. Referer string From c8c91f5412ec1914c44b1f53a34635f1a1947deb Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Thu, 29 Jun 2023 22:32:58 +0200 Subject: [PATCH 02/25] move cmd/oohelperd lib code to internal/oohelperd --- internal/cmd/oohelperd/handler.go | 118 ---------- internal/cmd/oohelperd/main.go | 106 +-------- internal/cmd/oohelperd/main_test.go | 5 + internal/{cmd => }/oohelperd/README.md | 0 internal/{cmd => }/oohelperd/dns.go | 2 +- internal/{cmd => }/oohelperd/dns_test.go | 2 +- internal/oohelperd/handler.go | 219 +++++++++++++++++++ internal/{cmd => }/oohelperd/handler_test.go | 8 +- internal/{cmd => }/oohelperd/http.go | 2 +- internal/{cmd => }/oohelperd/http_test.go | 2 +- internal/{cmd => }/oohelperd/ipinfo.go | 2 +- internal/{cmd => }/oohelperd/ipinfo_test.go | 2 +- internal/{cmd => }/oohelperd/logging.go | 2 +- internal/{cmd => }/oohelperd/logging_test.go | 2 +- internal/{cmd => }/oohelperd/measure.go | 4 +- internal/{cmd => }/oohelperd/metrics.go | 2 +- internal/{cmd => }/oohelperd/quic.go | 2 +- internal/{cmd => }/oohelperd/tcptls.go | 2 +- internal/{cmd => }/oohelperd/tcptls_test.go | 2 +- 19 files changed, 244 insertions(+), 240 deletions(-) delete mode 100644 internal/cmd/oohelperd/handler.go rename internal/{cmd => }/oohelperd/README.md (100%) rename internal/{cmd => }/oohelperd/dns.go (99%) rename internal/{cmd => }/oohelperd/dns_test.go (99%) create mode 100644 internal/oohelperd/handler.go rename internal/{cmd => }/oohelperd/handler_test.go (96%) rename internal/{cmd => }/oohelperd/http.go (99%) rename internal/{cmd => }/oohelperd/http_test.go (99%) rename internal/{cmd => }/oohelperd/ipinfo.go (99%) rename internal/{cmd => }/oohelperd/ipinfo_test.go (99%) rename internal/{cmd => }/oohelperd/logging.go (98%) rename internal/{cmd => }/oohelperd/logging_test.go (99%) rename internal/{cmd => }/oohelperd/measure.go (98%) rename internal/{cmd => }/oohelperd/metrics.go (98%) rename internal/{cmd => }/oohelperd/quic.go (99%) rename internal/{cmd => }/oohelperd/tcptls.go (99%) rename internal/{cmd => }/oohelperd/tcptls_test.go (98%) diff --git a/internal/cmd/oohelperd/handler.go b/internal/cmd/oohelperd/handler.go deleted file mode 100644 index 4e6f52aa5b..0000000000 --- a/internal/cmd/oohelperd/handler.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -// -// HTTP handler -// - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "sync/atomic" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/runtimex" - "github.com/ooni/probe-cli/v3/internal/version" -) - -// handler is an [http.Handler] implementing the Web -// Connectivity test helper HTTP API. -type handler struct { - // BaseLogger is the MANDATORY logger to use. - BaseLogger model.Logger - - // Indexer is the MANDATORY atomic integer used to assign an index to requests. - Indexer *atomic.Int64 - - // MaxAcceptableBody is the MANDATORY maximum acceptable response body. - MaxAcceptableBody int64 - - // Measure is the MANDATORY function that the handler should call - // for producing a response for a valid incoming request. - Measure func(ctx context.Context, config *handler, creq *model.THRequest) (*model.THResponse, error) - - // NewDialer is the MANDATORY factory to create a new Dialer. - NewDialer func(model.Logger) model.Dialer - - // NewHTTPClient is the MANDATORY factory to create a new HTTPClient. - NewHTTPClient func(model.Logger) model.HTTPClient - - // NewHTTP3Client is the MANDATORY factory to create a new HTTP3Client. - NewHTTP3Client func(model.Logger) model.HTTPClient - - // NewQUICDialer is the MANDATORY factory to create a new QUICDialer. - NewQUICDialer func(model.Logger) model.QUICDialer - - // NewResolver is the MANDATORY factory for creating a new resolver. - NewResolver func(model.Logger) model.Resolver - - // NewTLSHandshaker is the MANDATORY factory for creating a new TLS handshaker. - NewTLSHandshaker func(model.Logger) model.TLSHandshaker -} - -var _ http.Handler = &handler{} - -// ServeHTTP implements http.Handler. -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // track the number of in-flight requests - metricRequestsInflight.Inc() - defer metricRequestsInflight.Dec() - - // create and add the Server header - w.Header().Add("Server", fmt.Sprintf( - "oohelperd/%s ooniprobe-engine/%s", - version.Version, - version.Version, - )) - - // we only handle the POST method - if req.Method != "POST" { - metricRequestsCount.WithLabelValues("400", "bad_request_method").Inc() - w.WriteHeader(400) - return - } - - // read and parse request body - reader := io.LimitReader(req.Body, h.MaxAcceptableBody) - data, err := netxlite.ReadAllContext(req.Context(), reader) - if err != nil { - metricRequestsCount.WithLabelValues("400", "request_body_too_large").Inc() - w.WriteHeader(400) - return - } - var creq ctrlRequest - if err := json.Unmarshal(data, &creq); err != nil { - metricRequestsCount.WithLabelValues("400", "cannot_unmarshal_request_body").Inc() - w.WriteHeader(400) - return - } - - // measure the given input - started := time.Now() - cresp, err := h.Measure(req.Context(), h, &creq) - elapsed := time.Since(started) - - // track the time required to produce a response - metricWCTaskDurationSeconds.Observe(float64(elapsed.Seconds())) - - // handle the case of fundamental failure - if err != nil { - metricRequestsCount.WithLabelValues("400", "wctask_failed").Inc() - w.WriteHeader(400) - return - } - - // produce successful response. - // - // Note: we assume that json.Marshal cannot fail because it's a - // clearly-serializable data structure. - metricRequestsCount.WithLabelValues("200", "ok").Inc() - data, err = json.Marshal(cresp) - runtimex.PanicOnError(err, "json.Marshal failed") - w.Header().Add("Content-Type", "application/json") - w.Write(data) -} diff --git a/internal/cmd/oohelperd/main.go b/internal/cmd/oohelperd/main.go index 3d48afa401..f777157a15 100644 --- a/internal/cmd/oohelperd/main.go +++ b/internal/cmd/oohelperd/main.go @@ -7,28 +7,20 @@ import ( "fmt" "net" "net/http" - "net/http/cookiejar" "net/http/pprof" "os" "os/signal" "sync" - "sync/atomic" "syscall" "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/oohelperd" "github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/ooni/probe-cli/v3/internal/version" "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/net/publicsuffix" ) -// maxAcceptableBodySize is the maximum acceptable body size for incoming -// API requests as well as when we're measuring webpages. -const maxAcceptableBodySize = 1 << 24 - var ( // apiEndpoint is the endpoint where we serve ooniprobe requests apiEndpoint = flag.String("api-endpoint", "127.0.0.1:8080", "API endpoint") @@ -58,16 +50,6 @@ var ( versionFlag = flag.Bool("version", false, "Prints version information on the stdout") ) -// newResolver creates a new [model.Resolver] suitable for serving -// requests coming from ooniprobe clients. -func newResolver(logger model.Logger) model.Resolver { - // Implementation note: pin to a specific resolver so we don't depend upon the - // default resolver configured by the box. Also, use an encrypted transport thus - // we're less vulnerable to any policy implemented by the box's provider. - resolver := netxlite.NewParallelDNSOverHTTPSResolver(logger, "https://dns.google/dns-query") - return resolver -} - // shutdown calls srv.Shutdown with a reasonably long timeout. The srv.Shutdown // function will immediately close any open listener and then will wait until // all pending connections are closed or the context has expired. By giving pending @@ -81,90 +63,6 @@ func shutdown(srv *http.Server, wg *sync.WaitGroup) { srv.Shutdown(ctx) } -// newCookieJar is the factory for constructing a new cookier jar. -func newCookieJar() *cookiejar.Jar { - // Implementation note: the [cookiejar.New] function always returns a - // nil error; hence, it's safe here to use [runtimex.Try1]. - return runtimex.Try1(cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - })) -} - -// newHTTPClientWithTransportFactory creates a new HTTP client. -func newHTTPClientWithTransportFactory( - logger model.Logger, - txpFactory func(model.DebugLogger, model.Resolver) model.HTTPTransport, -) model.HTTPClient { - // If the DoH resolver we're using insists that a given domain maps to - // bogons, make sure we're going to fail the HTTP measurement. - // - // The TCP measurements scheduler in ipinfo.go will also refuse to - // schedule TCP measurements for bogons. - // - // While this seems theoretical, as of 2022-08-28, I see: - // - // % host polito.it - // polito.it has address 192.168.59.6 - // polito.it has address 192.168.40.1 - // polito.it mail is handled by 10 mx.polito.it. - // - // So, it's better to consider this as a possible corner case. - reso := netxlite.MaybeWrapWithBogonResolver( - true, // enabled - newResolver(logger), - ) - - // fix: We MUST set a cookie jar for measuring HTTP. See - // https://github.com/ooni/probe/issues/2488 for additional - // context and pointers to the relevant measurements. - client := &http.Client{ - Transport: txpFactory(logger, reso), - CheckRedirect: nil, - Jar: newCookieJar(), - Timeout: 0, - } - - return netxlite.WrapHTTPClient(client) -} - -// newHandler constructs the [handler] used by [main]. -func newHandler() *handler { - return &handler{ - BaseLogger: log.Log, - Indexer: &atomic.Int64{}, - MaxAcceptableBody: maxAcceptableBodySize, - Measure: measure, - - NewHTTPClient: func(logger model.Logger) model.HTTPClient { - return newHTTPClientWithTransportFactory( - logger, - netxlite.NewHTTPTransportWithResolver, - ) - }, - - NewHTTP3Client: func(logger model.Logger) model.HTTPClient { - return newHTTPClientWithTransportFactory( - logger, - netxlite.NewHTTP3TransportWithResolver, - ) - }, - - NewDialer: func(logger model.Logger) model.Dialer { - return netxlite.NewDialerWithoutResolver(logger) - }, - NewQUICDialer: func(logger model.Logger) model.QUICDialer { - return netxlite.NewQUICDialerWithoutResolver( - netxlite.NewQUICListener(), - logger, - ) - }, - NewResolver: newResolver, - NewTLSHandshaker: func(logger model.Logger) model.TLSHandshaker { - return netxlite.NewTLSHandshakerStdlib(logger) - }, - } -} - func main() { // parse command line options flag.Parse() @@ -194,7 +92,7 @@ func main() { mux := http.NewServeMux() // add the main oohelperd handler to the mux - mux.Handle("/", newHandler()) + mux.Handle("/", oohelperd.NewHandler()) // create a listening server for serving ooniprobe requests srv := &http.Server{Addr: *apiEndpoint, Handler: mux} diff --git a/internal/cmd/oohelperd/main_test.go b/internal/cmd/oohelperd/main_test.go index 8aefe4da8b..335e07bb3b 100644 --- a/internal/cmd/oohelperd/main_test.go +++ b/internal/cmd/oohelperd/main_test.go @@ -15,6 +15,11 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) +type ( + ctrlRequest = model.THRequest + ctrlResponse = model.THResponse +) + func TestMainRunServerWorkingAsIntended(t *testing.T) { // let the kernel pick a random free port *apiEndpoint = "127.0.0.1:0" diff --git a/internal/cmd/oohelperd/README.md b/internal/oohelperd/README.md similarity index 100% rename from internal/cmd/oohelperd/README.md rename to internal/oohelperd/README.md diff --git a/internal/cmd/oohelperd/dns.go b/internal/oohelperd/dns.go similarity index 99% rename from internal/cmd/oohelperd/dns.go rename to internal/oohelperd/dns.go index c7a1f2c83a..b56b9e20ee 100644 --- a/internal/cmd/oohelperd/dns.go +++ b/internal/oohelperd/dns.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // DNS measurements diff --git a/internal/cmd/oohelperd/dns_test.go b/internal/oohelperd/dns_test.go similarity index 99% rename from internal/cmd/oohelperd/dns_test.go rename to internal/oohelperd/dns_test.go index 5a6b3e354b..fc5efacc4a 100644 --- a/internal/cmd/oohelperd/dns_test.go +++ b/internal/oohelperd/dns_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "context" diff --git a/internal/oohelperd/handler.go b/internal/oohelperd/handler.go new file mode 100644 index 0000000000..15eda5435f --- /dev/null +++ b/internal/oohelperd/handler.go @@ -0,0 +1,219 @@ +package oohelperd + +// +// HTTP handler +// + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "sync/atomic" + "time" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/version" + "golang.org/x/net/publicsuffix" +) + +// maxAcceptableBodySize is the maximum acceptable body size for incoming +// API requests as well as when we're measuring webpages. +const maxAcceptableBodySize = 1 << 24 + +// Handler is an [http.Handler] implementing the Web +// Connectivity test helper HTTP API. +type Handler struct { + // BaseLogger is the MANDATORY logger to use. + BaseLogger model.Logger + + // Indexer is the MANDATORY atomic integer used to assign an index to requests. + Indexer *atomic.Int64 + + // MaxAcceptableBody is the MANDATORY maximum acceptable response body. + MaxAcceptableBody int64 + + // Measure is the MANDATORY function that the handler should call + // for producing a response for a valid incoming request. + Measure func(ctx context.Context, config *Handler, creq *model.THRequest) (*model.THResponse, error) + + // NewDialer is the MANDATORY factory to create a new Dialer. + NewDialer func(model.Logger) model.Dialer + + // NewHTTPClient is the MANDATORY factory to create a new HTTPClient. + NewHTTPClient func(model.Logger) model.HTTPClient + + // NewHTTP3Client is the MANDATORY factory to create a new HTTP3Client. + NewHTTP3Client func(model.Logger) model.HTTPClient + + // NewQUICDialer is the MANDATORY factory to create a new QUICDialer. + NewQUICDialer func(model.Logger) model.QUICDialer + + // NewResolver is the MANDATORY factory for creating a new resolver. + NewResolver func(model.Logger) model.Resolver + + // NewTLSHandshaker is the MANDATORY factory for creating a new TLS handshaker. + NewTLSHandshaker func(model.Logger) model.TLSHandshaker +} + +var _ http.Handler = &Handler{} + +// NewHandler constructs the [handler]. +func NewHandler() *Handler { + return &Handler{ + BaseLogger: log.Log, + Indexer: &atomic.Int64{}, + MaxAcceptableBody: maxAcceptableBodySize, + Measure: measure, + + NewHTTPClient: func(logger model.Logger) model.HTTPClient { + return newHTTPClientWithTransportFactory( + logger, + netxlite.NewHTTPTransportWithResolver, + ) + }, + + NewHTTP3Client: func(logger model.Logger) model.HTTPClient { + return newHTTPClientWithTransportFactory( + logger, + netxlite.NewHTTP3TransportWithResolver, + ) + }, + + NewDialer: func(logger model.Logger) model.Dialer { + return netxlite.NewDialerWithoutResolver(logger) + }, + NewQUICDialer: func(logger model.Logger) model.QUICDialer { + return netxlite.NewQUICDialerWithoutResolver( + netxlite.NewQUICListener(), + logger, + ) + }, + NewResolver: newResolver, + NewTLSHandshaker: func(logger model.Logger) model.TLSHandshaker { + return netxlite.NewTLSHandshakerStdlib(logger) + }, + } +} + +// ServeHTTP implements http.Handler. +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // track the number of in-flight requests + metricRequestsInflight.Inc() + defer metricRequestsInflight.Dec() + + // create and add the Server header + w.Header().Add("Server", fmt.Sprintf( + "oohelperd/%s ooniprobe-engine/%s", + version.Version, + version.Version, + )) + + // we only handle the POST method + if req.Method != "POST" { + metricRequestsCount.WithLabelValues("400", "bad_request_method").Inc() + w.WriteHeader(400) + return + } + + // read and parse request body + reader := io.LimitReader(req.Body, h.MaxAcceptableBody) + data, err := netxlite.ReadAllContext(req.Context(), reader) + if err != nil { + metricRequestsCount.WithLabelValues("400", "request_body_too_large").Inc() + w.WriteHeader(400) + return + } + var creq ctrlRequest + if err := json.Unmarshal(data, &creq); err != nil { + metricRequestsCount.WithLabelValues("400", "cannot_unmarshal_request_body").Inc() + w.WriteHeader(400) + return + } + + // measure the given input + started := time.Now() + cresp, err := h.Measure(req.Context(), h, &creq) + elapsed := time.Since(started) + + // track the time required to produce a response + metricWCTaskDurationSeconds.Observe(float64(elapsed.Seconds())) + + // handle the case of fundamental failure + if err != nil { + metricRequestsCount.WithLabelValues("400", "wctask_failed").Inc() + w.WriteHeader(400) + return + } + + // produce successful response. + // + // Note: we assume that json.Marshal cannot fail because it's a + // clearly-serializable data structure. + metricRequestsCount.WithLabelValues("200", "ok").Inc() + data, err = json.Marshal(cresp) + runtimex.PanicOnError(err, "json.Marshal failed") + w.Header().Add("Content-Type", "application/json") + w.Write(data) +} + +// newResolver creates a new [model.Resolver] suitable for serving +// requests coming from ooniprobe clients. +func newResolver(logger model.Logger) model.Resolver { + // Implementation note: pin to a specific resolver so we don't depend upon the + // default resolver configured by the box. Also, use an encrypted transport thus + // we're less vulnerable to any policy implemented by the box's provider. + resolver := netxlite.NewParallelDNSOverHTTPSResolver(logger, "https://dns.google/dns-query") + return resolver +} + +// newCookieJar is the factory for constructing a new cookier jar. +func newCookieJar() *cookiejar.Jar { + // Implementation note: the [cookiejar.New] function always returns a + // nil error; hence, it's safe here to use [runtimex.Try1]. + return runtimex.Try1(cookiejar.New(&cookiejar.Options{ + PublicSuffixList: publicsuffix.List, + })) +} + +// newHTTPClientWithTransportFactory creates a new HTTP client. +func newHTTPClientWithTransportFactory( + logger model.Logger, + txpFactory func(model.DebugLogger, model.Resolver) model.HTTPTransport, +) model.HTTPClient { + // If the DoH resolver we're using insists that a given domain maps to + // bogons, make sure we're going to fail the HTTP measurement. + // + // The TCP measurements scheduler in ipinfo.go will also refuse to + // schedule TCP measurements for bogons. + // + // While this seems theoretical, as of 2022-08-28, I see: + // + // % host polito.it + // polito.it has address 192.168.59.6 + // polito.it has address 192.168.40.1 + // polito.it mail is handled by 10 mx.polito.it. + // + // So, it's better to consider this as a possible corner case. + reso := netxlite.MaybeWrapWithBogonResolver( + true, // enabled + newResolver(logger), + ) + + // fix: We MUST set a cookie jar for measuring HTTP. See + // https://github.com/ooni/probe/issues/2488 for additional + // context and pointers to the relevant measurements. + client := &http.Client{ + Transport: txpFactory(logger, reso), + CheckRedirect: nil, + Jar: newCookieJar(), + Timeout: 0, + } + + return netxlite.WrapHTTPClient(client) +} diff --git a/internal/cmd/oohelperd/handler_test.go b/internal/oohelperd/handler_test.go similarity index 96% rename from internal/cmd/oohelperd/handler_test.go rename to internal/oohelperd/handler_test.go index fbe1405765..48a284e237 100644 --- a/internal/cmd/oohelperd/handler_test.go +++ b/internal/oohelperd/handler_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "context" @@ -70,7 +70,7 @@ func TestHandlerWorkingAsIntended(t *testing.T) { // measureFn optionally allows overriding the default // value of the handler.Measure function measureFn func( - ctx context.Context, config *handler, creq *model.THRequest) (*model.THResponse, error) + ctx context.Context, config *Handler, creq *model.THRequest) (*model.THResponse, error) // reqBody is the request body to use reqBody io.Reader @@ -124,7 +124,7 @@ func TestHandlerWorkingAsIntended(t *testing.T) { parseBody: false, }, { name: "with reasonably good request", - measureFn: func(ctx context.Context, config *handler, creq *model.THRequest) (*model.THResponse, error) { + measureFn: func(ctx context.Context, config *Handler, creq *model.THRequest) (*model.THResponse, error) { cresp := &model.THResponse{} return cresp, nil }, @@ -151,7 +151,7 @@ func TestHandlerWorkingAsIntended(t *testing.T) { for _, expect := range expectations { t.Run(expect.name, func(t *testing.T) { // create handler and possibly override .Measure - handler := newHandler() + handler := NewHandler() if expect.measureFn != nil { handler.Measure = expect.measureFn } diff --git a/internal/cmd/oohelperd/http.go b/internal/oohelperd/http.go similarity index 99% rename from internal/cmd/oohelperd/http.go rename to internal/oohelperd/http.go index da7fae089a..a7331d985c 100644 --- a/internal/cmd/oohelperd/http.go +++ b/internal/oohelperd/http.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // HTTP measurements diff --git a/internal/cmd/oohelperd/http_test.go b/internal/oohelperd/http_test.go similarity index 99% rename from internal/cmd/oohelperd/http_test.go rename to internal/oohelperd/http_test.go index 41c967c1e0..66a36b40f4 100644 --- a/internal/cmd/oohelperd/http_test.go +++ b/internal/oohelperd/http_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "context" diff --git a/internal/cmd/oohelperd/ipinfo.go b/internal/oohelperd/ipinfo.go similarity index 99% rename from internal/cmd/oohelperd/ipinfo.go rename to internal/oohelperd/ipinfo.go index 24e5aaf6cc..23669f0afe 100644 --- a/internal/cmd/oohelperd/ipinfo.go +++ b/internal/oohelperd/ipinfo.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // Generates IP and endpoint information. diff --git a/internal/cmd/oohelperd/ipinfo_test.go b/internal/oohelperd/ipinfo_test.go similarity index 99% rename from internal/cmd/oohelperd/ipinfo_test.go rename to internal/oohelperd/ipinfo_test.go index 5515c73b07..99aa4a6dba 100644 --- a/internal/cmd/oohelperd/ipinfo_test.go +++ b/internal/oohelperd/ipinfo_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "net/url" diff --git a/internal/cmd/oohelperd/logging.go b/internal/oohelperd/logging.go similarity index 98% rename from internal/cmd/oohelperd/logging.go rename to internal/oohelperd/logging.go index cc140cafb3..dbb73bb3f6 100644 --- a/internal/cmd/oohelperd/logging.go +++ b/internal/oohelperd/logging.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // Logging code diff --git a/internal/cmd/oohelperd/logging_test.go b/internal/oohelperd/logging_test.go similarity index 99% rename from internal/cmd/oohelperd/logging_test.go rename to internal/oohelperd/logging_test.go index 2008659995..bfaf93a2f3 100644 --- a/internal/cmd/oohelperd/logging_test.go +++ b/internal/oohelperd/logging_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "testing" diff --git a/internal/cmd/oohelperd/measure.go b/internal/oohelperd/measure.go similarity index 98% rename from internal/cmd/oohelperd/measure.go rename to internal/oohelperd/measure.go index 8a1766b454..d47930aaf5 100644 --- a/internal/cmd/oohelperd/measure.go +++ b/internal/oohelperd/measure.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // Top-level measurement algorithm @@ -24,7 +24,7 @@ type ( // measure performs the measurement described by the request and // returns the corresponding response or an error. -func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResponse, error) { +func measure(ctx context.Context, config *Handler, creq *ctrlRequest) (*ctrlResponse, error) { // create indexed logger logger := &prefixLogger{ indexstr: fmt.Sprintf("<#%d> ", config.Indexer.Add(1)), diff --git a/internal/cmd/oohelperd/metrics.go b/internal/oohelperd/metrics.go similarity index 98% rename from internal/cmd/oohelperd/metrics.go rename to internal/oohelperd/metrics.go index f8468b25ce..37e99b6e01 100644 --- a/internal/cmd/oohelperd/metrics.go +++ b/internal/oohelperd/metrics.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // Metrics definitions diff --git a/internal/cmd/oohelperd/quic.go b/internal/oohelperd/quic.go similarity index 99% rename from internal/cmd/oohelperd/quic.go rename to internal/oohelperd/quic.go index 842ffc5064..471d1b423b 100644 --- a/internal/cmd/oohelperd/quic.go +++ b/internal/oohelperd/quic.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // QUIC handshake measurements diff --git a/internal/cmd/oohelperd/tcptls.go b/internal/oohelperd/tcptls.go similarity index 99% rename from internal/cmd/oohelperd/tcptls.go rename to internal/oohelperd/tcptls.go index ec62d14382..3a76130709 100644 --- a/internal/cmd/oohelperd/tcptls.go +++ b/internal/oohelperd/tcptls.go @@ -1,4 +1,4 @@ -package main +package oohelperd // // TCP connect (and optionally TLS handshake) measurements diff --git a/internal/cmd/oohelperd/tcptls_test.go b/internal/oohelperd/tcptls_test.go similarity index 98% rename from internal/cmd/oohelperd/tcptls_test.go rename to internal/oohelperd/tcptls_test.go index a7f596a96c..6dac56cc19 100644 --- a/internal/cmd/oohelperd/tcptls_test.go +++ b/internal/oohelperd/tcptls_test.go @@ -1,4 +1,4 @@ -package main +package oohelperd import ( "testing" From 6cbe60109e5ca8089c498a9459fcf73964b18c0d Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Thu, 29 Jun 2023 22:33:57 +0200 Subject: [PATCH 03/25] filtering: expose DNSComposeResponse --- internal/netxlite/filtering/dns.go | 8 ++++---- internal/netxlite/filtering/http.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/netxlite/filtering/dns.go b/internal/netxlite/filtering/dns.go index 02669eed70..f23cac7384 100644 --- a/internal/netxlite/filtering/dns.go +++ b/internal/netxlite/filtering/dns.go @@ -153,11 +153,11 @@ func (p *DNSServer) nxdomain(query *dns.Msg) *dns.Msg { } func (p *DNSServer) localHost(query *dns.Msg) *dns.Msg { - return dnsComposeResponse(query, net.IPv6loopback, net.IPv4(127, 0, 0, 1)) + return DNSComposeResponse(query, net.IPv6loopback, net.IPv4(127, 0, 0, 1)) } func (p *DNSServer) empty(query *dns.Msg) *dns.Msg { - return dnsComposeResponse(query) + return DNSComposeResponse(query) } func dnsComposeQuery(domain string, qtype uint16) *dns.Msg { @@ -174,7 +174,7 @@ func dnsComposeQuery(domain string, qtype uint16) *dns.Msg { return query } -func dnsComposeResponse(query *dns.Msg, ips ...net.IP) *dns.Msg { +func DNSComposeResponse(query *dns.Msg, ips ...net.IP) *dns.Msg { runtimex.PanicIfTrue(len(query.Question) != 1, "expecting a single question") question := query.Question[0] reply := new(dns.Msg) @@ -219,5 +219,5 @@ func (p *DNSServer) cache(name string, query *dns.Msg) *dns.Msg { if len(ipAddrs) <= 0 { return p.nxdomain(query) } - return dnsComposeResponse(query, ipAddrs...) + return DNSComposeResponse(query, ipAddrs...) } diff --git a/internal/netxlite/filtering/http.go b/internal/netxlite/filtering/http.go index 031a0ea5bc..44a9b0f20a 100644 --- a/internal/netxlite/filtering/http.go +++ b/internal/netxlite/filtering/http.go @@ -176,7 +176,7 @@ func httpServeDNSOverHTTPS(w http.ResponseWriter, r *http.Request) { err = query.Unpack(rawQuery) runtimex.PanicOnError(err, "query.Unpack failed") runtimex.PanicIfTrue(query.Response, "is a response") - response := dnsComposeResponse(query, net.IPv4(8, 8, 8, 8), net.IPv4(8, 8, 4, 4)) + response := DNSComposeResponse(query, net.IPv4(8, 8, 8, 8), net.IPv4(8, 8, 4, 4)) rawResponse, err := response.Pack() runtimex.PanicOnError(err, "response.Pack failed") w.Write(rawResponse) From 65dc454eb63a841343149cf31b4aa5bf872ff994 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Thu, 29 Jun 2023 22:35:49 +0200 Subject: [PATCH 04/25] feat: webconnectivitylte success test using netemx --- .../webconnectivitylte/secureflow_test.go | 206 +++++++++++++++++- internal/netemx/dns.go | 62 ++++++ internal/netemx/geoip.go | 12 + 3 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 internal/netemx/dns.go create mode 100644 internal/netemx/geoip.go diff --git a/internal/experiment/webconnectivitylte/secureflow_test.go b/internal/experiment/webconnectivitylte/secureflow_test.go index 4fbaef4f41..56eb42c1ba 100644 --- a/internal/experiment/webconnectivitylte/secureflow_test.go +++ b/internal/experiment/webconnectivitylte/secureflow_test.go @@ -1,7 +1,8 @@ -package webconnectivitylte +package webconnectivitylte_test import ( "context" + "encoding/json" "errors" "net/http" "sync" @@ -9,24 +10,31 @@ import ( "testing" "time" + "github.com/apex/log" + "github.com/ooni/netem" + "github.com/ooni/probe-cli/v3/internal/engine" + "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/netemx" + "github.com/ooni/probe-cli/v3/internal/oohelperd" + "github.com/ooni/probe-cli/v3/internal/runtimex" ) func TestSecureFlow_Run(t *testing.T) { type fields struct { Address string - DNSCache *DNSCache + DNSCache *webconnectivitylte.DNSCache IDGenerator *atomic.Int64 Logger model.Logger - NumRedirects *NumRedirects - TestKeys *TestKeys + NumRedirects *webconnectivitylte.NumRedirects + TestKeys *webconnectivitylte.TestKeys ZeroTime time.Time WaitGroup *sync.WaitGroup ALPN []string CookieJar http.CookieJar FollowRedirects bool HostHeader string - PrioSelector *prioritySelector + PrioSelector *webconnectivitylte.PrioritySelector Referer string SNI string UDPAddress string @@ -52,7 +60,7 @@ func TestSecureFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: errNotAllowedToConnect, + want: webconnectivitylte.ErrNotAllowedToConnect, }, { name: "with loopback IPv6 endpoint", fields: fields{ @@ -63,11 +71,11 @@ func TestSecureFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: errNotAllowedToConnect, + want: webconnectivitylte.ErrNotAllowedToConnect, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tr := &SecureFlow{ + tr := &webconnectivitylte.SecureFlow{ Address: tt.fields.Address, DNSCache: tt.fields.DNSCache, IDGenerator: tt.fields.IDGenerator, @@ -94,3 +102,185 @@ func TestSecureFlow_Run(t *testing.T) { }) } } + +func TestSuccess(t *testing.T) { + // configure default UDP DNS server + dnsConfig := netem.NewDNSConfig() + dnsConfig.AddRecord( + "dns.quad9.net", + "dns.quad9.net", + "104.16.248.249", + ) + dnsConfig.AddRecord( + "mozilla.cloudflare-dns.com", + "mozilla.cloudflare-dns.com", + "104.16.248.249", + ) + dnsConfig.AddRecord( + "dns.nextdns.io", + "dns.nextdns.io", + "104.16.248.249", + ) + dnsConfig.AddRecord( + "dns.google", + "dns.google", + "104.16.248.249", + ) + dnsConfig.AddRecord( + "www.example.com", + "www.example.com", + "93.184.216.34", + ) + + // configure DoH server + dohServer := &netemx.DoHServer{} + dohServer.AddRecord("ams-pg-test.ooni.org", "188.166.93.143") + dohServer.AddRecord("geoip.ubuntu.com", "185.125.188.132") + dohServer.AddRecord("www.example.com", "93.184.216.34") + dohServer.AddRecord("0.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("1.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("2.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("3.th.ooni.org", "104.248.30.161") + + // client config with DNS server at 8.8.4.4 + clientConf := &netemx.ClientConfig{ + DNSConfig: dnsConfig, + ResolverAddr: "8.8.4.4", + } + + // servers config contains + serversConf := &netemx.ServersConfig{ + DNSConfig: dnsConfig, + Servers: []netemx.ConfigServerStack{ + { + ServerAddr: "13.13.13.13", + HTTPServers: []netemx.ConfigHTTPServer{{Port: 443}}, + }, + { + ServerAddr: "104.248.30.161", + HTTPServers: []netemx.ConfigHTTPServer{ + { + Port: 443, + Handler: oohelperd.NewHandler(), + }, + }, + }, + { + ServerAddr: "104.16.248.249", + HTTPServers: []netemx.ConfigHTTPServer{ + { + Port: 443, + Handler: dohServer, + }, + }, + }, + { + ServerAddr: "188.166.93.143", + HTTPServers: []netemx.ConfigHTTPServer{ + { + Port: 443, + Handler: &probeService{}, + }, + }, + }, + { + ServerAddr: "185.125.188.132", + HTTPServers: []netemx.ConfigHTTPServer{ + { + Port: 443, + Handler: &netemx.GeoIPLookup{}, + }, + }, + }, + }, + } + // create a new test environment + env := netemx.NewEnvironment(clientConf, serversConf) + defer env.Close() + env.Do(func() { + measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) + ctx := context.Background() + // we need a real session because we need the web-connectivity helper + // as well as the ASN database + sess := newsession(t, true) + measurement := &model.Measurement{Input: "https://www.example.com"} + callbacks := model.NewPrinterCallbacks(log.Log) + args := &model.ExperimentArgs{ + Callbacks: callbacks, + Measurement: measurement, + Session: sess, + } + err := measurer.Run(ctx, args) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*webconnectivitylte.TestKeys) + if tk.ControlFailure != nil { + t.Fatal("unexpected control_failure", *tk.ControlFailure) + } + if tk.DNSExperimentFailure != nil { + t.Fatal("unexpected dns_experiment_failure", *tk.DNSExperimentFailure) + } + if tk.HTTPExperimentFailure != nil { + t.Fatal("unexpected http_experiment_failure", *tk.HTTPExperimentFailure) + } + }) +} + +type probeService struct{} + +type th struct { + Addr string `json:"address"` + T string `json:"type"` +} + +func (p *probeService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp := map[string][]th{ + "web-connectivity": { + { + Addr: "https://2.th.ooni.org", + T: "https", + }, + { + Addr: "https://3.th.ooni.org", + T: "https", + }, + { + Addr: "https://0.th.ooni.org", + T: "https", + }, + { + Addr: "https://1.th.ooni.org", + T: "https", + }, + }, + } + data, err := json.Marshal(resp) + runtimex.PanicOnError(err, "json.Marshal failed") + w.Header().Add("Content-Type", "application/json") + w.Write(data) +} + +func newsession(t *testing.T, lookupBackends bool) model.ExperimentSession { + sess, err := engine.NewSession(context.Background(), engine.SessionConfig{ + AvailableProbeServices: []model.OOAPIService{{ + Address: "https://ams-pg-test.ooni.org", + Type: "https", + }}, + Logger: log.Log, + SoftwareName: "ooniprobe-engine", + SoftwareVersion: "0.0.1", + }) + if err != nil { + t.Fatal(err) + } + if lookupBackends { + if err := sess.MaybeLookupBackends(); err != nil { + t.Fatal(err) + } + } + if err := sess.MaybeLookupLocation(); err != nil { + t.Fatal(err) + } + return sess +} diff --git a/internal/netemx/dns.go b/internal/netemx/dns.go new file mode 100644 index 0000000000..1ce5f4d779 --- /dev/null +++ b/internal/netemx/dns.go @@ -0,0 +1,62 @@ +package netemx + +import ( + "io" + "net" + "net/http" + "sync" + + "github.com/miekg/dns" + "github.com/ooni/probe-cli/v3/internal/netxlite/filtering" + "github.com/ooni/probe-cli/v3/internal/runtimex" +) + +type DoHServer struct { + rec map[string]net.IP + mu sync.Mutex +} + +func (p *DoHServer) AddRecord(domain string, ip string) { + defer p.mu.Unlock() + p.mu.Lock() + if p.rec == nil { + p.rec = make(map[string]net.IP) + } + p.rec[domain+"."] = net.ParseIP(ip) +} + +func (p *DoHServer) lookup(name string) (net.IP, bool) { + defer p.mu.Unlock() + p.mu.Lock() + ip, found := p.rec[name] + return ip, found +} + +func (p *DoHServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer p.HTTPPanicToInternalServerError(w) + rawQuery, err := io.ReadAll(r.Body) + runtimex.PanicOnError(err, "io.ReadAll failed") + query := &dns.Msg{} + err = query.Unpack(rawQuery) + runtimex.PanicOnError(err, "query.Unpack failed") + runtimex.PanicIfTrue(query.Response, "is a response") + + ip, found := p.lookup(query.Question[0].Name) + var response *dns.Msg + if found { + response = filtering.DNSComposeResponse(query, ip) + } else { + response = &dns.Msg{} + response.SetRcode(query, dns.RcodeNameError) + } + rawResponse, err := response.Pack() + runtimex.PanicOnError(err, "response.Pack failed") + w.Header().Add("content-type", "application/dns-message") + w.Write(rawResponse) +} + +func (p *DoHServer) HTTPPanicToInternalServerError(w http.ResponseWriter) { + if r := recover(); r != nil { + w.WriteHeader(500) + } +} diff --git a/internal/netemx/geoip.go b/internal/netemx/geoip.go new file mode 100644 index 0000000000..27beed72a4 --- /dev/null +++ b/internal/netemx/geoip.go @@ -0,0 +1,12 @@ +package netemx + +import "net/http" + +type GeoIPLookup struct{} + +func (p *GeoIPLookup) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp := `89.0.2.153OKDEDEUGermany07Nordrhein-WestfalenAachen5207450.74796.04850Europe/Berlin` + + w.Header().Add("Content-Type", "text/xml") + w.Write([]byte(resp)) +} From daeea0d22dde74544547f9155319d4856062fc22 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 30 Jun 2023 00:22:57 +0200 Subject: [PATCH 05/25] renamed webconnectivitylte test --- .../webconnectivitylte/{secureflow_test.go => measurer_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/experiment/webconnectivitylte/{secureflow_test.go => measurer_test.go} (100%) diff --git a/internal/experiment/webconnectivitylte/secureflow_test.go b/internal/experiment/webconnectivitylte/measurer_test.go similarity index 100% rename from internal/experiment/webconnectivitylte/secureflow_test.go rename to internal/experiment/webconnectivitylte/measurer_test.go From 4e53b8fbdf591f36b5bea1cc369af3cc1f8242cb Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 30 Jun 2023 00:30:30 +0200 Subject: [PATCH 06/25] split webconnectivitylte tests --- .../webconnectivitylte/measurer_test.go | 99 ++----------------- .../webconnectivitylte/secureflow_test.go | 97 ++++++++++++++++++ 2 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 internal/experiment/webconnectivitylte/secureflow_test.go diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index 56eb42c1ba..fcef635a97 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -3,12 +3,8 @@ package webconnectivitylte_test import ( "context" "encoding/json" - "errors" "net/http" - "sync" - "sync/atomic" "testing" - "time" "github.com/apex/log" "github.com/ooni/netem" @@ -20,90 +16,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) -func TestSecureFlow_Run(t *testing.T) { - type fields struct { - Address string - DNSCache *webconnectivitylte.DNSCache - IDGenerator *atomic.Int64 - Logger model.Logger - NumRedirects *webconnectivitylte.NumRedirects - TestKeys *webconnectivitylte.TestKeys - ZeroTime time.Time - WaitGroup *sync.WaitGroup - ALPN []string - CookieJar http.CookieJar - FollowRedirects bool - HostHeader string - PrioSelector *webconnectivitylte.PrioritySelector - Referer string - SNI string - UDPAddress string - URLPath string - URLRawQuery string - } - type args struct { - parentCtx context.Context - index int64 - } - tests := []struct { - name string - fields fields - args args - want error - }{{ - name: "with loopback IPv4 endpoint", - fields: fields{ - Address: "127.0.0.1:443", - Logger: model.DiscardLogger, - }, - args: args{ - parentCtx: context.Background(), - index: 0, - }, - want: webconnectivitylte.ErrNotAllowedToConnect, - }, { - name: "with loopback IPv6 endpoint", - fields: fields{ - Address: "[::1]:443", - Logger: model.DiscardLogger, - }, - args: args{ - parentCtx: context.Background(), - index: 0, - }, - want: webconnectivitylte.ErrNotAllowedToConnect, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &webconnectivitylte.SecureFlow{ - Address: tt.fields.Address, - DNSCache: tt.fields.DNSCache, - IDGenerator: tt.fields.IDGenerator, - Logger: tt.fields.Logger, - NumRedirects: tt.fields.NumRedirects, - TestKeys: tt.fields.TestKeys, - ZeroTime: tt.fields.ZeroTime, - WaitGroup: tt.fields.WaitGroup, - ALPN: tt.fields.ALPN, - CookieJar: tt.fields.CookieJar, - FollowRedirects: tt.fields.FollowRedirects, - HostHeader: tt.fields.HostHeader, - PrioSelector: tt.fields.PrioSelector, - Referer: tt.fields.Referer, - SNI: tt.fields.SNI, - UDPAddress: tt.fields.UDPAddress, - URLPath: tt.fields.URLPath, - URLRawQuery: tt.fields.URLRawQuery, - } - err := tr.Run(tt.args.parentCtx, tt.args.index) - if !errors.Is(err, tt.want) { - t.Errorf("SecureFlow.Run() error = %v, want %v", err, tt.want) - } - }) - } -} - -func TestSuccess(t *testing.T) { +func newEnvironment() *netemx.Environment { // configure default UDP DNS server dnsConfig := netem.NewDNSConfig() dnsConfig.AddRecord( @@ -153,8 +66,8 @@ func TestSuccess(t *testing.T) { DNSConfig: dnsConfig, Servers: []netemx.ConfigServerStack{ { - ServerAddr: "13.13.13.13", - HTTPServers: []netemx.ConfigHTTPServer{{Port: 443}}, + ServerAddr: "93.184.216.34", + HTTPServers: []netemx.ConfigHTTPServer{{Port: 443}, {Port: 80}}, }, { ServerAddr: "104.248.30.161", @@ -196,6 +109,12 @@ func TestSuccess(t *testing.T) { } // create a new test environment env := netemx.NewEnvironment(clientConf, serversConf) + + return env +} + +func TestSuccess(t *testing.T) { + env := newEnvironment() defer env.Close() env.Do(func() { measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) diff --git a/internal/experiment/webconnectivitylte/secureflow_test.go b/internal/experiment/webconnectivitylte/secureflow_test.go new file mode 100644 index 0000000000..1fba228e36 --- /dev/null +++ b/internal/experiment/webconnectivitylte/secureflow_test.go @@ -0,0 +1,97 @@ +package webconnectivitylte_test + +import ( + "context" + "errors" + "net/http" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" + "github.com/ooni/probe-cli/v3/internal/model" +) + +func TestSecureFlow_Run(t *testing.T) { + type fields struct { + Address string + DNSCache *webconnectivitylte.DNSCache + IDGenerator *atomic.Int64 + Logger model.Logger + NumRedirects *webconnectivitylte.NumRedirects + TestKeys *webconnectivitylte.TestKeys + ZeroTime time.Time + WaitGroup *sync.WaitGroup + ALPN []string + CookieJar http.CookieJar + FollowRedirects bool + HostHeader string + PrioSelector *webconnectivitylte.PrioritySelector + Referer string + SNI string + UDPAddress string + URLPath string + URLRawQuery string + } + type args struct { + parentCtx context.Context + index int64 + } + tests := []struct { + name string + fields fields + args args + want error + }{{ + name: "with loopback IPv4 endpoint", + fields: fields{ + Address: "127.0.0.1:443", + Logger: model.DiscardLogger, + }, + args: args{ + parentCtx: context.Background(), + index: 0, + }, + want: webconnectivitylte.ErrNotAllowedToConnect, + }, { + name: "with loopback IPv6 endpoint", + fields: fields{ + Address: "[::1]:443", + Logger: model.DiscardLogger, + }, + args: args{ + parentCtx: context.Background(), + index: 0, + }, + want: webconnectivitylte.ErrNotAllowedToConnect, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &webconnectivitylte.SecureFlow{ + Address: tt.fields.Address, + DNSCache: tt.fields.DNSCache, + IDGenerator: tt.fields.IDGenerator, + Logger: tt.fields.Logger, + NumRedirects: tt.fields.NumRedirects, + TestKeys: tt.fields.TestKeys, + ZeroTime: tt.fields.ZeroTime, + WaitGroup: tt.fields.WaitGroup, + ALPN: tt.fields.ALPN, + CookieJar: tt.fields.CookieJar, + FollowRedirects: tt.fields.FollowRedirects, + HostHeader: tt.fields.HostHeader, + PrioSelector: tt.fields.PrioSelector, + Referer: tt.fields.Referer, + SNI: tt.fields.SNI, + UDPAddress: tt.fields.UDPAddress, + URLPath: tt.fields.URLPath, + URLRawQuery: tt.fields.URLRawQuery, + } + err := tr.Run(tt.args.parentCtx, tt.args.index) + if !errors.Is(err, tt.want) { + t.Errorf("SecureFlow.Run() error = %v, want %v", err, tt.want) + } + }) + } +} From 7f46267adcd26fb1c1388c7e694ef65aef8d402c Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 7 Jul 2023 10:21:31 +0200 Subject: [PATCH 07/25] webconnectivitylte: adapt measurer_test to new netemx QAEnv --- .../webconnectivitylte/measurer_test.go | 99 +++++-------------- 1 file changed, 25 insertions(+), 74 deletions(-) diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index fcef635a97..c72f495199 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/apex/log" - "github.com/ooni/netem" "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/model" @@ -16,100 +15,52 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) -func newEnvironment() *netemx.Environment { +func newEnvironment() *netemx.QAEnv { + // configure DoH server + dohServer := &netemx.DoHServer{} + dohServer.AddRecord("ams-pg-test.ooni.org", "188.166.93.143") + dohServer.AddRecord("geoip.ubuntu.com", "185.125.188.132") + dohServer.AddRecord("www.example.com", "93.184.216.34") + dohServer.AddRecord("0.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("1.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("2.th.ooni.org", "104.248.30.161") + dohServer.AddRecord("3.th.ooni.org", "104.248.30.161") + + env := netemx.NewQAEnv( + netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), + netemx.QAEnvOptionHTTPServer("93.184.216.34", netemx.QAEnvDefaultHTTPHandler()), + netemx.QAEnvOptionHTTPServer("104.248.30.161", oohelperd.NewHandler()), + netemx.QAEnvOptionHTTPServer("104.16.248.249", dohServer), + netemx.QAEnvOptionHTTPServer("188.166.93.143", &probeService{}), + netemx.QAEnvOptionHTTPServer("185.125.188.132", &netemx.GeoIPLookup{}), + ) + // configure default UDP DNS server - dnsConfig := netem.NewDNSConfig() - dnsConfig.AddRecord( + env.AddRecordToAllResolvers( "dns.quad9.net", "dns.quad9.net", "104.16.248.249", ) - dnsConfig.AddRecord( + env.AddRecordToAllResolvers( "mozilla.cloudflare-dns.com", "mozilla.cloudflare-dns.com", "104.16.248.249", ) - dnsConfig.AddRecord( + env.AddRecordToAllResolvers( "dns.nextdns.io", "dns.nextdns.io", "104.16.248.249", ) - dnsConfig.AddRecord( + env.AddRecordToAllResolvers( "dns.google", "dns.google", "104.16.248.249", ) - dnsConfig.AddRecord( + env.AddRecordToAllResolvers( "www.example.com", "www.example.com", "93.184.216.34", ) - - // configure DoH server - dohServer := &netemx.DoHServer{} - dohServer.AddRecord("ams-pg-test.ooni.org", "188.166.93.143") - dohServer.AddRecord("geoip.ubuntu.com", "185.125.188.132") - dohServer.AddRecord("www.example.com", "93.184.216.34") - dohServer.AddRecord("0.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("1.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("2.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("3.th.ooni.org", "104.248.30.161") - - // client config with DNS server at 8.8.4.4 - clientConf := &netemx.ClientConfig{ - DNSConfig: dnsConfig, - ResolverAddr: "8.8.4.4", - } - - // servers config contains - serversConf := &netemx.ServersConfig{ - DNSConfig: dnsConfig, - Servers: []netemx.ConfigServerStack{ - { - ServerAddr: "93.184.216.34", - HTTPServers: []netemx.ConfigHTTPServer{{Port: 443}, {Port: 80}}, - }, - { - ServerAddr: "104.248.30.161", - HTTPServers: []netemx.ConfigHTTPServer{ - { - Port: 443, - Handler: oohelperd.NewHandler(), - }, - }, - }, - { - ServerAddr: "104.16.248.249", - HTTPServers: []netemx.ConfigHTTPServer{ - { - Port: 443, - Handler: dohServer, - }, - }, - }, - { - ServerAddr: "188.166.93.143", - HTTPServers: []netemx.ConfigHTTPServer{ - { - Port: 443, - Handler: &probeService{}, - }, - }, - }, - { - ServerAddr: "185.125.188.132", - HTTPServers: []netemx.ConfigHTTPServer{ - { - Port: 443, - Handler: &netemx.GeoIPLookup{}, - }, - }, - }, - }, - } - // create a new test environment - env := netemx.NewEnvironment(clientConf, serversConf) - return env } From 8c608e2f0929aedbdf1a7eff240c4b41b3187113 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 7 Jul 2023 10:30:06 +0200 Subject: [PATCH 08/25] webconnectivity: add (failing) DPITarget test --- .../webconnectivitylte/measurer_test.go | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index c72f495199..0ccf2d4c6b 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/apex/log" + "github.com/ooni/netem" "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/model" @@ -97,6 +98,44 @@ func TestSuccess(t *testing.T) { }) } +func TestDPITarget(t *testing.T) { + env := newEnvironment() + dpi := env.DPIEngine() + dpi.AddRule(&netem.DPIResetTrafficForTLSSNI{ + Logger: model.DiscardLogger, + SNI: "www.example.com", + }) + defer env.Close() + env.Do(func() { + measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) + ctx := context.Background() + // we need a real session because we need the web-connectivity helper + // as well as the ASN database + sess := newsession(t, true) + measurement := &model.Measurement{Input: "https://www.example.com"} + callbacks := model.NewPrinterCallbacks(log.Log) + args := &model.ExperimentArgs{ + Callbacks: callbacks, + Measurement: measurement, + Session: sess, + } + err := measurer.Run(ctx, args) + if err != nil { + t.Fatal(err) + } + tk := measurement.TestKeys.(*webconnectivitylte.TestKeys) + if tk.ControlFailure != nil { + t.Fatal("unexpected control_failure", *tk.ControlFailure) + } + if tk.DNSExperimentFailure != nil { + t.Fatal("unexpected dns_experiment_failure") + } + if tk.HTTPExperimentFailure == nil { + t.Fatal("expected an http_experiment_failure") + } + }) +} + type probeService struct{} type th struct { From 24401f062994eab51db278b923e6014a4c387277 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:00:21 +0200 Subject: [PATCH 09/25] feat: add optional underlying network to netxlite stdlib structs --- internal/netxlite/dialer.go | 7 ++ internal/netxlite/dialer_test.go | 17 +++++ internal/netxlite/dnsovergetaddrinfo.go | 10 +++ internal/netxlite/dnsovergetaddrinfo_test.go | 28 ++++++++ internal/netxlite/quic.go | 28 ++++++-- internal/netxlite/quic_test.go | 69 ++++++++++++++++++++ internal/netxlite/tls.go | 7 ++ internal/netxlite/tls_test.go | 65 ++++++++++++++++++ 8 files changed, 227 insertions(+), 4 deletions(-) diff --git a/internal/netxlite/dialer.go b/internal/netxlite/dialer.go index 6b0b84e4bc..c685d52c2c 100644 --- a/internal/netxlite/dialer.go +++ b/internal/netxlite/dialer.go @@ -148,6 +148,10 @@ func NewDialerWithoutResolver(dl model.DebugLogger, w ...model.DialerWrapper) mo type DialerSystem struct { // timeout is the OPTIONAL timeout (for testing). timeout time.Duration + + // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // If nil, we will use tproxySingleton() as underlying network. + underlying model.UnderlyingNetwork } var _ model.Dialer = &DialerSystem{} @@ -163,6 +167,9 @@ func (d *DialerSystem) configuredTimeout() time.Duration { } func (d *DialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if d.underlying != nil { + return d.underlying.DialContext(ctx, d.configuredTimeout(), network, address) + } return tproxySingleton().DialContext(ctx, d.configuredTimeout(), network, address) } diff --git a/internal/netxlite/dialer_test.go b/internal/netxlite/dialer_test.go index 5d6c9c8a6a..aa250191f9 100644 --- a/internal/netxlite/dialer_test.go +++ b/internal/netxlite/dialer_test.go @@ -134,6 +134,23 @@ func TestDialerSystem(t *testing.T) { t.Fatal("unable to enforce timeout") } }) + + t.Run("with custom underlying network", func(t *testing.T) { + expected := errors.New("mocked underlying network") + proxy := &mocks.UnderlyingNetwork{ + MockDialContext: func(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { + return nil, expected + }, + } + d := &DialerSystem{underlying: proxy} + conn, err := d.DialContext(context.Background(), "tcp", "dns.google:443") + if conn != nil { + t.Fatal("unexpected conn") + } + if err != expected { + t.Fatal("unexpected err") + } + }) }) } diff --git a/internal/netxlite/dnsovergetaddrinfo.go b/internal/netxlite/dnsovergetaddrinfo.go index 6ec3b7e166..613caf79d7 100644 --- a/internal/netxlite/dnsovergetaddrinfo.go +++ b/internal/netxlite/dnsovergetaddrinfo.go @@ -22,6 +22,10 @@ type dnsOverGetaddrinfoTransport struct { // (OPTIONAL) allows to mock the underlying getaddrinfo call testableLookupANY func(ctx context.Context, domain string) ([]string, string, error) + + // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // If nil, we will use tproxySingleton() as underlying network. + underlying model.UnderlyingNetwork } // NewDNSOverGetaddrinfoTransport creates a new dns-over-getaddrinfo transport. @@ -104,6 +108,9 @@ func (txp *dnsOverGetaddrinfoTransport) lookupfn() func(ctx context.Context, dom if txp.testableLookupANY != nil { return txp.testableLookupANY } + if txp.underlying != nil { + return txp.underlying.GetaddrinfoLookupANY + } return tproxySingleton().GetaddrinfoLookupANY } @@ -112,6 +119,9 @@ func (txp *dnsOverGetaddrinfoTransport) RequiresPadding() bool { } func (txp *dnsOverGetaddrinfoTransport) Network() string { + if txp.underlying != nil { + return txp.underlying.GetaddrinfoResolverNetwork() + } return tproxySingleton().GetaddrinfoResolverNetwork() } diff --git a/internal/netxlite/dnsovergetaddrinfo_test.go b/internal/netxlite/dnsovergetaddrinfo_test.go index 368d5d9a43..e9baf6de47 100644 --- a/internal/netxlite/dnsovergetaddrinfo_test.go +++ b/internal/netxlite/dnsovergetaddrinfo_test.go @@ -190,6 +190,34 @@ func TestDNSOverGetaddrinfo(t *testing.T) { }) } +func TestGetaddrinfoWithCustomUnderlyingNetwork(t *testing.T) { + expected := errors.New("mocked underlying network") + proxy := &mocks.UnderlyingNetwork{ + MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { + return nil, "", expected + }, + MockGetaddrinfoResolverNetwork: func() string { + return "mocked" + }, + } + txp := &dnsOverGetaddrinfoTransport{ + underlying: proxy, + } + encoder := &DNSEncoderMiekg{} + query := encoder.Encode("dns.google", dns.TypeANY, false) + ctx := context.Background() + resp, err := txp.RoundTrip(ctx, query) + if resp != nil { + t.Fatal("unexpected non-nil response") + } + if err != expected { + t.Fatal("unexpected error") + } + if txp.Network() != "mocked" { + t.Fatal("unexpected network string") + } +} + func TestDNSOverGetaddrinfoResponse(t *testing.T) { t.Run("Query works as intended", func(t *testing.T) { t.Run("when query is not nil", func(t *testing.T) { diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index b4fdf1f42a..90d7630e7f 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -23,12 +23,19 @@ func NewQUICListener() model.QUICListener { } // quicListenerStdlib is a QUICListener using the standard library. -type quicListenerStdlib struct{} +type quicListenerStdlib struct { + // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // If nil, we will use tproxySingleton() as underlying network. + underlying model.UnderlyingNetwork +} var _ model.QUICListener = &quicListenerStdlib{} // Listen implements QUICListener.Listen. func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) { + if qls.underlying != nil { + return qls.underlying.ListenUDP("udp", addr) + } return tproxySingleton().ListenUDP("udp", addr) } @@ -51,11 +58,17 @@ func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, err // that aggregates all the errors that occurred. func NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLogger, resolver model.Resolver, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { + baseDialer := &quicDialerQUICGo{ + QUICListener: listener, + } + return WrapQUICDialer(logger, resolver, baseDialer, wrappers...) +} + +func WrapQUICDialer(logger model.DebugLogger, resolver model.Resolver, + baseDialer model.QUICDialer, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { outDialer = &quicDialerErrWrapper{ QUICDialer: &quicDialerHandshakeCompleter{ - Dialer: &quicDialerQUICGo{ - QUICListener: listener, - }, + Dialer: baseDialer, }, } for _, wrapper := range wrappers { @@ -89,6 +102,10 @@ type quicDialerQUICGo struct { // QUICListener is the underlying QUICListener to use. QUICListener model.QUICListener + // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // If nil, we will use tproxySingleton() as underlying network. + underlying model.UnderlyingNetwork + // mockDialEarlyContext allows to mock quic.DialEarlyContext. mockDialEarlyContext func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, @@ -177,6 +194,9 @@ func (d *quicDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) * if config.RootCAs == nil { // See https://github.com/ooni/probe/issues/2413 for context config.RootCAs = tproxySingleton().DefaultCertPool() + if d.underlying != nil { + config.RootCAs = d.underlying.DefaultCertPool() + } } if len(config.NextProtos) <= 0 { switch port { diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index fd7911950a..8dbb6628f8 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -3,9 +3,12 @@ package netxlite import ( "context" "crypto/tls" + "crypto/x509" "errors" "io" "net" + "net/http" + "net/http/httptest" "strings" "testing" @@ -330,6 +333,72 @@ func TestQUICDialerQUICGo(t *testing.T) { }) } +func TestQUICDialerWithCustomUnderlyingNetwork(t *testing.T) { + tlsConf := &tls.Config{ServerName: "dns.google"} + qConf := &quic.Config{} + ctx := context.Background() + + t.Run("UDP listen", func(t *testing.T) { + expected := errors.New("mocked underlying network") + proxy := &mocks.UnderlyingNetwork{ + MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { + return nil, expected + }, + } + systemdialer := &quicDialerQUICGo{ + QUICListener: &quicListenerStdlib{underlying: proxy}, + } + qconn, err := systemdialer.DialContext(ctx, "8.8.8.8:443", tlsConf, qConf) + if qconn != nil { + t.Fatal("unexpected conn") + } + if err != expected { + t.Fatal("unexpected err") + } + }) + t.Run("DefaultCertPool", func(t *testing.T) { + srvr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(444) + })) + defer srvr.Close() + + expectedPool := x509.NewCertPool() + expectedPool.AddCert(srvr.Certificate()) + + // TODO(bassosimone): we need a more compact and ergonomic + // way of overriding the underlying network + proxy := &mocks.UnderlyingNetwork{ + MockDefaultCertPool: func() *x509.CertPool { + return expectedPool + }, + } + expected := errors.New("mocked") + var gotTLSConfig *tls.Config + systemdialer := &quicDialerQUICGo{ + QUICListener: &quicListenerStdlib{}, + underlying: proxy, + mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, + host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { + gotTLSConfig = tlsConfig + return nil, expected + }, + } + qconn, err := systemdialer.DialContext(ctx, "8.8.8.8:443", tlsConf, qConf) + if qconn != nil { + t.Fatal("unexpected conn, should be nil") + } + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if tlsConf.RootCAs != nil { + t.Fatal("tlsConf.RootCAs should still be nil") + } + if gotTLSConfig.RootCAs != expectedPool { + t.Fatal("gotTLSConfig.RootCAs has not been correctly set") + } + }) +} + func TestQUICDialerHandshakeCompleter(t *testing.T) { t.Run("DialContext", func(t *testing.T) { t.Run("in case of failure", func(t *testing.T) { diff --git a/internal/netxlite/tls.go b/internal/netxlite/tls.go index 9e824269e1..eca9032f41 100644 --- a/internal/netxlite/tls.go +++ b/internal/netxlite/tls.go @@ -187,6 +187,10 @@ type tlsHandshakerConfigurable struct { // Timeout is the OPTIONAL timeout imposed on the TLS handshake. If zero // or negative, we will use default timeout of 10 seconds. Timeout time.Duration + + // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // If nil, we will use tproxySingleton() as underlying network. + underlying model.UnderlyingNetwork } var _ model.TLSHandshaker = &tlsHandshakerConfigurable{} @@ -218,6 +222,9 @@ func (h *tlsHandshakerConfigurable) Handshake( config = config.Clone() // See https://github.com/ooni/probe/issues/2413 for context config.RootCAs = tproxySingleton().DefaultCertPool() + if h.underlying != nil { + config.RootCAs = h.underlying.DefaultCertPool() + } } tlsconn, err := h.newConn(conn, config) if err != nil { diff --git a/internal/netxlite/tls_test.go b/internal/netxlite/tls_test.go index 662a22fb9c..90768dc41b 100644 --- a/internal/netxlite/tls_test.go +++ b/internal/netxlite/tls_test.go @@ -3,6 +3,7 @@ package netxlite import ( "context" "crypto/tls" + "crypto/x509" "errors" "io" "net" @@ -274,6 +275,70 @@ func TestTLSHandshakerConfigurable(t *testing.T) { } }) + t.Run("sets root CA of custom proxy", func(t *testing.T) { + srvr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(444) + })) + defer srvr.Close() + + expectedPool := x509.NewCertPool() + expectedPool.AddCert(srvr.Certificate()) + + // TODO(bassosimone): we need a more compact and ergonomic + // way of overriding the underlying network + proxy := &mocks.UnderlyingNetwork{ + MockDefaultCertPool: func() *x509.CertPool { + return expectedPool + }, + } + expected := errors.New("mocked error") + var gotTLSConfig *tls.Config + handshaker := &tlsHandshakerConfigurable{ + NewConn: func(conn net.Conn, config *tls.Config) (TLSConn, error) { + gotTLSConfig = config + return &mocks.TLSConn{ + MockHandshakeContext: func(ctx context.Context) error { + return expected + }, + }, nil + }, + underlying: proxy, + } + ctx := context.Background() + config := &tls.Config{ServerName: "dns.google"} + conn := &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockRemoteAddr: func() net.Addr { + return &mocks.Addr{ + MockString: func() string { + return "8.8.8.8:443" + }, + MockNetwork: func() string { + return "tcp" + }, + } + }, + } + tlsConn, connState, err := handshaker.Handshake(ctx, conn, config) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if !reflect.ValueOf(connState).IsZero() { + t.Fatal("expected zero connState here") + } + if tlsConn != nil { + t.Fatal("expected nil tlsConn here") + } + if config.RootCAs != nil { + t.Fatal("config.RootCAs should still be nil") + } + if gotTLSConfig.RootCAs != expectedPool { + t.Fatal("gotTLSConfig.RootCAs has not been correctly set") + } + }) + t.Run("h.newConn fails", func(t *testing.T) { expected := errors.New("mocked error") handshaker := &tlsHandshakerConfigurable{ From 457117e64bceffb35bb47a01e5a016e342c3e61c Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:01:29 +0200 Subject: [PATCH 10/25] webconnectivitylte: analysiscore: comment for legacy analysisFlagTLSBlocking handling --- internal/experiment/webconnectivitylte/analysiscore.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/experiment/webconnectivitylte/analysiscore.go b/internal/experiment/webconnectivitylte/analysiscore.go index b68159f4c8..59b4b25906 100644 --- a/internal/experiment/webconnectivitylte/analysiscore.go +++ b/internal/experiment/webconnectivitylte/analysiscore.go @@ -121,6 +121,8 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { tk.BlockingFlags, tk.Accessible, tk.Blocking, ) + // assigning "http-failure" for both TLS and HTTP blocking is legacy + // because the spec does not consider the case of TLS based blocking case (tk.BlockingFlags & (analysisFlagTLSBlocking | analysisFlagHTTPBlocking)) != 0: tk.Blocking = "http-failure" tk.Accessible = false From 0f26164d86e4084e5d8910f9b41b8e65ee96c949 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:03:57 +0200 Subject: [PATCH 11/25] feat(netxlite): Net: constructors for operations on custom underlying network --- internal/netxlite/net.go | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 internal/netxlite/net.go diff --git a/internal/netxlite/net.go b/internal/netxlite/net.go new file mode 100644 index 0000000000..5bf6c40206 --- /dev/null +++ b/internal/netxlite/net.go @@ -0,0 +1,70 @@ +package netxlite + +// +// Net is a high-level structure that provides constructors for basic netxlite network operations +// using a custom Underlying Network. +// + +import ( + "github.com/ooni/probe-cli/v3/internal/model" +) + +// Net contains a [model.UnderlyingNetwork] to perform network operations. +type Net struct { + Underlying model.UnderlyingNetwork +} + +// NewStdlibResolver is like netxlite.NewStdlibResolver but +// the constructed resolver uses the given [UnderlyingNetwork]. +func (n *Net) NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver { + unwrapped := &resolverSystem{ + t: WrapDNSTransport(&dnsOverGetaddrinfoTransport{underlying: n.Underlying}, wrappers...), + } + return WrapResolver(logger, unwrapped) +} + +// NewDialerWithResolver is like netxlite.NewDialerWithResolver but +// the constructed dialer uses the given [UnderlyingNetwork]. +func (n *Net) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { + return WrapDialer(dl, r, &DialerSystem{underlying: n.Underlying}, w...) +} + +// NewQUICListener is like netxlite.NewQUICListener but +// the constructed listener uses the given [UnderlyingNetwork]. +func (n *Net) NewQUICListener() model.QUICListener { + return &quicListenerErrWrapper{&quicListenerStdlib{underlying: n.Underlying}} +} + +// NewQUICDialerWithResolver is like netxlite.NewQUICDialerWithResolver but +// the constructed QUIC dialer uses the given [UnderlyingNetwork]. +func (n *Net) NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLogger, + resolver model.Resolver, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { + baseDialer := &quicDialerQUICGo{ + QUICListener: listener, + underlying: n.Underlying, + } + return WrapQUICDialer(logger, resolver, baseDialer, wrappers...) +} + +// NewTLSHandshakerStdlib is like netxlite.NewTLSHandshakerStdlib but +// the constructed handshaker uses the given [UnderlyingNetwork]. +func (n *Net) NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { + return newTLSHandshakerLogger(&tlsHandshakerConfigurable{underlying: n.Underlying}, logger) +} + +// NewHTTPTransportStdlib is like netxlite.NewHTTPTransportStdlib but +// the constructed transport uses the given [UnderlyingNetwork]. +func (n *Net) NewHTTPTransportStdlib(logger model.DebugLogger) model.HTTPTransport { + dialer := n.NewDialerWithResolver(logger, n.NewStdlibResolver(logger)) + tlsDialer := NewTLSDialer(dialer, n.NewTLSHandshakerStdlib(logger)) + return NewHTTPTransport(logger, dialer, tlsDialer) +} + +// NewHTTP3TransportStdlib is like netxlite.NewHTTP3TransportStdlib but +// the constructed transport uses the given [UnderlyingNetwork]. +func (n *Net) NewHTTP3TransportStdlib(logger model.DebugLogger) model.HTTPTransport { + ql := n.NewQUICListener() + reso := n.NewStdlibResolver(logger) + qd := n.NewQUICDialerWithResolver(ql, logger, reso) + return NewHTTP3Transport(logger, qd, nil) +} From a65d6a51e5fd1f9cdb69051c2dc080434d82b839 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:18:27 +0200 Subject: [PATCH 12/25] feat(qaenv): make the environment's server stacks accessible, option to add handler after environment creation --- internal/netemx/qaenv.go | 76 +++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/internal/netemx/qaenv.go b/internal/netemx/qaenv.go index 7e9ce97895..bfd58ad540 100644 --- a/internal/netemx/qaenv.go +++ b/internal/netemx/qaenv.go @@ -81,7 +81,9 @@ func QAEnvOptionDNSOverUDPResolvers(ipAddrs ...string) QAEnvOption { // we will not create any HTTP server. func QAEnvOptionHTTPServer(ipAddr string, handler http.Handler) QAEnvOption { runtimex.Assert(net.ParseIP(ipAddr) != nil, "not an IP addr") - runtimex.Assert(handler != nil, "passed a nil handler") + // TODO: we might want to pass a nil handler first and add another one later + // (see: experiment/webconnectivitylte/measurer_test.go) + // runtimex.Assert(handler != nil, "passed a nil handler") return func(config *qaEnvConfig) { config.httpServers[ipAddr] = handler } @@ -113,6 +115,9 @@ type QAEnv struct { // clientStack is the client stack to use. clientStack *netem.UNetStack + // serverStacks are the server stacks to use. + serverStacks map[string]*netem.UNetStack + // ispResolverConfig is the DNS config used by the ISP resolver. ispResolverConfig *netem.DNSConfig @@ -132,6 +137,11 @@ type QAEnv struct { closables []io.Closer } +// GetServerStack returns the server stack at the given address. +func (env *QAEnv) GetServerStack(addr string) *netem.UNetStack { + return env.serverStacks[addr] +} + // NewQAEnv creates a new [QAEnv]. func NewQAEnv(options ...QAEnvOption) *QAEnv { // initialize the configuration @@ -159,6 +169,7 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { once: sync.Once{}, otherResolversConfig: netem.NewDNSConfig(), topology: runtimex.Try1(netem.NewStarTopology(config.logger)), + serverStacks: make(map[string]*netem.UNetStack), closables: []io.Closer{}, } @@ -171,6 +182,15 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { return env } +// AddHandler is used to add another handler at a given server stack after creating the environment. +// We need this option to use a server stack that has been created during NewQAEnv as an underlying +// network of a HTTP handler. +// (see: experiment/webconnectivitylte/measurer_test.go) +func (env *QAEnv) AddHandler(serverAddr string, handler http.Handler) { + serverStack := env.serverStacks[serverAddr] + env.closables = append(env.closables, env.serverListen(serverStack, handler, serverAddr)...) +} + func (env *QAEnv) mustNewISPResolverStack(config *qaEnvConfig) io.Closer { // Create the ISP's DNS server TCP/IP stack. // @@ -264,34 +284,42 @@ func (env *QAEnv) mustNewHTTPServers(config *qaEnvConfig) (closables []io.Closer RightToLeftDelay: time.Millisecond, }, )) + env.serverStacks[addr] = stack - ipAddr := net.ParseIP(addr) - runtimex.Assert(ipAddr != nil, "invalid IP addr") - - // listen for HTTP - { - listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 80})) - srv := &http.Server{Handler: handler} - closables = append(closables, srv) - go srv.Serve(listener) + if handler == nil { + continue } + closables = env.serverListen(stack, handler, addr) + } + return +} - // listen for HTTPS - { - listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 443})) - srv := &http.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} - closables = append(closables, srv) - go srv.ServeTLS(listener, "", "") - } +func (env *QAEnv) serverListen(stack *netem.UNetStack, handler http.Handler, addr string) (closables []io.Closer) { + ipAddr := net.ParseIP(addr) + runtimex.Assert(ipAddr != nil, "invalid IP addr") - // listen for HTTP3 - { - listener := runtimex.Try1(stack.ListenUDP("udp", &net.UDPAddr{IP: ipAddr, Port: 443})) - srv := &http3.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} - closables = append(closables, listener, srv) - go srv.Serve(listener) + // listen for HTTP + { + listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 80})) + srv := &http.Server{Handler: handler} + closables = append(closables, srv) + go srv.Serve(listener) + } - } + // listen for HTTPS + { + listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 443})) + srv := &http.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} + closables = append(closables, srv) + go srv.ServeTLS(listener, "", "") + } + + // listen for HTTP3 + { + listener := runtimex.Try1(stack.ListenUDP("udp", &net.UDPAddr{IP: ipAddr, Port: 443})) + srv := &http3.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} + closables = append(closables, listener, srv) + go srv.Serve(listener) } return } From 24439d6620833dffb15fb78e7af114b04c022160 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:18:54 +0200 Subject: [PATCH 13/25] feat(adapter): wrap given netem.UnderlyingNetwork --- internal/netemx/adapter.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/netemx/adapter.go b/internal/netemx/adapter.go index 02bfa036ae..5bd8880435 100644 --- a/internal/netemx/adapter.go +++ b/internal/netemx/adapter.go @@ -22,6 +22,11 @@ func WithCustomTProxy(tproxy netem.UnderlyingNetwork, function func()) { netxlite.WithCustomTProxy(&adapter{tproxy}, function) } +// GetCustomTProxy returns the tproxy wrapped by the adapter. +func GetCustomTProxy(tproxy netem.UnderlyingNetwork) model.UnderlyingNetwork { + return &adapter{tproxy} +} + // adapter adapts [netem.UnderlyingNetwork] to [model.UnderlyingNetwork]. type adapter struct { tp netem.UnderlyingNetwork From c5e67400efbb3d5b8240779e0f4a71be4c1a8e1e Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Sat, 15 Jul 2023 17:19:42 +0200 Subject: [PATCH 14/25] test(webconnectivitylte): use testhelper on concurrent server stack --- .../webconnectivitylte/measurer_test.go | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index 0ccf2d4c6b..5ab8b44947 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "net/http/cookiejar" "testing" "github.com/apex/log" @@ -12,8 +13,10 @@ import ( "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "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/oohelperd" "github.com/ooni/probe-cli/v3/internal/runtimex" + "golang.org/x/net/publicsuffix" ) func newEnvironment() *netemx.QAEnv { @@ -30,12 +33,17 @@ func newEnvironment() *netemx.QAEnv { env := netemx.NewQAEnv( netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), netemx.QAEnvOptionHTTPServer("93.184.216.34", netemx.QAEnvDefaultHTTPHandler()), - netemx.QAEnvOptionHTTPServer("104.248.30.161", oohelperd.NewHandler()), + netemx.QAEnvOptionHTTPServer("104.248.30.161", nil), netemx.QAEnvOptionHTTPServer("104.16.248.249", dohServer), netemx.QAEnvOptionHTTPServer("188.166.93.143", &probeService{}), netemx.QAEnvOptionHTTPServer("185.125.188.132", &netemx.GeoIPLookup{}), ) + // create new testhelper handler using the newly created server stack + underlyingStack := env.GetServerStack("104.248.30.161") + helperHandler := newTestHelper(underlyingStack) + env.AddHandler("104.248.30.161", helperHandler) + // configure default UDP DNS server env.AddRecordToAllResolvers( "dns.quad9.net", @@ -65,6 +73,48 @@ func newEnvironment() *netemx.QAEnv { return env } +func newTestHelper(underlying netem.UnderlyingNetwork) *oohelperd.Handler { + n := netxlite.Net{Underlying: netemx.GetCustomTProxy(underlying)} + helperHandler := oohelperd.NewHandler() + helperHandler.NewDialer = func(logger model.Logger) model.Dialer { + return n.NewDialerWithResolver(logger, n.NewStdlibResolver(logger)) + } + helperHandler.NewQUICDialer = func(logger model.Logger) model.QUICDialer { + return n.NewQUICDialerWithResolver( + n.NewQUICListener(), + logger, + n.NewStdlibResolver(logger), + ) + } + helperHandler.NewResolver = func(logger model.Logger) model.Resolver { + return n.NewStdlibResolver(logger) + } + + helperHandler.NewHTTPClient = func(logger model.Logger) model.HTTPClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{ + PublicSuffixList: publicsuffix.List, + }) + return &http.Client{ + Transport: n.NewHTTPTransportStdlib(logger), + CheckRedirect: nil, + Jar: cookieJar, + Timeout: 0, + } + } + helperHandler.NewHTTP3Client = func(logger model.Logger) model.HTTPClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{ + PublicSuffixList: publicsuffix.List, + }) + return &http.Client{ + Transport: n.NewHTTP3TransportStdlib(logger), + CheckRedirect: nil, + Jar: cookieJar, + Timeout: 0, + } + } + return helperHandler +} + func TestSuccess(t *testing.T) { env := newEnvironment() defer env.Close() @@ -89,11 +139,11 @@ func TestSuccess(t *testing.T) { if tk.ControlFailure != nil { t.Fatal("unexpected control_failure", *tk.ControlFailure) } - if tk.DNSExperimentFailure != nil { - t.Fatal("unexpected dns_experiment_failure", *tk.DNSExperimentFailure) + if tk.Blocking != false { + t.Fatal("unexpected blocking detected") } - if tk.HTTPExperimentFailure != nil { - t.Fatal("unexpected http_experiment_failure", *tk.HTTPExperimentFailure) + if tk.Accessible != true { + t.Fatal("unexpected accessible flag: should be accessible") } }) } @@ -127,11 +177,11 @@ func TestDPITarget(t *testing.T) { if tk.ControlFailure != nil { t.Fatal("unexpected control_failure", *tk.ControlFailure) } - if tk.DNSExperimentFailure != nil { - t.Fatal("unexpected dns_experiment_failure") + if tk.Blocking != "http-failure" { + t.Fatal("unexpected blocking type") } - if tk.HTTPExperimentFailure == nil { - t.Fatal("expected an http_experiment_failure") + if tk.Accessible == true { + t.Fatal("unexpected accessible flag: should be false") } }) } From ca42b775826ab72562558141070cf5e26b634594 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 21 Jul 2023 12:18:39 +0200 Subject: [PATCH 15/25] fix: add QAEnv.serverStacks --- internal/netemx/qaenv.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/netemx/qaenv.go b/internal/netemx/qaenv.go index d9314d6599..f831e25a00 100644 --- a/internal/netemx/qaenv.go +++ b/internal/netemx/qaenv.go @@ -144,6 +144,9 @@ type QAEnv struct { // clientStack is the client stack to use. clientStack *netem.UNetStack + // serverStacks are the server stacks to use. + serverStacks map[string]*netem.UNetStack + // closables contains all entities where we have to take care of closing. closables []io.Closer @@ -191,6 +194,7 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { env := &QAEnv{ clientNICWrapper: config.clientNICWrapper, clientStack: nil, + serverStacks: make(map[string]*netem.UNetStack), closables: []io.Closer{}, ispResolverConfig: netem.NewDNSConfig(), dpi: netem.NewDPIEngine(config.logger), From 66cff414413b02647541fb6bf93d968a1dc11651 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 21 Jul 2023 12:40:41 +0200 Subject: [PATCH 16/25] make underlying network mandatory for system dialers/resolver/handshaker --- internal/netxlite/dialer.go | 9 ++---- internal/netxlite/dialer_test.go | 16 +++++------ internal/netxlite/dnsovergetaddrinfo.go | 14 +++------ internal/netxlite/dnsovergetaddrinfo_test.go | 17 +++++++---- internal/netxlite/quic.go | 17 ++++------- internal/netxlite/quic_test.go | 30 +++++++++++++------- internal/netxlite/tls.go | 9 ++---- internal/netxlite/tls_test.go | 12 +++++--- internal/netxlite/utls.go | 3 +- 9 files changed, 65 insertions(+), 62 deletions(-) diff --git a/internal/netxlite/dialer.go b/internal/netxlite/dialer.go index c685d52c2c..31774b1ff2 100644 --- a/internal/netxlite/dialer.go +++ b/internal/netxlite/dialer.go @@ -25,7 +25,7 @@ func NewDialerWithStdlibResolver(dl model.DebugLogger) model.Dialer { // NewDialerWithResolver is equivalent to calling WrapDialer with // the dialer argument being equal to &DialerSystem{}. func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { - return WrapDialer(dl, r, &DialerSystem{}, w...) + return WrapDialer(dl, r, &DialerSystem{underlying: tproxySingleton()}, w...) } // WrapDialer wraps an existing Dialer to add extra functionality @@ -149,7 +149,7 @@ type DialerSystem struct { // timeout is the OPTIONAL timeout (for testing). timeout time.Duration - // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // underlying is the MANDATORY custom [UnderlyingNetwork]. // If nil, we will use tproxySingleton() as underlying network. underlying model.UnderlyingNetwork } @@ -167,10 +167,7 @@ func (d *DialerSystem) configuredTimeout() time.Duration { } func (d *DialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if d.underlying != nil { - return d.underlying.DialContext(ctx, d.configuredTimeout(), network, address) - } - return tproxySingleton().DialContext(ctx, d.configuredTimeout(), network, address) + return d.underlying.DialContext(ctx, d.configuredTimeout(), network, address) } func (d *DialerSystem) CloseIdleConnections() { diff --git a/internal/netxlite/dialer_test.go b/internal/netxlite/dialer_test.go index aa250191f9..264fb7c961 100644 --- a/internal/netxlite/dialer_test.go +++ b/internal/netxlite/dialer_test.go @@ -82,7 +82,7 @@ func TestNewDialer(t *testing.T) { func TestDialerSystem(t *testing.T) { t.Run("has a default timeout", func(t *testing.T) { - d := &DialerSystem{} + d := &DialerSystem{underlying: tproxySingleton()} timeout := d.configuredTimeout() if timeout != dialerDefaultTimeout { t.Fatal("unexpected default timeout") @@ -91,7 +91,7 @@ func TestDialerSystem(t *testing.T) { t.Run("we can change the timeout for testing", func(t *testing.T) { const smaller = 1 * time.Second - d := &DialerSystem{timeout: smaller} + d := &DialerSystem{timeout: smaller, underlying: tproxySingleton()} timeout := d.configuredTimeout() if timeout != smaller { t.Fatal("unexpected timeout") @@ -99,13 +99,13 @@ func TestDialerSystem(t *testing.T) { }) t.Run("CloseIdleConnections", func(t *testing.T) { - d := &DialerSystem{} + d := &DialerSystem{underlying: tproxySingleton()} d.CloseIdleConnections() // to avoid missing coverage }) t.Run("DialContext", func(t *testing.T) { t.Run("with canceled context", func(t *testing.T) { - d := &DialerSystem{} + d := &DialerSystem{underlying: tproxySingleton()} ctx, cancel := context.WithCancel(context.Background()) cancel() // immediately! conn, err := d.DialContext(ctx, "tcp", "8.8.8.8:443") @@ -119,7 +119,7 @@ func TestDialerSystem(t *testing.T) { t.Run("enforces the configured timeout", func(t *testing.T) { const timeout = 1 * time.Nanosecond - d := &DialerSystem{timeout: timeout} + d := &DialerSystem{timeout: timeout, underlying: tproxySingleton()} ctx := context.Background() start := time.Now() conn, err := d.DialContext(ctx, "tcp", "dns.google:443") @@ -158,7 +158,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("DialContext", func(t *testing.T) { t.Run("fails without a port", func(t *testing.T) { d := &dialerResolverWithTracing{ - Dialer: &DialerSystem{}, + Dialer: &DialerSystem{underlying: tproxySingleton()}, Resolver: NewUnwrappedStdlibResolver(), } const missingPort = "ooni.nu" @@ -505,7 +505,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("lookupHost", func(t *testing.T) { t.Run("handles addresses correctly", func(t *testing.T) { dialer := &dialerResolverWithTracing{ - Dialer: &DialerSystem{}, + Dialer: &DialerSystem{underlying: tproxySingleton()}, Resolver: &NullResolver{}, } addrs, err := dialer.lookupHost(context.Background(), "1.1.1.1") @@ -519,7 +519,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("fails correctly on lookup error", func(t *testing.T) { dialer := &dialerResolverWithTracing{ - Dialer: &DialerSystem{}, + Dialer: &DialerSystem{underlying: tproxySingleton()}, Resolver: &NullResolver{}, } ctx := context.Background() diff --git a/internal/netxlite/dnsovergetaddrinfo.go b/internal/netxlite/dnsovergetaddrinfo.go index 613caf79d7..6df347b1f4 100644 --- a/internal/netxlite/dnsovergetaddrinfo.go +++ b/internal/netxlite/dnsovergetaddrinfo.go @@ -23,14 +23,14 @@ type dnsOverGetaddrinfoTransport struct { // (OPTIONAL) allows to mock the underlying getaddrinfo call testableLookupANY func(ctx context.Context, domain string) ([]string, string, error) - // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // underlying is the MANDATORY custom [UnderlyingNetwork]. // If nil, we will use tproxySingleton() as underlying network. underlying model.UnderlyingNetwork } // NewDNSOverGetaddrinfoTransport creates a new dns-over-getaddrinfo transport. func NewDNSOverGetaddrinfoTransport() model.DNSTransport { - return &dnsOverGetaddrinfoTransport{} + return &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} } var _ model.DNSTransport = &dnsOverGetaddrinfoTransport{} @@ -108,10 +108,7 @@ func (txp *dnsOverGetaddrinfoTransport) lookupfn() func(ctx context.Context, dom if txp.testableLookupANY != nil { return txp.testableLookupANY } - if txp.underlying != nil { - return txp.underlying.GetaddrinfoLookupANY - } - return tproxySingleton().GetaddrinfoLookupANY + return txp.underlying.GetaddrinfoLookupANY } func (txp *dnsOverGetaddrinfoTransport) RequiresPadding() bool { @@ -119,10 +116,7 @@ func (txp *dnsOverGetaddrinfoTransport) RequiresPadding() bool { } func (txp *dnsOverGetaddrinfoTransport) Network() string { - if txp.underlying != nil { - return txp.underlying.GetaddrinfoResolverNetwork() - } - return tproxySingleton().GetaddrinfoResolverNetwork() + return txp.underlying.GetaddrinfoResolverNetwork() } func (txp *dnsOverGetaddrinfoTransport) Address() string { diff --git a/internal/netxlite/dnsovergetaddrinfo_test.go b/internal/netxlite/dnsovergetaddrinfo_test.go index e9baf6de47..418f5f5449 100644 --- a/internal/netxlite/dnsovergetaddrinfo_test.go +++ b/internal/netxlite/dnsovergetaddrinfo_test.go @@ -15,40 +15,40 @@ import ( func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("RequiresPadding", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} if txp.RequiresPadding() { t.Fatal("expected false") } }) t.Run("Network", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} if txp.Network() != getaddrinfoResolverNetwork() { t.Fatal("unexpected Network") } }) t.Run("Address", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} if txp.Address() != "" { t.Fatal("unexpected Address") } }) t.Run("CloseIdleConnections", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} txp.CloseIdleConnections() // does not crash }) t.Run("check default timeout", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} if txp.timeout() != 15*time.Second { t.Fatal("unexpected default timeout") } }) t.Run("check default lookup host func not nil", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{} + txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} if txp.lookupfn() == nil { t.Fatal("expected non-nil func here") } @@ -57,6 +57,7 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("RoundTrip", func(t *testing.T) { t.Run("with invalid query type", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ + underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return []string{"8.8.8.8"}, "dns.google", nil }, @@ -75,6 +76,7 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("with success", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ + underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return []string{"8.8.8.8"}, "dns.google", nil }, @@ -123,6 +125,7 @@ func TestDNSOverGetaddrinfo(t *testing.T) { wg.Add(1) done := make(chan interface{}) txp := &dnsOverGetaddrinfoTransport{ + underlying: tproxySingleton(), testableTimeout: 1 * time.Microsecond, testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { defer wg.Done() @@ -149,6 +152,7 @@ func TestDNSOverGetaddrinfo(t *testing.T) { wg.Add(1) done := make(chan interface{}) txp := &dnsOverGetaddrinfoTransport{ + underlying: tproxySingleton(), testableTimeout: 1 * time.Microsecond, testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { defer wg.Done() @@ -172,6 +176,7 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("with NXDOMAIN", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ + underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return nil, "", ErrOODNSNoSuchHost }, diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index 90d7630e7f..eaea8fd909 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -19,12 +19,12 @@ import ( // NewQUICListener creates a new QUICListener using the standard // library to create listening UDP sockets. func NewQUICListener() model.QUICListener { - return &quicListenerErrWrapper{&quicListenerStdlib{}} + return &quicListenerErrWrapper{&quicListenerStdlib{underlying: tproxySingleton()}} } // quicListenerStdlib is a QUICListener using the standard library. type quicListenerStdlib struct { - // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // underlying is the MANDATORY custom [UnderlyingNetwork]. // If nil, we will use tproxySingleton() as underlying network. underlying model.UnderlyingNetwork } @@ -33,10 +33,7 @@ var _ model.QUICListener = &quicListenerStdlib{} // Listen implements QUICListener.Listen. func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) { - if qls.underlying != nil { - return qls.underlying.ListenUDP("udp", addr) - } - return tproxySingleton().ListenUDP("udp", addr) + return qls.underlying.ListenUDP("udp", addr) } // NewQUICDialerWithResolver is the WrapDialer equivalent for QUIC where @@ -60,6 +57,7 @@ func NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLo resolver model.Resolver, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { baseDialer := &quicDialerQUICGo{ QUICListener: listener, + underlying: tproxySingleton(), } return WrapQUICDialer(logger, resolver, baseDialer, wrappers...) } @@ -102,7 +100,7 @@ type quicDialerQUICGo struct { // QUICListener is the underlying QUICListener to use. QUICListener model.QUICListener - // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // underlying is the MANDATORY custom [UnderlyingNetwork]. // If nil, we will use tproxySingleton() as underlying network. underlying model.UnderlyingNetwork @@ -193,10 +191,7 @@ func (d *quicDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) * config = config.Clone() if config.RootCAs == nil { // See https://github.com/ooni/probe/issues/2413 for context - config.RootCAs = tproxySingleton().DefaultCertPool() - if d.underlying != nil { - config.RootCAs = d.underlying.DefaultCertPool() - } + config.RootCAs = d.underlying.DefaultCertPool() } if len(config.NextProtos) <= 0 { switch port { diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index 8dbb6628f8..ab47fc6c21 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -132,7 +132,8 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + underlying: tproxySingleton(), } defer systemdialer.CloseIdleConnections() // just to see it running ctx := context.Background() @@ -151,7 +152,8 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -169,7 +171,8 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -193,6 +196,7 @@ func TestQUICDialerQUICGo(t *testing.T) { return nil, expected }, }, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -210,7 +214,8 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + underlying: tproxySingleton(), } ctx, cancel := context.WithCancel(context.Background()) cancel() // fail immediately @@ -231,13 +236,14 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { gotTLSConfig = tlsConfig return nil, expected }, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -272,13 +278,14 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { gotTLSConfig = tlsConfig return nil, expected }, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -312,12 +319,13 @@ func TestQUICDialerQUICGo(t *testing.T) { } fakeconn := &mocks.QUICEarlyConnection{} systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { return fakeconn, nil }, + underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -347,6 +355,7 @@ func TestQUICDialerWithCustomUnderlyingNetwork(t *testing.T) { } systemdialer := &quicDialerQUICGo{ QUICListener: &quicListenerStdlib{underlying: proxy}, + underlying: tproxySingleton(), } qconn, err := systemdialer.DialContext(ctx, "8.8.8.8:443", tlsConf, qConf) if qconn != nil { @@ -375,7 +384,7 @@ func TestQUICDialerWithCustomUnderlyingNetwork(t *testing.T) { expected := errors.New("mocked") var gotTLSConfig *tls.Config systemdialer := &quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, underlying: proxy, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { @@ -556,7 +565,7 @@ func TestQUICDialerResolver(t *testing.T) { tlsConfig := &tls.Config{} dialer := &quicDialerResolver{ Resolver: NewStdlibResolver(log.Log), - Dialer: &quicDialerQUICGo{}} + Dialer: &quicDialerQUICGo{underlying: tproxySingleton()}} qconn, err := dialer.DialContext( context.Background(), "www.google.com", tlsConfig, &quic.Config{}) @@ -594,7 +603,8 @@ func TestQUICDialerResolver(t *testing.T) { dialer := &quicDialerResolver{ Resolver: NewStdlibResolver(log.Log), Dialer: &quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{}, + QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + underlying: tproxySingleton(), }} qconn, err := dialer.DialContext( context.Background(), "8.8.4.4:x", diff --git a/internal/netxlite/tls.go b/internal/netxlite/tls.go index eca9032f41..501b4f4368 100644 --- a/internal/netxlite/tls.go +++ b/internal/netxlite/tls.go @@ -164,7 +164,7 @@ var _ TLSConn = &tls.Conn{} // 3. that we are going to use Mozilla CA if the [tls.Config] // RootCAs field is zero initialized. func NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { - return newTLSHandshakerLogger(&tlsHandshakerConfigurable{}, logger) + return newTLSHandshakerLogger(&tlsHandshakerConfigurable{underlying: tproxySingleton()}, logger) } // newTLSHandshakerLogger creates a new tlsHandshakerLogger instance. @@ -188,7 +188,7 @@ type tlsHandshakerConfigurable struct { // or negative, we will use default timeout of 10 seconds. Timeout time.Duration - // underlying is the OPTIONAL custom [UnderlyingNetwork]. + // underlying is the MANDATORY custom [UnderlyingNetwork]. // If nil, we will use tproxySingleton() as underlying network. underlying model.UnderlyingNetwork } @@ -221,10 +221,7 @@ func (h *tlsHandshakerConfigurable) Handshake( if config.RootCAs == nil { config = config.Clone() // See https://github.com/ooni/probe/issues/2413 for context - config.RootCAs = tproxySingleton().DefaultCertPool() - if h.underlying != nil { - config.RootCAs = h.underlying.DefaultCertPool() - } + config.RootCAs = h.underlying.DefaultCertPool() } tlsconn, err := h.newConn(conn, config) if err != nil { diff --git a/internal/netxlite/tls_test.go b/internal/netxlite/tls_test.go index 90768dc41b..a8e63e070b 100644 --- a/internal/netxlite/tls_test.go +++ b/internal/netxlite/tls_test.go @@ -137,7 +137,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { t.Run("Handshake", func(t *testing.T) { t.Run("with handshake I/O error", func(t *testing.T) { var times []time.Time - h := &tlsHandshakerConfigurable{} + h := &tlsHandshakerConfigurable{underlying: tproxySingleton()} tcpConn := &mocks.Conn{ MockWrite: func(b []byte) (int, error) { return 0, io.EOF @@ -209,7 +209,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { t.Fatal(err) } defer conn.Close() - handshaker := &tlsHandshakerConfigurable{} + handshaker := &tlsHandshakerConfigurable{underlying: tproxySingleton()} ctx := context.Background() config := &tls.Config{ InsecureSkipVerify: true, @@ -239,6 +239,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { }, }, nil }, + underlying: tproxySingleton(), } ctx := context.Background() config := &tls.Config{} @@ -345,6 +346,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { NewConn: func(conn net.Conn, config *tls.Config) (TLSConn, error) { return nil, expected }, + underlying: tproxySingleton(), } ctx := context.Background() config := &tls.Config{} @@ -711,7 +713,7 @@ func TestTLSDialer(t *testing.T) { t.Run("failure dialing", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // immediately fail - dialer := tlsDialer{Dialer: &DialerSystem{}} + dialer := tlsDialer{Dialer: &DialerSystem{underlying: tproxySingleton()}} conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443") if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") { t.Fatal("not the error we expected", err) @@ -743,7 +745,9 @@ func TestTLSDialer(t *testing.T) { } }}, nil }}, - TLSHandshaker: &tlsHandshakerConfigurable{}, + TLSHandshaker: &tlsHandshakerConfigurable{ + underlying: tproxySingleton(), + }, } conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443") if !errors.Is(err, io.EOF) { diff --git a/internal/netxlite/utls.go b/internal/netxlite/utls.go index dd45d31c7a..620a63d55e 100644 --- a/internal/netxlite/utls.go +++ b/internal/netxlite/utls.go @@ -33,7 +33,8 @@ import ( // Passing a nil `id` will make this function panic. func NewTLSHandshakerUTLS(logger model.DebugLogger, id *utls.ClientHelloID) model.TLSHandshaker { return newTLSHandshakerLogger(&tlsHandshakerConfigurable{ - NewConn: newUTLSConnFactory(id), + NewConn: newUTLSConnFactory(id), + underlying: tproxySingleton(), }, logger) } From 617a52971eedcbf41719d5a06aadce544e49cd17 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 21 Jul 2023 13:51:36 +0200 Subject: [PATCH 17/25] remove package webconnectivitylte: use mocks.Session, revert exposing webconnectivitylte fields --- .../webconnectivitylte/cleartextflow.go | 2 +- .../webconnectivitylte/cleartextflow_test.go | 6 +- .../experiment/webconnectivitylte/control.go | 6 +- .../webconnectivitylte/dnsresolvers.go | 6 +- .../webconnectivitylte/ipfiltering.go | 8 +- .../webconnectivitylte/ipfiltering_test.go | 10 +- .../webconnectivitylte/measurer_test.go | 106 ++++++++++++------ .../experiment/webconnectivitylte/priority.go | 22 ++-- .../experiment/webconnectivitylte/qa_test.go | 5 +- .../webconnectivitylte/secureflow.go | 2 +- .../webconnectivitylte/secureflow_test.go | 17 ++- 11 files changed, 114 insertions(+), 76 deletions(-) diff --git a/internal/experiment/webconnectivitylte/cleartextflow.go b/internal/experiment/webconnectivitylte/cleartextflow.go index 02f20fec39..fdb20e281b 100644 --- a/internal/experiment/webconnectivitylte/cleartextflow.go +++ b/internal/experiment/webconnectivitylte/cleartextflow.go @@ -63,7 +63,7 @@ type CleartextFlow struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether this flow is allowed to fetch the webpage. - PrioSelector *PrioritySelector + PrioSelector *prioritySelector // Referer contains the OPTIONAL referer, used for redirects. Referer string diff --git a/internal/experiment/webconnectivitylte/cleartextflow_test.go b/internal/experiment/webconnectivitylte/cleartextflow_test.go index 82bfa0586b..39564a6b50 100644 --- a/internal/experiment/webconnectivitylte/cleartextflow_test.go +++ b/internal/experiment/webconnectivitylte/cleartextflow_test.go @@ -25,7 +25,7 @@ func TestCleartextFlow_Run(t *testing.T) { CookieJar http.CookieJar FollowRedirects bool HostHeader string - PrioSelector *PrioritySelector + PrioSelector *prioritySelector Referer string UDPAddress string URLPath string @@ -50,7 +50,7 @@ func TestCleartextFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "with loopback IPv6 endpoint", fields: fields{ @@ -61,7 +61,7 @@ func TestCleartextFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/experiment/webconnectivitylte/control.go b/internal/experiment/webconnectivitylte/control.go index 5c20eabf1a..d691532376 100644 --- a/internal/experiment/webconnectivitylte/control.go +++ b/internal/experiment/webconnectivitylte/control.go @@ -21,10 +21,10 @@ import ( type EndpointMeasurementsStarter interface { // startCleartextFlows starts a TCP measurement flow for each IP addr. The [ps] // argument determines whether this flow will be allowed to fetch the webpage. - startCleartextFlows(ctx context.Context, ps *PrioritySelector, addresses []DNSEntry) + startCleartextFlows(ctx context.Context, ps *prioritySelector, addresses []DNSEntry) // startSecureFlows is like startCleartextFlows but for HTTPS. - startSecureFlows(ctx context.Context, ps *PrioritySelector, addresses []DNSEntry) + startSecureFlows(ctx context.Context, ps *prioritySelector, addresses []DNSEntry) } // Control issues a Control request and saves the results @@ -45,7 +45,7 @@ type Control struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether we will be allowed to fetch the webpage. - PrioSelector *PrioritySelector + PrioSelector *prioritySelector // TestKeys is MANDATORY and contains the TestKeys. TestKeys *TestKeys diff --git a/internal/experiment/webconnectivitylte/dnsresolvers.go b/internal/experiment/webconnectivitylte/dnsresolvers.go index 828b558da3..92e6000eb7 100644 --- a/internal/experiment/webconnectivitylte/dnsresolvers.go +++ b/internal/experiment/webconnectivitylte/dnsresolvers.go @@ -418,7 +418,7 @@ func (t *DNSResolvers) dohSplitQueries( // startCleartextFlows starts a TCP measurement flow for each IP addr. func (t *DNSResolvers) startCleartextFlows( ctx context.Context, - ps *PrioritySelector, + ps *prioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "http" { @@ -456,7 +456,7 @@ func (t *DNSResolvers) startCleartextFlows( // startSecureFlows starts a TCP+TLS measurement flow for each IP addr. func (t *DNSResolvers) startSecureFlows( ctx context.Context, - ps *PrioritySelector, + ps *prioritySelector, addresses []DNSEntry, ) { if t.URL.Scheme != "https" { @@ -500,7 +500,7 @@ func (t *DNSResolvers) startSecureFlows( // maybeStartControlFlow starts the control flow iff .Session and .TestHelpers are set. func (t *DNSResolvers) maybeStartControlFlow( ctx context.Context, - ps *PrioritySelector, + ps *prioritySelector, addresses []DNSEntry, ) { // note: for subsequent requests we don't set .Session and .TestHelpers hence diff --git a/internal/experiment/webconnectivitylte/ipfiltering.go b/internal/experiment/webconnectivitylte/ipfiltering.go index 0d0fe94083..291b737367 100644 --- a/internal/experiment/webconnectivitylte/ipfiltering.go +++ b/internal/experiment/webconnectivitylte/ipfiltering.go @@ -12,15 +12,15 @@ import ( "github.com/ooni/probe-cli/v3/internal/netxlite" ) -// ErrNotAllowedToConnect indicates we're not allowed to connect. -var ErrNotAllowedToConnect = errors.New("webconnectivity: not allowed to connect") +// errNotAllowedToConnect indicates we're not allowed to connect. +var errNotAllowedToConnect = errors.New("webconnectivity: not allowed to connect") // allowedToConnect returns nil if we can connect to a given endpoint. Otherwise // it returns an error explaining why we cannot connect. func allowedToConnect(endpoint string) error { addr, _, err := net.SplitHostPort(endpoint) if err != nil { - return fmt.Errorf("%w: %s", ErrNotAllowedToConnect, err.Error()) + return fmt.Errorf("%w: %s", errNotAllowedToConnect, err.Error()) } // Implementation note: we don't remove bogons because accessing // them can lead us to discover block pages. This may change in @@ -29,7 +29,7 @@ func allowedToConnect(endpoint string) error { // We prevent connecting to localhost, however, as documented // inside https://github.com/ooni/probe/issues/2397. if netxlite.IsLoopback(addr) { - return fmt.Errorf("%w: is loopback", ErrNotAllowedToConnect) + return fmt.Errorf("%w: is loopback", errNotAllowedToConnect) } return nil } diff --git a/internal/experiment/webconnectivitylte/ipfiltering_test.go b/internal/experiment/webconnectivitylte/ipfiltering_test.go index 7036ff0b86..af33761282 100644 --- a/internal/experiment/webconnectivitylte/ipfiltering_test.go +++ b/internal/experiment/webconnectivitylte/ipfiltering_test.go @@ -13,19 +13,19 @@ func Test_allowedToConnect(t *testing.T) { }{{ name: "we cannot connect when there's no port", endpoint: "8.8.4.4", - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "we cannot connect when address is a domain (should not happen)", endpoint: "dns.google:443", - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "we cannot connect for IPv4 loopback 127.0.0.1", endpoint: "127.0.0.1:443", - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "we cannot connect for IPv4 loopback 127.0.0.2", endpoint: "127.0.0.2:443", - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "we can connect to 10.0.0.1 (may change in the future)", endpoint: "10.0.0.1:443", @@ -33,7 +33,7 @@ func Test_allowedToConnect(t *testing.T) { }, { name: "we cannot connect for IPv6 loopback", endpoint: "::1", - want: ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "we can connect for public IPv4 address", endpoint: "8.8.8.8:443", diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index 5ab8b44947..d6d0b4e9c9 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -1,4 +1,4 @@ -package webconnectivitylte_test +package webconnectivitylte import ( "context" @@ -9,13 +9,15 @@ import ( "github.com/apex/log" "github.com/ooni/netem" - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" + "github.com/ooni/probe-cli/v3/internal/bytecounter" + "github.com/ooni/probe-cli/v3/internal/kvstore" + "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/oohelperd" "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/sessionresolver" "golang.org/x/net/publicsuffix" ) @@ -119,11 +121,9 @@ func TestSuccess(t *testing.T) { env := newEnvironment() defer env.Close() env.Do(func() { - measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) + measurer := NewExperimentMeasurer(&Config{}) ctx := context.Background() - // we need a real session because we need the web-connectivity helper - // as well as the ASN database - sess := newsession(t, true) + sess := newSession() measurement := &model.Measurement{Input: "https://www.example.com"} callbacks := model.NewPrinterCallbacks(log.Log) args := &model.ExperimentArgs{ @@ -135,7 +135,7 @@ func TestSuccess(t *testing.T) { if err != nil { t.Fatal(err) } - tk := measurement.TestKeys.(*webconnectivitylte.TestKeys) + tk := measurement.TestKeys.(*TestKeys) if tk.ControlFailure != nil { t.Fatal("unexpected control_failure", *tk.ControlFailure) } @@ -157,11 +157,9 @@ func TestDPITarget(t *testing.T) { }) defer env.Close() env.Do(func() { - measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) + measurer := NewExperimentMeasurer(&Config{}) ctx := context.Background() - // we need a real session because we need the web-connectivity helper - // as well as the ASN database - sess := newsession(t, true) + sess := newSession() measurement := &model.Measurement{Input: "https://www.example.com"} callbacks := model.NewPrinterCallbacks(log.Log) args := &model.ExperimentArgs{ @@ -173,7 +171,7 @@ func TestDPITarget(t *testing.T) { if err != nil { t.Fatal(err) } - tk := measurement.TestKeys.(*webconnectivitylte.TestKeys) + tk := measurement.TestKeys.(*TestKeys) if tk.ControlFailure != nil { t.Fatal("unexpected control_failure", *tk.ControlFailure) } @@ -220,26 +218,68 @@ func (p *probeService) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(data) } -func newsession(t *testing.T, lookupBackends bool) model.ExperimentSession { - sess, err := engine.NewSession(context.Background(), engine.SessionConfig{ - AvailableProbeServices: []model.OOAPIService{{ - Address: "https://ams-pg-test.ooni.org", - Type: "https", - }}, - Logger: log.Log, - SoftwareName: "ooniprobe-engine", - SoftwareVersion: "0.0.1", - }) - if err != nil { - t.Fatal(err) +// newSession creates a new [mocks.Session]. +func newSession() model.ExperimentSession { + byteCounter := bytecounter.New() + resolver := &sessionresolver.Resolver{ + ByteCounter: byteCounter, + KVStore: &kvstore.Memory{}, + Logger: log.Log, + ProxyURL: nil, } - if lookupBackends { - if err := sess.MaybeLookupBackends(); err != nil { - t.Fatal(err) - } - } - if err := sess.MaybeLookupLocation(); err != nil { - t.Fatal(err) + txp := netxlite.NewHTTPTransportWithLoggerResolverAndOptionalProxyURL( + log.Log, resolver, nil, + ) + txp = bytecounter.WrapHTTPTransport(txp, byteCounter) + return &mocks.Session{ + MockGetTestHelpersByName: func(name string) ([]model.OOAPIService, bool) { + output := []model.OOAPIService{ + { + Address: "https://3.th.ooni.org", + Type: "https", + }, + { + Address: "https://2.th.ooni.org", + Type: "https", + }, + { + Address: "https://1.th.ooni.org", + Type: "https", + }, + { + Address: "https://0.th.ooni.org", + Type: "https", + }, + } + return output, true + }, + MockDefaultHTTPClient: func() model.HTTPClient { + return &http.Client{Transport: txp} + }, + MockFetchPsiphonConfig: nil, + MockFetchTorTargets: nil, + MockKeyValueStore: nil, + MockLogger: func() model.Logger { + return log.Log + }, + MockMaybeResolverIP: nil, + MockProbeASNString: nil, + MockProbeCC: nil, + MockProbeIP: nil, + MockProbeNetworkName: nil, + MockProxyURL: nil, + MockResolverIP: nil, + MockSoftwareName: nil, + MockSoftwareVersion: nil, + MockTempDir: nil, + MockTorArgs: nil, + MockTorBinary: nil, + MockTunnelDir: nil, + MockUserAgent: func() string { + return model.HTTPHeaderUserAgent + }, + MockNewExperimentBuilder: nil, + MockNewSubmitter: nil, + MockCheckIn: nil, } - return sess } diff --git a/internal/experiment/webconnectivitylte/priority.go b/internal/experiment/webconnectivitylte/priority.go index cf883a430d..c86a4cce90 100644 --- a/internal/experiment/webconnectivitylte/priority.go +++ b/internal/experiment/webconnectivitylte/priority.go @@ -41,8 +41,8 @@ import ( // because another goroutine have already fetched that webpage. var errNotPermittedToFetch = errors.New("webconnectivity: not permitted to fetch") -// PrioritySelector selects the connection with the highest priority. -type PrioritySelector struct { +// prioritySelector selects the connection with the highest priority. +type prioritySelector struct { // ch is the channel used to ask for priority ch chan *priorityRequest @@ -78,15 +78,15 @@ type priorityRequest struct { resp chan bool } -// newPrioritySelector creates a new PrioritySelector instance. +// newPrioritySelector creates a new prioritySelector instance. func newPrioritySelector( ctx context.Context, zeroTime time.Time, tk *TestKeys, logger model.Logger, addrs []DNSEntry, -) *PrioritySelector { - ps := &PrioritySelector{ +) *prioritySelector { + ps := &prioritySelector{ ch: make(chan *priorityRequest), logger: logger, m: map[string]int64{}, @@ -115,17 +115,17 @@ func newPrioritySelector( } // log formats and emits a ConnPriorityLogEntry -func (ps *PrioritySelector) log(format string, v ...any) { +func (ps *prioritySelector) log(format string, v ...any) { ps.tk.AppendConnPriorityLogEntry(&ConnPriorityLogEntry{ Msg: fmt.Sprintf(format, v...), T: time.Since(ps.zeroTime).Seconds(), }) - ps.logger.Infof("PrioritySelector: "+format, v...) + ps.logger.Infof("prioritySelector: "+format, v...) } // permissionToFetch returns whether this ready-to-use connection // is permitted to perform a round trip and fetch the webpage. -func (ps *PrioritySelector) permissionToFetch(address string) bool { +func (ps *prioritySelector) permissionToFetch(address string) bool { ipAddr, _, err := net.SplitHostPort(address) runtimex.PanicOnError(err, "net.SplitHostPort failed") r := &priorityRequest{ @@ -153,7 +153,7 @@ func (ps *PrioritySelector) permissionToFetch(address string) bool { // background goroutine and terminates when [ctx] is done. // // This function implements https://github.com/ooni/probe/issues/2276. -func (ps *PrioritySelector) selector(ctx context.Context) { +func (ps *prioritySelector) selector(ctx context.Context) { // Implementation note: setting an arbitrary timeout here would // be ~an issue because we want this goroutine to be available in // case the only connections from which we could fetch a webpage @@ -213,7 +213,7 @@ Loop: } // findHighestPriority returns the highest priority request -func (ps *PrioritySelector) findHighestPriority(reqs []*priorityRequest) *priorityRequest { +func (ps *prioritySelector) findHighestPriority(reqs []*priorityRequest) *priorityRequest { runtimex.Assert(len(reqs) > 0, "findHighestPriority wants a non-empty reqs slice") for _, r := range reqs { if ps.isHighestPriority(r) { @@ -224,7 +224,7 @@ func (ps *PrioritySelector) findHighestPriority(reqs []*priorityRequest) *priori } // isHighestPriority returns whether this request is highest priority -func (ps *PrioritySelector) isHighestPriority(r *priorityRequest) bool { +func (ps *prioritySelector) isHighestPriority(r *priorityRequest) bool { // See https://github.com/ooni/probe/issues/2276 flags := ps.m[r.addr] if ps.nsystem > 0 { diff --git a/internal/experiment/webconnectivitylte/qa_test.go b/internal/experiment/webconnectivitylte/qa_test.go index ef0613c74c..a4036d9c49 100644 --- a/internal/experiment/webconnectivitylte/qa_test.go +++ b/internal/experiment/webconnectivitylte/qa_test.go @@ -1,4 +1,4 @@ -package webconnectivitylte_test +package webconnectivitylte import ( "context" @@ -11,7 +11,6 @@ import ( "github.com/apex/log" "github.com/ooni/netem" - "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netemx" @@ -193,7 +192,7 @@ func qaRunWithURL(input string, setISPResolverConfig func(*netem.DNSConfig), setDPI(env.DPIEngine()) // create the measurer and the context - measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) + measurer := NewExperimentMeasurer(&Config{}) ctx := context.Background() // create a new measurement diff --git a/internal/experiment/webconnectivitylte/secureflow.go b/internal/experiment/webconnectivitylte/secureflow.go index f227555bfd..a1079aae11 100644 --- a/internal/experiment/webconnectivitylte/secureflow.go +++ b/internal/experiment/webconnectivitylte/secureflow.go @@ -67,7 +67,7 @@ type SecureFlow struct { // PrioSelector is the OPTIONAL priority selector to use to determine // whether this flow is allowed to fetch the webpage. - PrioSelector *PrioritySelector + PrioSelector *prioritySelector // Referer contains the OPTIONAL referer, used for redirects. Referer string diff --git a/internal/experiment/webconnectivitylte/secureflow_test.go b/internal/experiment/webconnectivitylte/secureflow_test.go index 1fba228e36..4fbaef4f41 100644 --- a/internal/experiment/webconnectivitylte/secureflow_test.go +++ b/internal/experiment/webconnectivitylte/secureflow_test.go @@ -1,4 +1,4 @@ -package webconnectivitylte_test +package webconnectivitylte import ( "context" @@ -9,25 +9,24 @@ import ( "testing" "time" - "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/model" ) func TestSecureFlow_Run(t *testing.T) { type fields struct { Address string - DNSCache *webconnectivitylte.DNSCache + DNSCache *DNSCache IDGenerator *atomic.Int64 Logger model.Logger - NumRedirects *webconnectivitylte.NumRedirects - TestKeys *webconnectivitylte.TestKeys + NumRedirects *NumRedirects + TestKeys *TestKeys ZeroTime time.Time WaitGroup *sync.WaitGroup ALPN []string CookieJar http.CookieJar FollowRedirects bool HostHeader string - PrioSelector *webconnectivitylte.PrioritySelector + PrioSelector *prioritySelector Referer string SNI string UDPAddress string @@ -53,7 +52,7 @@ func TestSecureFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: webconnectivitylte.ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }, { name: "with loopback IPv6 endpoint", fields: fields{ @@ -64,11 +63,11 @@ func TestSecureFlow_Run(t *testing.T) { parentCtx: context.Background(), index: 0, }, - want: webconnectivitylte.ErrNotAllowedToConnect, + want: errNotAllowedToConnect, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tr := &webconnectivitylte.SecureFlow{ + tr := &SecureFlow{ Address: tt.fields.Address, DNSCache: tt.fields.DNSCache, IDGenerator: tt.fields.IDGenerator, From a4f9e92675f8451eb6d4ab86f48894881c9810ef Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 21 Jul 2023 14:53:53 +0200 Subject: [PATCH 18/25] netemx geoip: change mock geoip service's response to public server --- internal/netemx/geoip.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/netemx/geoip.go b/internal/netemx/geoip.go index 27beed72a4..28f93c2992 100644 --- a/internal/netemx/geoip.go +++ b/internal/netemx/geoip.go @@ -5,7 +5,7 @@ import "net/http" type GeoIPLookup struct{} func (p *GeoIPLookup) ServeHTTP(w http.ResponseWriter, r *http.Request) { - resp := `89.0.2.153OKDEDEUGermany07Nordrhein-WestfalenAachen5207450.74796.04850Europe/Berlin` + resp := `99.83.231.61OKUSUSAUnited States of AmericaWashingtonSeattle9810847.5413-122.3129America/Los_Angeles` w.Header().Add("Content-Type", "text/xml") w.Write([]byte(resp)) From 16afba76395806f911d29bdbbb21758394185fb9 Mon Sep 17 00:00:00 2001 From: kelmenhorst Date: Fri, 21 Jul 2023 15:03:34 +0200 Subject: [PATCH 19/25] QAEnvHTTPHandlerFactory: change construction pattern for http.Handlers --- .../webconnectivitylte/measurer_test.go | 38 +++++++++---- .../experiment/webconnectivitylte/qa_test.go | 10 +++- internal/netemx/qaenv.go | 55 +++++++++++-------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go index d6d0b4e9c9..a806d9d15d 100644 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ b/internal/experiment/webconnectivitylte/measurer_test.go @@ -34,18 +34,28 @@ func newEnvironment() *netemx.QAEnv { env := netemx.NewQAEnv( netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), - netemx.QAEnvOptionHTTPServer("93.184.216.34", netemx.QAEnvDefaultHTTPHandler()), - netemx.QAEnvOptionHTTPServer("104.248.30.161", nil), - netemx.QAEnvOptionHTTPServer("104.16.248.249", dohServer), - netemx.QAEnvOptionHTTPServer("188.166.93.143", &probeService{}), - netemx.QAEnvOptionHTTPServer("185.125.188.132", &netemx.GeoIPLookup{}), + netemx.QAEnvOptionHTTPServerWithFactory( + "93.184.216.34", + netemx.QAEnvAlwaysReturnThisHandler(netemx.QAEnvDefaultHTTPHandler()), + ), + netemx.QAEnvOptionHTTPServerWithFactory( + "104.248.30.161", + testHelperFactory{}, + ), + netemx.QAEnvOptionHTTPServerWithFactory( + "104.16.248.249", + netemx.QAEnvAlwaysReturnThisHandler(dohServer), + ), + netemx.QAEnvOptionHTTPServerWithFactory( + "188.166.93.143", + netemx.QAEnvAlwaysReturnThisHandler(&probeService{}), + ), + netemx.QAEnvOptionHTTPServerWithFactory( + "185.125.188.132", + netemx.QAEnvAlwaysReturnThisHandler(&netemx.GeoIPLookup{}), + ), ) - // create new testhelper handler using the newly created server stack - underlyingStack := env.GetServerStack("104.248.30.161") - helperHandler := newTestHelper(underlyingStack) - env.AddHandler("104.248.30.161", helperHandler) - // configure default UDP DNS server env.AddRecordToAllResolvers( "dns.quad9.net", @@ -75,8 +85,12 @@ func newEnvironment() *netemx.QAEnv { return env } -func newTestHelper(underlying netem.UnderlyingNetwork) *oohelperd.Handler { - n := netxlite.Net{Underlying: netemx.GetCustomTProxy(underlying)} +// testHelperFactory is the factory to create a oohelperd testhelper using the given underlying network. +type testHelperFactory struct{} + +// NewHandler implements QAEnvHTTPHandlerFactory.NewHandler. +func (f testHelperFactory) NewHandler(net netem.UnderlyingNetwork) http.Handler { + n := netxlite.Net{Underlying: netemx.GetCustomTProxy(net)} helperHandler := oohelperd.NewHandler() helperHandler.NewDialer = func(logger model.Logger) model.Dialer { return n.NewDialerWithResolver(logger, n.NewStdlibResolver(logger)) diff --git a/internal/experiment/webconnectivitylte/qa_test.go b/internal/experiment/webconnectivitylte/qa_test.go index a4036d9c49..b3b599b2d6 100644 --- a/internal/experiment/webconnectivitylte/qa_test.go +++ b/internal/experiment/webconnectivitylte/qa_test.go @@ -117,8 +117,14 @@ func qaAddTHDomains(config *netem.DNSConfig) { func qaNewEnvironment() *netemx.QAEnv { return netemx.NewQAEnv( netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), - netemx.QAEnvOptionHTTPServer(qaWebServerAddress, netemx.QAEnvDefaultHTTPHandler()), - netemx.QAEnvOptionHTTPServer(qaZeroTHOoniOrg, qaNewMockedTestHelper()), + netemx.QAEnvOptionHTTPServerWithFactory( + qaWebServerAddress, + netemx.QAEnvAlwaysReturnThisHandler(netemx.QAEnvDefaultHTTPHandler()), + ), + netemx.QAEnvOptionHTTPServerWithFactory( + qaZeroTHOoniOrg, + netemx.QAEnvAlwaysReturnThisHandler(qaNewMockedTestHelper()), + ), ) } diff --git a/internal/netemx/qaenv.go b/internal/netemx/qaenv.go index f831e25a00..9c9aeae98d 100644 --- a/internal/netemx/qaenv.go +++ b/internal/netemx/qaenv.go @@ -36,8 +36,8 @@ type qaEnvConfig struct { // dnsOverUDPResolvers contains the DNS-over-UDP resolvers to create. dnsOverUDPResolvers []string - // httpServers contains the HTTP servers to create. - httpServers map[string]http.Handler + // httpServers contains factory functions for the HTTP servers to create. + httpServers map[string]QAEnvHTTPHandlerFactory // ispResolver is the ISP resolver to use. ispResolver string @@ -80,18 +80,38 @@ func QAEnvOptionDNSOverUDPResolvers(ipAddrs ...string) QAEnvOption { } } -// QAEnvOptionHTTPServer adds the given HTTP server. If you do not set this option -// we will not create any HTTP server. -func QAEnvOptionHTTPServer(ipAddr string, handler http.Handler) QAEnvOption { +// QAEnvHTTPHandlerFactory constructs an [http.Handler] using the given underlying network. +type QAEnvHTTPHandlerFactory interface { + NewHandler(net netem.UnderlyingNetwork) http.Handler +} + +// QAEnvOptionHTTPServerWithFactory adds the given HTTP server as a factory function. +// If you do not set this option we will not create any HTTP server. +func QAEnvOptionHTTPServerWithFactory(ipAddr string, factory QAEnvHTTPHandlerFactory) QAEnvOption { runtimex.Assert(net.ParseIP(ipAddr) != nil, "not an IP addr") - // TODO: we might want to pass a nil handler first and add another one later - // (see: experiment/webconnectivitylte/measurer_test.go) - // runtimex.Assert(handler != nil, "passed a nil handler") + runtimex.Assert(factory != nil, "passed a nil handler factory") + return func(config *qaEnvConfig) { - config.httpServers[ipAddr] = handler + config.httpServers[ipAddr] = factory } } +// defaultHTTPHandlerFactory is the default handler factory that just returns a given [http.Handler]. +type defaultHTTPHandlerFactory struct { + handler http.Handler +} + +// NewHandler implements QAEnvHTTPHandlerFactory.NewHandler. +func (f defaultHTTPHandlerFactory) NewHandler(net netem.UnderlyingNetwork) http.Handler { + return f.handler +} + +// QAEnvAlwaysReturnThisHandler returns a QAEnvHTTPHandlerFactory such that we can always use the +// new API that requires a httpHandlerFactory. +func QAEnvAlwaysReturnThisHandler(handler http.Handler) QAEnvHTTPHandlerFactory { + return &defaultHTTPHandlerFactory{handler} +} + // QAEnvOptionISPResolverAddress sets the ISP's resolver IP address. If you do not set this option // we will use [QAEnvDefaultISPResolverAddress] as the address. func QAEnvOptionISPResolverAddress(ipAddr string) QAEnvOption { @@ -178,7 +198,7 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { clientAddress: QAEnvDefaultClientAddress, clientNICWrapper: nil, dnsOverUDPResolvers: []string{}, - httpServers: map[string]http.Handler{}, + httpServers: map[string]QAEnvHTTPHandlerFactory{}, ispResolver: QAEnvDefaultISPResolverAddress, logger: model.DiscardLogger, netStacks: map[string]QAEnvNetStackHandler{}, @@ -213,15 +233,6 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { return env } -// AddHandler is used to add another handler at a given server stack after creating the environment. -// We need this option to use a server stack that has been created during NewQAEnv as an underlying -// network of a HTTP handler. -// (see: experiment/webconnectivitylte/measurer_test.go) -func (env *QAEnv) AddHandler(serverAddr string, handler http.Handler) { - serverStack := env.serverStacks[serverAddr] - env.closables = append(env.closables, env.serverListen(serverStack, handler, serverAddr)...) -} - func (env *QAEnv) mustNewISPResolverStack(config *qaEnvConfig) io.Closer { // Create the ISP's DNS server TCP/IP stack. // @@ -301,7 +312,7 @@ func (env *QAEnv) mustNewHTTPServers(config *qaEnvConfig) (closables []io.Closer runtimex.Assert(len(config.dnsOverUDPResolvers) >= 1, "expected at least one DNS resolver") resolver := config.dnsOverUDPResolvers[0] - for addr, handler := range config.httpServers { + for addr, factory := range config.httpServers { // Create the server's TCP/IP stack // // Note: because the stack is created using topology.AddHost, we don't @@ -317,9 +328,7 @@ func (env *QAEnv) mustNewHTTPServers(config *qaEnvConfig) (closables []io.Closer )) env.serverStacks[addr] = stack - if handler == nil { - continue - } + handler := factory.NewHandler(stack) closables = env.serverListen(stack, handler, addr) } return From 05648fe9da44a6e0a60a597fa8c2ebfa6bfe2e31 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 13:13:48 +0200 Subject: [PATCH 20/25] sync --- internal/{ => cmd}/oohelperd/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/{ => cmd}/oohelperd/README.md (100%) diff --git a/internal/oohelperd/README.md b/internal/cmd/oohelperd/README.md similarity index 100% rename from internal/oohelperd/README.md rename to internal/cmd/oohelperd/README.md From b02ecc31ce6acdf0d02b4f872d29f2ec58293741 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 14:12:00 +0200 Subject: [PATCH 21/25] x --- internal/netxlite/dialer.go | 2 +- internal/netxlite/dialer_test.go | 16 ++++++------ internal/netxlite/dnsovergetaddrinfo.go | 2 +- internal/netxlite/dnsovergetaddrinfo_test.go | 17 +++++------- internal/netxlite/net.go | 14 ++++++---- internal/netxlite/quic.go | 6 +---- internal/netxlite/quic_test.go | 27 +++++++------------- internal/netxlite/tls.go | 2 +- internal/netxlite/tls_test.go | 12 +++------ internal/netxlite/utls.go | 3 +-- 10 files changed, 41 insertions(+), 60 deletions(-) diff --git a/internal/netxlite/dialer.go b/internal/netxlite/dialer.go index 87432dd280..54561b9c82 100644 --- a/internal/netxlite/dialer.go +++ b/internal/netxlite/dialer.go @@ -25,7 +25,7 @@ func NewDialerWithStdlibResolver(dl model.DebugLogger) model.Dialer { // NewDialerWithResolver is equivalent to calling WrapDialer with // the dialer argument being equal to &DialerSystem{}. func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { - return WrapDialer(dl, r, &DialerSystem{underlying: tproxySingleton()}, w...) + return WrapDialer(dl, r, &DialerSystem{}, w...) } // WrapDialer wraps an existing Dialer to add extra functionality diff --git a/internal/netxlite/dialer_test.go b/internal/netxlite/dialer_test.go index d002009b4e..80b84528c9 100644 --- a/internal/netxlite/dialer_test.go +++ b/internal/netxlite/dialer_test.go @@ -82,7 +82,7 @@ func TestNewDialer(t *testing.T) { func TestDialerSystem(t *testing.T) { t.Run("has a default timeout", func(t *testing.T) { - d := &DialerSystem{underlying: tproxySingleton()} + d := &DialerSystem{} timeout := d.configuredTimeout() if timeout != dialerDefaultTimeout { t.Fatal("unexpected default timeout") @@ -91,7 +91,7 @@ func TestDialerSystem(t *testing.T) { t.Run("we can change the timeout for testing", func(t *testing.T) { const smaller = 1 * time.Second - d := &DialerSystem{timeout: smaller, underlying: tproxySingleton()} + d := &DialerSystem{timeout: smaller} timeout := d.configuredTimeout() if timeout != smaller { t.Fatal("unexpected timeout") @@ -99,13 +99,13 @@ func TestDialerSystem(t *testing.T) { }) t.Run("CloseIdleConnections", func(t *testing.T) { - d := &DialerSystem{underlying: tproxySingleton()} + d := &DialerSystem{} d.CloseIdleConnections() // to avoid missing coverage }) t.Run("DialContext", func(t *testing.T) { t.Run("with canceled context", func(t *testing.T) { - d := &DialerSystem{underlying: tproxySingleton()} + d := &DialerSystem{} ctx, cancel := context.WithCancel(context.Background()) cancel() // immediately! conn, err := d.DialContext(ctx, "tcp", "8.8.8.8:443") @@ -119,7 +119,7 @@ func TestDialerSystem(t *testing.T) { t.Run("enforces the configured timeout", func(t *testing.T) { const timeout = 1 * time.Nanosecond - d := &DialerSystem{timeout: timeout, underlying: tproxySingleton()} + d := &DialerSystem{timeout: timeout} ctx := context.Background() start := time.Now() conn, err := d.DialContext(ctx, "tcp", "dns.google:443") @@ -158,7 +158,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("DialContext", func(t *testing.T) { t.Run("fails without a port", func(t *testing.T) { d := &dialerResolverWithTracing{ - Dialer: &DialerSystem{underlying: tproxySingleton()}, + Dialer: &DialerSystem{}, Resolver: NewUnwrappedStdlibResolver(), } const missingPort = "ooni.nu" @@ -505,7 +505,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("lookupHost", func(t *testing.T) { t.Run("handles addresses correctly", func(t *testing.T) { dialer := &dialerResolverWithTracing{ - Dialer: &DialerSystem{underlying: tproxySingleton()}, + Dialer: &DialerSystem{}, Resolver: &NullResolver{}, } addrs, err := dialer.lookupHost(context.Background(), "1.1.1.1") @@ -519,7 +519,7 @@ func TestDialerResolverWithTracing(t *testing.T) { t.Run("fails correctly on lookup error", func(t *testing.T) { dialer := &dialerResolverWithTracing{ - Dialer: &DialerSystem{underlying: tproxySingleton()}, + Dialer: &DialerSystem{}, Resolver: &NullResolver{}, } ctx := context.Background() diff --git a/internal/netxlite/dnsovergetaddrinfo.go b/internal/netxlite/dnsovergetaddrinfo.go index 972f60bfb0..17b5691855 100644 --- a/internal/netxlite/dnsovergetaddrinfo.go +++ b/internal/netxlite/dnsovergetaddrinfo.go @@ -29,7 +29,7 @@ type dnsOverGetaddrinfoTransport struct { // NewDNSOverGetaddrinfoTransport creates a new dns-over-getaddrinfo transport. func NewDNSOverGetaddrinfoTransport() model.DNSTransport { - return &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + return &dnsOverGetaddrinfoTransport{} } var _ model.DNSTransport = &dnsOverGetaddrinfoTransport{} diff --git a/internal/netxlite/dnsovergetaddrinfo_test.go b/internal/netxlite/dnsovergetaddrinfo_test.go index a36a897b3a..250330959c 100644 --- a/internal/netxlite/dnsovergetaddrinfo_test.go +++ b/internal/netxlite/dnsovergetaddrinfo_test.go @@ -15,40 +15,40 @@ import ( func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("RequiresPadding", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} if txp.RequiresPadding() { t.Fatal("expected false") } }) t.Run("Network", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} if txp.Network() != getaddrinfoResolverNetwork() { t.Fatal("unexpected Network") } }) t.Run("Address", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} if txp.Address() != "" { t.Fatal("unexpected Address") } }) t.Run("CloseIdleConnections", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} txp.CloseIdleConnections() // does not crash }) t.Run("check default timeout", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} if txp.timeout() != 15*time.Second { t.Fatal("unexpected default timeout") } }) t.Run("check default lookup host func not nil", func(t *testing.T) { - txp := &dnsOverGetaddrinfoTransport{underlying: tproxySingleton()} + txp := &dnsOverGetaddrinfoTransport{} if txp.lookupfn() == nil { t.Fatal("expected non-nil func here") } @@ -57,7 +57,6 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("RoundTrip", func(t *testing.T) { t.Run("with invalid query type", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ - underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return []string{"8.8.8.8"}, "dns.google", nil }, @@ -76,7 +75,6 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("with success", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ - underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return []string{"8.8.8.8"}, "dns.google", nil }, @@ -125,7 +123,6 @@ func TestDNSOverGetaddrinfo(t *testing.T) { wg.Add(1) done := make(chan interface{}) txp := &dnsOverGetaddrinfoTransport{ - underlying: tproxySingleton(), testableTimeout: 1 * time.Microsecond, testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { defer wg.Done() @@ -152,7 +149,6 @@ func TestDNSOverGetaddrinfo(t *testing.T) { wg.Add(1) done := make(chan interface{}) txp := &dnsOverGetaddrinfoTransport{ - underlying: tproxySingleton(), testableTimeout: 1 * time.Microsecond, testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { defer wg.Done() @@ -176,7 +172,6 @@ func TestDNSOverGetaddrinfo(t *testing.T) { t.Run("with NXDOMAIN", func(t *testing.T) { txp := &dnsOverGetaddrinfoTransport{ - underlying: tproxySingleton(), testableLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { return nil, "", ErrOODNSNoSuchHost }, diff --git a/internal/netxlite/net.go b/internal/netxlite/net.go index 5bf6c40206..358fc29184 100644 --- a/internal/netxlite/net.go +++ b/internal/netxlite/net.go @@ -14,11 +14,15 @@ type Net struct { Underlying model.UnderlyingNetwork } +func (n *Net) tproxyNilSafeProvider() *tproxyNilSafeProvider { + return &tproxyNilSafeProvider{n.Underlying} +} + // NewStdlibResolver is like netxlite.NewStdlibResolver but // the constructed resolver uses the given [UnderlyingNetwork]. func (n *Net) NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver { unwrapped := &resolverSystem{ - t: WrapDNSTransport(&dnsOverGetaddrinfoTransport{underlying: n.Underlying}, wrappers...), + t: WrapDNSTransport(&dnsOverGetaddrinfoTransport{provider: n.tproxyNilSafeProvider()}, wrappers...), } return WrapResolver(logger, unwrapped) } @@ -26,13 +30,13 @@ func (n *Net) NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNST // NewDialerWithResolver is like netxlite.NewDialerWithResolver but // the constructed dialer uses the given [UnderlyingNetwork]. func (n *Net) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { - return WrapDialer(dl, r, &DialerSystem{underlying: n.Underlying}, w...) + return WrapDialer(dl, r, &DialerSystem{provider: n.tproxyNilSafeProvider()}, w...) } // NewQUICListener is like netxlite.NewQUICListener but // the constructed listener uses the given [UnderlyingNetwork]. func (n *Net) NewQUICListener() model.QUICListener { - return &quicListenerErrWrapper{&quicListenerStdlib{underlying: n.Underlying}} + return &quicListenerErrWrapper{&quicListenerStdlib{provider: n.tproxyNilSafeProvider()}} } // NewQUICDialerWithResolver is like netxlite.NewQUICDialerWithResolver but @@ -41,7 +45,7 @@ func (n *Net) NewQUICDialerWithResolver(listener model.QUICListener, logger mode resolver model.Resolver, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { baseDialer := &quicDialerQUICGo{ QUICListener: listener, - underlying: n.Underlying, + provider: n.tproxyNilSafeProvider(), } return WrapQUICDialer(logger, resolver, baseDialer, wrappers...) } @@ -49,7 +53,7 @@ func (n *Net) NewQUICDialerWithResolver(listener model.QUICListener, logger mode // NewTLSHandshakerStdlib is like netxlite.NewTLSHandshakerStdlib but // the constructed handshaker uses the given [UnderlyingNetwork]. func (n *Net) NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { - return newTLSHandshakerLogger(&tlsHandshakerConfigurable{underlying: n.Underlying}, logger) + return newTLSHandshakerLogger(&tlsHandshakerConfigurable{provider: n.tproxyNilSafeProvider()}, logger) } // NewHTTPTransportStdlib is like netxlite.NewHTTPTransportStdlib but diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index 9d8d0f3e3c..e77469c4c2 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -19,7 +19,7 @@ import ( // NewQUICListener creates a new QUICListener using the standard // library to create listening UDP sockets. func NewQUICListener() model.QUICListener { - return &quicListenerErrWrapper{&quicListenerStdlib{underlying: tproxySingleton()}} + return &quicListenerErrWrapper{&quicListenerStdlib{}} } // quicListenerStdlib is a QUICListener using the standard library. @@ -100,10 +100,6 @@ type quicDialerQUICGo struct { // QUICListener is the underlying QUICListener to use. QUICListener model.QUICListener - // underlying is the MANDATORY custom [UnderlyingNetwork]. - // If nil, we will use tproxySingleton() as underlying network. - underlying model.UnderlyingNetwork - // mockDialEarlyContext allows to mock quic.DialEarlyContext. mockDialEarlyContext func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index dc593affef..f7852fa9bc 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -132,8 +132,7 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, - underlying: tproxySingleton(), + QUICListener: &quicListenerStdlib{}, } defer systemdialer.CloseIdleConnections() // just to see it running ctx := context.Background() @@ -152,8 +151,7 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, - underlying: tproxySingleton(), + QUICListener: &quicListenerStdlib{}, } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -171,8 +169,7 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "www.google.com", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, - underlying: tproxySingleton(), + QUICListener: &quicListenerStdlib{}, } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -196,7 +193,6 @@ func TestQUICDialerQUICGo(t *testing.T) { return nil, expected }, }, - underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -214,8 +210,7 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, - underlying: tproxySingleton(), + QUICListener: &quicListenerStdlib{}, } ctx, cancel := context.WithCancel(context.Background()) cancel() // fail immediately @@ -236,14 +231,13 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + QUICListener: &quicListenerStdlib{}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { gotTLSConfig = tlsConfig return nil, expected }, - underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -278,14 +272,13 @@ func TestQUICDialerQUICGo(t *testing.T) { ServerName: "dns.google", } systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + QUICListener: &quicListenerStdlib{}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { gotTLSConfig = tlsConfig return nil, expected }, - underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -319,13 +312,12 @@ func TestQUICDialerQUICGo(t *testing.T) { } fakeconn := &mocks.QUICEarlyConnection{} systemdialer := quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, + QUICListener: &quicListenerStdlib{}, mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, host string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { return fakeconn, nil }, - underlying: tproxySingleton(), } ctx := context.Background() qconn, err := systemdialer.DialContext( @@ -565,7 +557,7 @@ func TestQUICDialerResolver(t *testing.T) { tlsConfig := &tls.Config{} dialer := &quicDialerResolver{ Resolver: NewStdlibResolver(log.Log), - Dialer: &quicDialerQUICGo{underlying: tproxySingleton()}} + Dialer: &quicDialerQUICGo{}} qconn, err := dialer.DialContext( context.Background(), "www.google.com", tlsConfig, &quic.Config{}) @@ -603,8 +595,7 @@ func TestQUICDialerResolver(t *testing.T) { dialer := &quicDialerResolver{ Resolver: NewStdlibResolver(log.Log), Dialer: &quicDialerQUICGo{ - QUICListener: &quicListenerStdlib{underlying: tproxySingleton()}, - underlying: tproxySingleton(), + QUICListener: &quicListenerStdlib{}, }} qconn, err := dialer.DialContext( context.Background(), "8.8.4.4:x", diff --git a/internal/netxlite/tls.go b/internal/netxlite/tls.go index 47425aaabd..113ec1ebcf 100644 --- a/internal/netxlite/tls.go +++ b/internal/netxlite/tls.go @@ -164,7 +164,7 @@ var _ TLSConn = &tls.Conn{} // 3. that we are going to use Mozilla CA if the [tls.Config] // RootCAs field is zero initialized. func NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { - return newTLSHandshakerLogger(&tlsHandshakerConfigurable{underlying: tproxySingleton()}, logger) + return newTLSHandshakerLogger(&tlsHandshakerConfigurable{}, logger) } // newTLSHandshakerLogger creates a new tlsHandshakerLogger instance. diff --git a/internal/netxlite/tls_test.go b/internal/netxlite/tls_test.go index 4ec5909e52..54a8cd3483 100644 --- a/internal/netxlite/tls_test.go +++ b/internal/netxlite/tls_test.go @@ -137,7 +137,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { t.Run("Handshake", func(t *testing.T) { t.Run("with handshake I/O error", func(t *testing.T) { var times []time.Time - h := &tlsHandshakerConfigurable{underlying: tproxySingleton()} + h := &tlsHandshakerConfigurable{} tcpConn := &mocks.Conn{ MockWrite: func(b []byte) (int, error) { return 0, io.EOF @@ -209,7 +209,7 @@ func TestTLSHandshakerConfigurable(t *testing.T) { t.Fatal(err) } defer conn.Close() - handshaker := &tlsHandshakerConfigurable{underlying: tproxySingleton()} + handshaker := &tlsHandshakerConfigurable{} ctx := context.Background() config := &tls.Config{ InsecureSkipVerify: true, @@ -239,7 +239,6 @@ func TestTLSHandshakerConfigurable(t *testing.T) { }, }, nil }, - underlying: tproxySingleton(), } ctx := context.Background() config := &tls.Config{} @@ -346,7 +345,6 @@ func TestTLSHandshakerConfigurable(t *testing.T) { NewConn: func(conn net.Conn, config *tls.Config) (TLSConn, error) { return nil, expected }, - underlying: tproxySingleton(), } ctx := context.Background() config := &tls.Config{} @@ -713,7 +711,7 @@ func TestTLSDialer(t *testing.T) { t.Run("failure dialing", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // immediately fail - dialer := tlsDialer{Dialer: &DialerSystem{underlying: tproxySingleton()}} + dialer := tlsDialer{Dialer: &DialerSystem{}} conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443") if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") { t.Fatal("not the error we expected", err) @@ -745,9 +743,7 @@ func TestTLSDialer(t *testing.T) { } }}, nil }}, - TLSHandshaker: &tlsHandshakerConfigurable{ - underlying: tproxySingleton(), - }, + TLSHandshaker: &tlsHandshakerConfigurable{}, } conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443") if !errors.Is(err, io.EOF) { diff --git a/internal/netxlite/utls.go b/internal/netxlite/utls.go index 620a63d55e..dd45d31c7a 100644 --- a/internal/netxlite/utls.go +++ b/internal/netxlite/utls.go @@ -33,8 +33,7 @@ import ( // Passing a nil `id` will make this function panic. func NewTLSHandshakerUTLS(logger model.DebugLogger, id *utls.ClientHelloID) model.TLSHandshaker { return newTLSHandshakerLogger(&tlsHandshakerConfigurable{ - NewConn: newUTLSConnFactory(id), - underlying: tproxySingleton(), + NewConn: newUTLSConnFactory(id), }, logger) } From d350efd4517dfd2d40e3806f5813a73d6ea68c20 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 14:22:26 +0200 Subject: [PATCH 22/25] x --- .../webconnectivitylte/analysiscore.go | 2 - .../webconnectivitylte/measurer_test.go | 299 ------------------ .../experiment/webconnectivitylte/qa_test.go | 15 +- internal/netemx/adapter.go | 5 - internal/netemx/dns.go | 62 ---- internal/netemx/geoip.go | 12 - internal/netemx/qaenv.go | 101 ++---- 7 files changed, 37 insertions(+), 459 deletions(-) delete mode 100644 internal/experiment/webconnectivitylte/measurer_test.go delete mode 100644 internal/netemx/dns.go delete mode 100644 internal/netemx/geoip.go diff --git a/internal/experiment/webconnectivitylte/analysiscore.go b/internal/experiment/webconnectivitylte/analysiscore.go index 2f4f2077a6..cdb5cb9046 100644 --- a/internal/experiment/webconnectivitylte/analysiscore.go +++ b/internal/experiment/webconnectivitylte/analysiscore.go @@ -122,8 +122,6 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { tk.BlockingFlags, tk.Accessible, tk.Blocking, ) - // assigning "http-failure" for both TLS and HTTP blocking is legacy - // because the spec does not consider the case of TLS based blocking case (tk.BlockingFlags & (analysisFlagTLSBlocking | analysisFlagHTTPBlocking)) != 0: tk.Blocking = "http-failure" tk.Accessible = false diff --git a/internal/experiment/webconnectivitylte/measurer_test.go b/internal/experiment/webconnectivitylte/measurer_test.go deleted file mode 100644 index a806d9d15d..0000000000 --- a/internal/experiment/webconnectivitylte/measurer_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package webconnectivitylte - -import ( - "context" - "encoding/json" - "net/http" - "net/http/cookiejar" - "testing" - - "github.com/apex/log" - "github.com/ooni/netem" - "github.com/ooni/probe-cli/v3/internal/bytecounter" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "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/oohelperd" - "github.com/ooni/probe-cli/v3/internal/runtimex" - "github.com/ooni/probe-cli/v3/internal/sessionresolver" - "golang.org/x/net/publicsuffix" -) - -func newEnvironment() *netemx.QAEnv { - // configure DoH server - dohServer := &netemx.DoHServer{} - dohServer.AddRecord("ams-pg-test.ooni.org", "188.166.93.143") - dohServer.AddRecord("geoip.ubuntu.com", "185.125.188.132") - dohServer.AddRecord("www.example.com", "93.184.216.34") - dohServer.AddRecord("0.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("1.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("2.th.ooni.org", "104.248.30.161") - dohServer.AddRecord("3.th.ooni.org", "104.248.30.161") - - env := netemx.NewQAEnv( - netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), - netemx.QAEnvOptionHTTPServerWithFactory( - "93.184.216.34", - netemx.QAEnvAlwaysReturnThisHandler(netemx.QAEnvDefaultHTTPHandler()), - ), - netemx.QAEnvOptionHTTPServerWithFactory( - "104.248.30.161", - testHelperFactory{}, - ), - netemx.QAEnvOptionHTTPServerWithFactory( - "104.16.248.249", - netemx.QAEnvAlwaysReturnThisHandler(dohServer), - ), - netemx.QAEnvOptionHTTPServerWithFactory( - "188.166.93.143", - netemx.QAEnvAlwaysReturnThisHandler(&probeService{}), - ), - netemx.QAEnvOptionHTTPServerWithFactory( - "185.125.188.132", - netemx.QAEnvAlwaysReturnThisHandler(&netemx.GeoIPLookup{}), - ), - ) - - // configure default UDP DNS server - env.AddRecordToAllResolvers( - "dns.quad9.net", - "dns.quad9.net", - "104.16.248.249", - ) - env.AddRecordToAllResolvers( - "mozilla.cloudflare-dns.com", - "mozilla.cloudflare-dns.com", - "104.16.248.249", - ) - env.AddRecordToAllResolvers( - "dns.nextdns.io", - "dns.nextdns.io", - "104.16.248.249", - ) - env.AddRecordToAllResolvers( - "dns.google", - "dns.google", - "104.16.248.249", - ) - env.AddRecordToAllResolvers( - "www.example.com", - "www.example.com", - "93.184.216.34", - ) - return env -} - -// testHelperFactory is the factory to create a oohelperd testhelper using the given underlying network. -type testHelperFactory struct{} - -// NewHandler implements QAEnvHTTPHandlerFactory.NewHandler. -func (f testHelperFactory) NewHandler(net netem.UnderlyingNetwork) http.Handler { - n := netxlite.Net{Underlying: netemx.GetCustomTProxy(net)} - helperHandler := oohelperd.NewHandler() - helperHandler.NewDialer = func(logger model.Logger) model.Dialer { - return n.NewDialerWithResolver(logger, n.NewStdlibResolver(logger)) - } - helperHandler.NewQUICDialer = func(logger model.Logger) model.QUICDialer { - return n.NewQUICDialerWithResolver( - n.NewQUICListener(), - logger, - n.NewStdlibResolver(logger), - ) - } - helperHandler.NewResolver = func(logger model.Logger) model.Resolver { - return n.NewStdlibResolver(logger) - } - - helperHandler.NewHTTPClient = func(logger model.Logger) model.HTTPClient { - cookieJar, _ := cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - return &http.Client{ - Transport: n.NewHTTPTransportStdlib(logger), - CheckRedirect: nil, - Jar: cookieJar, - Timeout: 0, - } - } - helperHandler.NewHTTP3Client = func(logger model.Logger) model.HTTPClient { - cookieJar, _ := cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - return &http.Client{ - Transport: n.NewHTTP3TransportStdlib(logger), - CheckRedirect: nil, - Jar: cookieJar, - Timeout: 0, - } - } - return helperHandler -} - -func TestSuccess(t *testing.T) { - env := newEnvironment() - defer env.Close() - env.Do(func() { - measurer := NewExperimentMeasurer(&Config{}) - ctx := context.Background() - sess := newSession() - measurement := &model.Measurement{Input: "https://www.example.com"} - callbacks := model.NewPrinterCallbacks(log.Log) - args := &model.ExperimentArgs{ - Callbacks: callbacks, - Measurement: measurement, - Session: sess, - } - err := measurer.Run(ctx, args) - if err != nil { - t.Fatal(err) - } - tk := measurement.TestKeys.(*TestKeys) - if tk.ControlFailure != nil { - t.Fatal("unexpected control_failure", *tk.ControlFailure) - } - if tk.Blocking != false { - t.Fatal("unexpected blocking detected") - } - if tk.Accessible != true { - t.Fatal("unexpected accessible flag: should be accessible") - } - }) -} - -func TestDPITarget(t *testing.T) { - env := newEnvironment() - dpi := env.DPIEngine() - dpi.AddRule(&netem.DPIResetTrafficForTLSSNI{ - Logger: model.DiscardLogger, - SNI: "www.example.com", - }) - defer env.Close() - env.Do(func() { - measurer := NewExperimentMeasurer(&Config{}) - ctx := context.Background() - sess := newSession() - measurement := &model.Measurement{Input: "https://www.example.com"} - callbacks := model.NewPrinterCallbacks(log.Log) - args := &model.ExperimentArgs{ - Callbacks: callbacks, - Measurement: measurement, - Session: sess, - } - err := measurer.Run(ctx, args) - if err != nil { - t.Fatal(err) - } - tk := measurement.TestKeys.(*TestKeys) - if tk.ControlFailure != nil { - t.Fatal("unexpected control_failure", *tk.ControlFailure) - } - if tk.Blocking != "http-failure" { - t.Fatal("unexpected blocking type") - } - if tk.Accessible == true { - t.Fatal("unexpected accessible flag: should be false") - } - }) -} - -type probeService struct{} - -type th struct { - Addr string `json:"address"` - T string `json:"type"` -} - -func (p *probeService) ServeHTTP(w http.ResponseWriter, r *http.Request) { - resp := map[string][]th{ - "web-connectivity": { - { - Addr: "https://2.th.ooni.org", - T: "https", - }, - { - Addr: "https://3.th.ooni.org", - T: "https", - }, - { - Addr: "https://0.th.ooni.org", - T: "https", - }, - { - Addr: "https://1.th.ooni.org", - T: "https", - }, - }, - } - data, err := json.Marshal(resp) - runtimex.PanicOnError(err, "json.Marshal failed") - w.Header().Add("Content-Type", "application/json") - w.Write(data) -} - -// newSession creates a new [mocks.Session]. -func newSession() model.ExperimentSession { - byteCounter := bytecounter.New() - resolver := &sessionresolver.Resolver{ - ByteCounter: byteCounter, - KVStore: &kvstore.Memory{}, - Logger: log.Log, - ProxyURL: nil, - } - txp := netxlite.NewHTTPTransportWithLoggerResolverAndOptionalProxyURL( - log.Log, resolver, nil, - ) - txp = bytecounter.WrapHTTPTransport(txp, byteCounter) - return &mocks.Session{ - MockGetTestHelpersByName: func(name string) ([]model.OOAPIService, bool) { - output := []model.OOAPIService{ - { - Address: "https://3.th.ooni.org", - Type: "https", - }, - { - Address: "https://2.th.ooni.org", - Type: "https", - }, - { - Address: "https://1.th.ooni.org", - Type: "https", - }, - { - Address: "https://0.th.ooni.org", - Type: "https", - }, - } - return output, true - }, - MockDefaultHTTPClient: func() model.HTTPClient { - return &http.Client{Transport: txp} - }, - MockFetchPsiphonConfig: nil, - MockFetchTorTargets: nil, - MockKeyValueStore: nil, - MockLogger: func() model.Logger { - return log.Log - }, - MockMaybeResolverIP: nil, - MockProbeASNString: nil, - MockProbeCC: nil, - MockProbeIP: nil, - MockProbeNetworkName: nil, - MockProxyURL: nil, - MockResolverIP: nil, - MockSoftwareName: nil, - MockSoftwareVersion: nil, - MockTempDir: nil, - MockTorArgs: nil, - MockTorBinary: nil, - MockTunnelDir: nil, - MockUserAgent: func() string { - return model.HTTPHeaderUserAgent - }, - MockNewExperimentBuilder: nil, - MockNewSubmitter: nil, - MockCheckIn: nil, - } -} diff --git a/internal/experiment/webconnectivitylte/qa_test.go b/internal/experiment/webconnectivitylte/qa_test.go index b3b599b2d6..ef0613c74c 100644 --- a/internal/experiment/webconnectivitylte/qa_test.go +++ b/internal/experiment/webconnectivitylte/qa_test.go @@ -1,4 +1,4 @@ -package webconnectivitylte +package webconnectivitylte_test import ( "context" @@ -11,6 +11,7 @@ import ( "github.com/apex/log" "github.com/ooni/netem" + "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netemx" @@ -117,14 +118,8 @@ func qaAddTHDomains(config *netem.DNSConfig) { func qaNewEnvironment() *netemx.QAEnv { return netemx.NewQAEnv( netemx.QAEnvOptionDNSOverUDPResolvers("8.8.4.4"), - netemx.QAEnvOptionHTTPServerWithFactory( - qaWebServerAddress, - netemx.QAEnvAlwaysReturnThisHandler(netemx.QAEnvDefaultHTTPHandler()), - ), - netemx.QAEnvOptionHTTPServerWithFactory( - qaZeroTHOoniOrg, - netemx.QAEnvAlwaysReturnThisHandler(qaNewMockedTestHelper()), - ), + netemx.QAEnvOptionHTTPServer(qaWebServerAddress, netemx.QAEnvDefaultHTTPHandler()), + netemx.QAEnvOptionHTTPServer(qaZeroTHOoniOrg, qaNewMockedTestHelper()), ) } @@ -198,7 +193,7 @@ func qaRunWithURL(input string, setISPResolverConfig func(*netem.DNSConfig), setDPI(env.DPIEngine()) // create the measurer and the context - measurer := NewExperimentMeasurer(&Config{}) + measurer := webconnectivitylte.NewExperimentMeasurer(&webconnectivitylte.Config{}) ctx := context.Background() // create a new measurement diff --git a/internal/netemx/adapter.go b/internal/netemx/adapter.go index 5bd8880435..02bfa036ae 100644 --- a/internal/netemx/adapter.go +++ b/internal/netemx/adapter.go @@ -22,11 +22,6 @@ func WithCustomTProxy(tproxy netem.UnderlyingNetwork, function func()) { netxlite.WithCustomTProxy(&adapter{tproxy}, function) } -// GetCustomTProxy returns the tproxy wrapped by the adapter. -func GetCustomTProxy(tproxy netem.UnderlyingNetwork) model.UnderlyingNetwork { - return &adapter{tproxy} -} - // adapter adapts [netem.UnderlyingNetwork] to [model.UnderlyingNetwork]. type adapter struct { tp netem.UnderlyingNetwork diff --git a/internal/netemx/dns.go b/internal/netemx/dns.go deleted file mode 100644 index 1ce5f4d779..0000000000 --- a/internal/netemx/dns.go +++ /dev/null @@ -1,62 +0,0 @@ -package netemx - -import ( - "io" - "net" - "net/http" - "sync" - - "github.com/miekg/dns" - "github.com/ooni/probe-cli/v3/internal/netxlite/filtering" - "github.com/ooni/probe-cli/v3/internal/runtimex" -) - -type DoHServer struct { - rec map[string]net.IP - mu sync.Mutex -} - -func (p *DoHServer) AddRecord(domain string, ip string) { - defer p.mu.Unlock() - p.mu.Lock() - if p.rec == nil { - p.rec = make(map[string]net.IP) - } - p.rec[domain+"."] = net.ParseIP(ip) -} - -func (p *DoHServer) lookup(name string) (net.IP, bool) { - defer p.mu.Unlock() - p.mu.Lock() - ip, found := p.rec[name] - return ip, found -} - -func (p *DoHServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer p.HTTPPanicToInternalServerError(w) - rawQuery, err := io.ReadAll(r.Body) - runtimex.PanicOnError(err, "io.ReadAll failed") - query := &dns.Msg{} - err = query.Unpack(rawQuery) - runtimex.PanicOnError(err, "query.Unpack failed") - runtimex.PanicIfTrue(query.Response, "is a response") - - ip, found := p.lookup(query.Question[0].Name) - var response *dns.Msg - if found { - response = filtering.DNSComposeResponse(query, ip) - } else { - response = &dns.Msg{} - response.SetRcode(query, dns.RcodeNameError) - } - rawResponse, err := response.Pack() - runtimex.PanicOnError(err, "response.Pack failed") - w.Header().Add("content-type", "application/dns-message") - w.Write(rawResponse) -} - -func (p *DoHServer) HTTPPanicToInternalServerError(w http.ResponseWriter) { - if r := recover(); r != nil { - w.WriteHeader(500) - } -} diff --git a/internal/netemx/geoip.go b/internal/netemx/geoip.go deleted file mode 100644 index 28f93c2992..0000000000 --- a/internal/netemx/geoip.go +++ /dev/null @@ -1,12 +0,0 @@ -package netemx - -import "net/http" - -type GeoIPLookup struct{} - -func (p *GeoIPLookup) ServeHTTP(w http.ResponseWriter, r *http.Request) { - resp := `99.83.231.61OKUSUSAUnited States of AmericaWashingtonSeattle9810847.5413-122.3129America/Los_Angeles` - - w.Header().Add("Content-Type", "text/xml") - w.Write([]byte(resp)) -} diff --git a/internal/netemx/qaenv.go b/internal/netemx/qaenv.go index 9c9aeae98d..1b47ace68e 100644 --- a/internal/netemx/qaenv.go +++ b/internal/netemx/qaenv.go @@ -36,8 +36,8 @@ type qaEnvConfig struct { // dnsOverUDPResolvers contains the DNS-over-UDP resolvers to create. dnsOverUDPResolvers []string - // httpServers contains factory functions for the HTTP servers to create. - httpServers map[string]QAEnvHTTPHandlerFactory + // httpServers contains the HTTP servers to create. + httpServers map[string]http.Handler // ispResolver is the ISP resolver to use. ispResolver string @@ -80,38 +80,16 @@ func QAEnvOptionDNSOverUDPResolvers(ipAddrs ...string) QAEnvOption { } } -// QAEnvHTTPHandlerFactory constructs an [http.Handler] using the given underlying network. -type QAEnvHTTPHandlerFactory interface { - NewHandler(net netem.UnderlyingNetwork) http.Handler -} - -// QAEnvOptionHTTPServerWithFactory adds the given HTTP server as a factory function. -// If you do not set this option we will not create any HTTP server. -func QAEnvOptionHTTPServerWithFactory(ipAddr string, factory QAEnvHTTPHandlerFactory) QAEnvOption { +// QAEnvOptionHTTPServer adds the given HTTP server. If you do not set this option +// we will not create any HTTP server. +func QAEnvOptionHTTPServer(ipAddr string, handler http.Handler) QAEnvOption { runtimex.Assert(net.ParseIP(ipAddr) != nil, "not an IP addr") - runtimex.Assert(factory != nil, "passed a nil handler factory") - + runtimex.Assert(handler != nil, "passed a nil handler") return func(config *qaEnvConfig) { - config.httpServers[ipAddr] = factory + config.httpServers[ipAddr] = handler } } -// defaultHTTPHandlerFactory is the default handler factory that just returns a given [http.Handler]. -type defaultHTTPHandlerFactory struct { - handler http.Handler -} - -// NewHandler implements QAEnvHTTPHandlerFactory.NewHandler. -func (f defaultHTTPHandlerFactory) NewHandler(net netem.UnderlyingNetwork) http.Handler { - return f.handler -} - -// QAEnvAlwaysReturnThisHandler returns a QAEnvHTTPHandlerFactory such that we can always use the -// new API that requires a httpHandlerFactory. -func QAEnvAlwaysReturnThisHandler(handler http.Handler) QAEnvHTTPHandlerFactory { - return &defaultHTTPHandlerFactory{handler} -} - // QAEnvOptionISPResolverAddress sets the ISP's resolver IP address. If you do not set this option // we will use [QAEnvDefaultISPResolverAddress] as the address. func QAEnvOptionISPResolverAddress(ipAddr string) QAEnvOption { @@ -164,9 +142,6 @@ type QAEnv struct { // clientStack is the client stack to use. clientStack *netem.UNetStack - // serverStacks are the server stacks to use. - serverStacks map[string]*netem.UNetStack - // closables contains all entities where we have to take care of closing. closables []io.Closer @@ -186,11 +161,6 @@ type QAEnv struct { topology *netem.StarTopology } -// GetServerStack returns the server stack at the given address. -func (env *QAEnv) GetServerStack(addr string) *netem.UNetStack { - return env.serverStacks[addr] -} - // NewQAEnv creates a new [QAEnv]. func NewQAEnv(options ...QAEnvOption) *QAEnv { // initialize the configuration @@ -198,7 +168,7 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { clientAddress: QAEnvDefaultClientAddress, clientNICWrapper: nil, dnsOverUDPResolvers: []string{}, - httpServers: map[string]QAEnvHTTPHandlerFactory{}, + httpServers: map[string]http.Handler{}, ispResolver: QAEnvDefaultISPResolverAddress, logger: model.DiscardLogger, netStacks: map[string]QAEnvNetStackHandler{}, @@ -214,7 +184,6 @@ func NewQAEnv(options ...QAEnvOption) *QAEnv { env := &QAEnv{ clientNICWrapper: config.clientNICWrapper, clientStack: nil, - serverStacks: make(map[string]*netem.UNetStack), closables: []io.Closer{}, ispResolverConfig: netem.NewDNSConfig(), dpi: netem.NewDPIEngine(config.logger), @@ -312,7 +281,7 @@ func (env *QAEnv) mustNewHTTPServers(config *qaEnvConfig) (closables []io.Closer runtimex.Assert(len(config.dnsOverUDPResolvers) >= 1, "expected at least one DNS resolver") resolver := config.dnsOverUDPResolvers[0] - for addr, factory := range config.httpServers { + for addr, handler := range config.httpServers { // Create the server's TCP/IP stack // // Note: because the stack is created using topology.AddHost, we don't @@ -326,40 +295,34 @@ func (env *QAEnv) mustNewHTTPServers(config *qaEnvConfig) (closables []io.Closer RightToLeftDelay: time.Millisecond, }, )) - env.serverStacks[addr] = stack - handler := factory.NewHandler(stack) - closables = env.serverListen(stack, handler, addr) - } - return -} + ipAddr := net.ParseIP(addr) + runtimex.Assert(ipAddr != nil, "invalid IP addr") -func (env *QAEnv) serverListen(stack *netem.UNetStack, handler http.Handler, addr string) (closables []io.Closer) { - ipAddr := net.ParseIP(addr) - runtimex.Assert(ipAddr != nil, "invalid IP addr") + // listen for HTTP + { + listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 80})) + srv := &http.Server{Handler: handler} + closables = append(closables, srv) + go srv.Serve(listener) + } - // listen for HTTP - { - listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 80})) - srv := &http.Server{Handler: handler} - closables = append(closables, srv) - go srv.Serve(listener) - } + // listen for HTTPS + { + listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 443})) + srv := &http.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} + closables = append(closables, srv) + go srv.ServeTLS(listener, "", "") + } - // listen for HTTPS - { - listener := runtimex.Try1(stack.ListenTCP("tcp", &net.TCPAddr{IP: ipAddr, Port: 443})) - srv := &http.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} - closables = append(closables, srv) - go srv.ServeTLS(listener, "", "") - } + // listen for HTTP3 + { + listener := runtimex.Try1(stack.ListenUDP("udp", &net.UDPAddr{IP: ipAddr, Port: 443})) + srv := &http3.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} + closables = append(closables, listener, srv) + go srv.Serve(listener) - // listen for HTTP3 - { - listener := runtimex.Try1(stack.ListenUDP("udp", &net.UDPAddr{IP: ipAddr, Port: 443})) - srv := &http3.Server{TLSConfig: stack.ServerTLSConfig(), Handler: handler} - closables = append(closables, listener, srv) - go srv.Serve(listener) + } } return } From e0fa7c6fd856a64bea49683433e54fb5df390b97 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 14:57:47 +0200 Subject: [PATCH 23/25] start to add some basic tests --- internal/netxlite/net.go | 62 ++++++++-------- internal/netxlite/net_test.go | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 internal/netxlite/net_test.go diff --git a/internal/netxlite/net.go b/internal/netxlite/net.go index 358fc29184..54b0eb7e7d 100644 --- a/internal/netxlite/net.go +++ b/internal/netxlite/net.go @@ -1,47 +1,51 @@ package netxlite // -// Net is a high-level structure that provides constructors for basic netxlite network operations -// using a custom Underlying Network. +// Net is a high-level structure that provides constructors for basic netxlite +// network operations using a custom model.UnderlyingNetwork. // -import ( - "github.com/ooni/probe-cli/v3/internal/model" -) +import "github.com/ooni/probe-cli/v3/internal/model" -// Net contains a [model.UnderlyingNetwork] to perform network operations. -type Net struct { +// TODO(bassosimone,kelmenhorst): we should gradually refactor the top-level netxlite +// functions to operate on a [Net] struct using a nil-initialized Underlying field. + +// Netx allows constructing netxlite data types using a specific [model.UnderlyingNetwork]. +type Netx struct { + // Underlying is the OPTIONAL [model.UnderlyingNetwork] to use. Leaving this field + // nil makes this implementation functionally equivalent to netxlite top-level functions. Underlying model.UnderlyingNetwork } -func (n *Net) tproxyNilSafeProvider() *tproxyNilSafeProvider { +// tproxyNilSafeProvider wraps the [model.UnderlyingNetwork] using a [tproxyNilSafeProvider]. +func (n *Netx) tproxyNilSafeProvider() *tproxyNilSafeProvider { return &tproxyNilSafeProvider{n.Underlying} } -// NewStdlibResolver is like netxlite.NewStdlibResolver but -// the constructed resolver uses the given [UnderlyingNetwork]. -func (n *Net) NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver { +// NewStdlibResolver is like [netxlite.NewStdlibResolver] but the constructed [model.Resolver] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver { unwrapped := &resolverSystem{ t: WrapDNSTransport(&dnsOverGetaddrinfoTransport{provider: n.tproxyNilSafeProvider()}, wrappers...), } return WrapResolver(logger, unwrapped) } -// NewDialerWithResolver is like netxlite.NewDialerWithResolver but -// the constructed dialer uses the given [UnderlyingNetwork]. -func (n *Net) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { +// NewDialerWithResolver is like [netxlite.NewDialerWithResolver] but the constructed [model.Dialer] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer { return WrapDialer(dl, r, &DialerSystem{provider: n.tproxyNilSafeProvider()}, w...) } -// NewQUICListener is like netxlite.NewQUICListener but -// the constructed listener uses the given [UnderlyingNetwork]. -func (n *Net) NewQUICListener() model.QUICListener { +// NewQUICListener is like [netxlite.NewQUICListener] but the constructed [model.QUICListener] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewQUICListener() model.QUICListener { return &quicListenerErrWrapper{&quicListenerStdlib{provider: n.tproxyNilSafeProvider()}} } -// NewQUICDialerWithResolver is like netxlite.NewQUICDialerWithResolver but -// the constructed QUIC dialer uses the given [UnderlyingNetwork]. -func (n *Net) NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLogger, +// NewQUICDialerWithResolver is like [netxlite.NewQUICDialerWithResolver] but the constructed +// [model.QUICDialer] uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewQUICDialerWithResolver(listener model.QUICListener, logger model.DebugLogger, resolver model.Resolver, wrappers ...model.QUICDialerWrapper) (outDialer model.QUICDialer) { baseDialer := &quicDialerQUICGo{ QUICListener: listener, @@ -50,23 +54,23 @@ func (n *Net) NewQUICDialerWithResolver(listener model.QUICListener, logger mode return WrapQUICDialer(logger, resolver, baseDialer, wrappers...) } -// NewTLSHandshakerStdlib is like netxlite.NewTLSHandshakerStdlib but -// the constructed handshaker uses the given [UnderlyingNetwork]. -func (n *Net) NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { +// NewTLSHandshakerStdlib is like [netxlite.NewTLSHandshakerStdlib] but the constructed [model.TLSHandshaker] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker { return newTLSHandshakerLogger(&tlsHandshakerConfigurable{provider: n.tproxyNilSafeProvider()}, logger) } -// NewHTTPTransportStdlib is like netxlite.NewHTTPTransportStdlib but -// the constructed transport uses the given [UnderlyingNetwork]. -func (n *Net) NewHTTPTransportStdlib(logger model.DebugLogger) model.HTTPTransport { +// NewHTTPTransportStdlib is like [netxlite.NewHTTPTransportStdlib] but the constructed [model.HTTPTransport] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewHTTPTransportStdlib(logger model.DebugLogger) model.HTTPTransport { dialer := n.NewDialerWithResolver(logger, n.NewStdlibResolver(logger)) tlsDialer := NewTLSDialer(dialer, n.NewTLSHandshakerStdlib(logger)) return NewHTTPTransport(logger, dialer, tlsDialer) } -// NewHTTP3TransportStdlib is like netxlite.NewHTTP3TransportStdlib but -// the constructed transport uses the given [UnderlyingNetwork]. -func (n *Net) NewHTTP3TransportStdlib(logger model.DebugLogger) model.HTTPTransport { +// NewHTTP3TransportStdlib is like [netxlite.NewHTTP3TransportStdlib] but the constructed [model.HTTPTransport] +// uses the [UnderlyingNetwork] configured inside the [Net] structure. +func (n *Netx) NewHTTP3TransportStdlib(logger model.DebugLogger) model.HTTPTransport { ql := n.NewQUICListener() reso := n.NewStdlibResolver(logger) qd := n.NewQUICDialerWithResolver(ql, logger, reso) diff --git a/internal/netxlite/net_test.go b/internal/netxlite/net_test.go new file mode 100644 index 0000000000..318fbd4d5c --- /dev/null +++ b/internal/netxlite/net_test.go @@ -0,0 +1,130 @@ +package netxlite + +import ( + "context" + "crypto/tls" + "errors" + "net" + "testing" + "time" + + "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/quic-go/quic-go" +) + +func TestNetx(t *testing.T) { + t.Run("NewStdlibResolver", func(t *testing.T) { + expected := errors.New("mocked error") + netx := &Netx{&mocks.UnderlyingNetwork{ + MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { + return nil, "", expected + }, + MockGetaddrinfoResolverNetwork: func() string { + return "antani" + }, + }} + + reso := netx.NewStdlibResolver(model.DiscardLogger) + + if reso.Network() != "antani" { + t.Fatal("unexpected network") + } + + addrs, err := reso.LookupHost(context.Background(), "dns.google") + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + if len(addrs) != 0 { + t.Fatal("unexpected addrs") + } + }) + + t.Run("NewDialerWithResolver", func(t *testing.T) { + netx := &Netx{&mocks.UnderlyingNetwork{ + MockDialContext: func(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { + conn := &mocks.Conn{ + MockRemoteAddr: func() net.Addr { + addr := &mocks.Addr{ + MockString: func() string { + return address + }, + MockNetwork: func() string { + return network + }, + } + return addr + }, + } + return conn, nil + }, + MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { + return []string{"8.8.8.8"}, "", nil + }, + MockGetaddrinfoResolverNetwork: func() string { + return "antani" + }, + }} + + reso := netx.NewStdlibResolver(model.DiscardLogger) + dialer := netx.NewDialerWithResolver(model.DiscardLogger, reso) + + conn, err := dialer.DialContext(context.Background(), "tcp", "dns.google:443") + if err != nil { + t.Fatal(err) + } + + remoteAddr := conn.RemoteAddr() + if remoteAddr.String() != "8.8.8.8:443" { + t.Fatal("unexpected remote addr string") + } + if remoteAddr.Network() != "tcp" { + t.Fatal("unexpected remote addr network") + } + }) + + t.Run("NewQUICListener", func(t *testing.T) { + expected := errors.New("mocked error") + netx := &Netx{&mocks.UnderlyingNetwork{ + MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { + return nil, expected + }, + }} + + listener := netx.NewQUICListener() + conn, err := listener.Listen(&net.UDPAddr{}) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + if conn != nil { + t.Fatal("expected nil conn") + } + }) + + t.Run("NewQUICDialerWithResolver", func(t *testing.T) { + expected := errors.New("mocked error") + netx := &Netx{&mocks.UnderlyingNetwork{ + MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { + return nil, expected + }, + MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { + return []string{"8.8.8.8"}, "", nil + }, + MockGetaddrinfoResolverNetwork: func() string { + return "antani" + }, + }} + + reso := netx.NewStdlibResolver(model.DiscardLogger) + ql := netx.NewQUICListener() + dialer := netx.NewQUICDialerWithResolver(ql, model.DiscardLogger, reso) + + conn, err := dialer.DialContext(context.Background(), "dns.google:443", &tls.Config{}, &quic.Config{}) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + if conn != nil { + t.Fatal("expected nil conn") + } + }) +} From 702548d9221327531360292ec615c15136906262 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 15:47:11 +0200 Subject: [PATCH 24/25] x --- internal/netemx/adapter.go | 43 +-------- internal/netxlite/net_test.go | 130 -------------------------- internal/netxlite/netem.go | 46 +++++++++ internal/netxlite/{net.go => netx.go} | 0 internal/netxlite/netx_test.go | 116 +++++++++++++++++++++++ 5 files changed, 163 insertions(+), 172 deletions(-) delete mode 100644 internal/netxlite/net_test.go create mode 100644 internal/netxlite/netem.go rename internal/netxlite/{net.go => netx.go} (100%) create mode 100644 internal/netxlite/netx_test.go diff --git a/internal/netemx/adapter.go b/internal/netemx/adapter.go index 02bfa036ae..4ddaba9509 100644 --- a/internal/netemx/adapter.go +++ b/internal/netemx/adapter.go @@ -5,53 +5,12 @@ package netemx // import ( - "context" - "crypto/x509" - "net" - "time" - "github.com/ooni/netem" - "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/runtimex" ) // WithCustomTProxy executes the given function using the given [netem.UnderlyingNetwork] // as the [model.UnderlyingNetwork] used by the [netxlite] package. func WithCustomTProxy(tproxy netem.UnderlyingNetwork, function func()) { - netxlite.WithCustomTProxy(&adapter{tproxy}, function) -} - -// adapter adapts [netem.UnderlyingNetwork] to [model.UnderlyingNetwork]. -type adapter struct { - tp netem.UnderlyingNetwork -} - -var _ model.UnderlyingNetwork = &adapter{} - -// DefaultCertPool implements model.UnderlyingNetwork -func (a *adapter) DefaultCertPool() *x509.CertPool { - return runtimex.Try1(a.tp.DefaultCertPool()) -} - -// DialContext implements model.UnderlyingNetwork -func (a *adapter) DialContext(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - return a.tp.DialContext(ctx, network, address) -} - -// GetaddrinfoLookupANY implements model.UnderlyingNetwork -func (a *adapter) GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { - return a.tp.GetaddrinfoLookupANY(ctx, domain) -} - -// GetaddrinfoResolverNetwork implements model.UnderlyingNetwork -func (a *adapter) GetaddrinfoResolverNetwork() string { - return a.tp.GetaddrinfoResolverNetwork() -} - -// ListenUDP implements model.UnderlyingNetwork -func (a *adapter) ListenUDP(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { - return a.tp.ListenUDP(network, addr) + netxlite.WithCustomTProxy(&netxlite.NetemUnderlyingNetworkAdapter{UNet: tproxy}, function) } diff --git a/internal/netxlite/net_test.go b/internal/netxlite/net_test.go deleted file mode 100644 index 318fbd4d5c..0000000000 --- a/internal/netxlite/net_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package netxlite - -import ( - "context" - "crypto/tls" - "errors" - "net" - "testing" - "time" - - "github.com/ooni/probe-cli/v3/internal/mocks" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/quic-go/quic-go" -) - -func TestNetx(t *testing.T) { - t.Run("NewStdlibResolver", func(t *testing.T) { - expected := errors.New("mocked error") - netx := &Netx{&mocks.UnderlyingNetwork{ - MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { - return nil, "", expected - }, - MockGetaddrinfoResolverNetwork: func() string { - return "antani" - }, - }} - - reso := netx.NewStdlibResolver(model.DiscardLogger) - - if reso.Network() != "antani" { - t.Fatal("unexpected network") - } - - addrs, err := reso.LookupHost(context.Background(), "dns.google") - if !errors.Is(err, expected) { - t.Fatal("unexpected err") - } - if len(addrs) != 0 { - t.Fatal("unexpected addrs") - } - }) - - t.Run("NewDialerWithResolver", func(t *testing.T) { - netx := &Netx{&mocks.UnderlyingNetwork{ - MockDialContext: func(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { - conn := &mocks.Conn{ - MockRemoteAddr: func() net.Addr { - addr := &mocks.Addr{ - MockString: func() string { - return address - }, - MockNetwork: func() string { - return network - }, - } - return addr - }, - } - return conn, nil - }, - MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { - return []string{"8.8.8.8"}, "", nil - }, - MockGetaddrinfoResolverNetwork: func() string { - return "antani" - }, - }} - - reso := netx.NewStdlibResolver(model.DiscardLogger) - dialer := netx.NewDialerWithResolver(model.DiscardLogger, reso) - - conn, err := dialer.DialContext(context.Background(), "tcp", "dns.google:443") - if err != nil { - t.Fatal(err) - } - - remoteAddr := conn.RemoteAddr() - if remoteAddr.String() != "8.8.8.8:443" { - t.Fatal("unexpected remote addr string") - } - if remoteAddr.Network() != "tcp" { - t.Fatal("unexpected remote addr network") - } - }) - - t.Run("NewQUICListener", func(t *testing.T) { - expected := errors.New("mocked error") - netx := &Netx{&mocks.UnderlyingNetwork{ - MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { - return nil, expected - }, - }} - - listener := netx.NewQUICListener() - conn, err := listener.Listen(&net.UDPAddr{}) - if !errors.Is(err, expected) { - t.Fatal("unexpected err") - } - if conn != nil { - t.Fatal("expected nil conn") - } - }) - - t.Run("NewQUICDialerWithResolver", func(t *testing.T) { - expected := errors.New("mocked error") - netx := &Netx{&mocks.UnderlyingNetwork{ - MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { - return nil, expected - }, - MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) { - return []string{"8.8.8.8"}, "", nil - }, - MockGetaddrinfoResolverNetwork: func() string { - return "antani" - }, - }} - - reso := netx.NewStdlibResolver(model.DiscardLogger) - ql := netx.NewQUICListener() - dialer := netx.NewQUICDialerWithResolver(ql, model.DiscardLogger, reso) - - conn, err := dialer.DialContext(context.Background(), "dns.google:443", &tls.Config{}, &quic.Config{}) - if !errors.Is(err, expected) { - t.Fatal("unexpected err") - } - if conn != nil { - t.Fatal("expected nil conn") - } - }) -} diff --git a/internal/netxlite/netem.go b/internal/netxlite/netem.go new file mode 100644 index 0000000000..ba1dfc8004 --- /dev/null +++ b/internal/netxlite/netem.go @@ -0,0 +1,46 @@ +package netxlite + +import ( + "context" + "crypto/x509" + "net" + "time" + + "github.com/ooni/netem" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/runtimex" +) + +// NetemUnderlyingNetworkAdapter adapts [netem.UnderlyingNetwork] to [model.UnderlyingNetwork]. +type NetemUnderlyingNetworkAdapter struct { + UNet netem.UnderlyingNetwork +} + +var _ model.UnderlyingNetwork = &NetemUnderlyingNetworkAdapter{} + +// DefaultCertPool implements model.UnderlyingNetwork +func (a *NetemUnderlyingNetworkAdapter) DefaultCertPool() *x509.CertPool { + return runtimex.Try1(a.UNet.DefaultCertPool()) +} + +// DialContext implements model.UnderlyingNetwork +func (a *NetemUnderlyingNetworkAdapter) DialContext(ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + return a.UNet.DialContext(ctx, network, address) +} + +// GetaddrinfoLookupANY implements model.UnderlyingNetwork +func (a *NetemUnderlyingNetworkAdapter) GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) { + return a.UNet.GetaddrinfoLookupANY(ctx, domain) +} + +// GetaddrinfoResolverNetwork implements model.UnderlyingNetwork +func (a *NetemUnderlyingNetworkAdapter) GetaddrinfoResolverNetwork() string { + return a.UNet.GetaddrinfoResolverNetwork() +} + +// ListenUDP implements model.UnderlyingNetwork +func (a *NetemUnderlyingNetworkAdapter) ListenUDP(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) { + return a.UNet.ListenUDP(network, addr) +} diff --git a/internal/netxlite/net.go b/internal/netxlite/netx.go similarity index 100% rename from internal/netxlite/net.go rename to internal/netxlite/netx.go diff --git a/internal/netxlite/netx_test.go b/internal/netxlite/netx_test.go new file mode 100644 index 0000000000..c502efca70 --- /dev/null +++ b/internal/netxlite/netx_test.go @@ -0,0 +1,116 @@ +package netxlite + +import ( + "context" + "net" + "net/http" + "testing" + + "github.com/apex/log" + "github.com/google/go-cmp/cmp" + "github.com/ooni/netem" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +func TestNetx(t *testing.T) { + // create a star network topology + topology := runtimex.Try1(netem.NewStarTopology(log.Log)) + defer topology.Close() + + // constants for the IP address we're using + const ( + clientAddress = "130.192.91.211" + exampleComAddress = "93.184.216.34" + quad8Address = "8.8.8.8" + ) + + // create and configure the name server + nameServerStack := runtimex.Try1(topology.AddHost(quad8Address, quad8Address, &netem.LinkConfig{})) + nameServerConfig := netem.NewDNSConfig() + nameServerConfig.AddRecord("www.example.com", "web01.example.com", exampleComAddress) + nameServer := runtimex.Try1(netem.NewDNSServer(log.Log, nameServerStack, quad8Address, nameServerConfig)) + defer nameServer.Close() + + // create the web server handler + bonsoirElliot := []byte("Bonsoir, Elliot!\r\n") + webServerStack := runtimex.Try1(topology.AddHost(exampleComAddress, quad8Address, &netem.LinkConfig{})) + webServerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(bonsoirElliot) + }) + + // listen for HTTPS requests using the above handler + webServerTCPAddress := &net.TCPAddr{ + IP: net.ParseIP(exampleComAddress), + Port: 443, + Zone: "", + } + webServerTCPListener := runtimex.Try1(webServerStack.ListenTCP("tcp", webServerTCPAddress)) + webServerTCPServer := &http.Server{ + Handler: webServerHandler, + TLSConfig: webServerStack.ServerTLSConfig(), + } + go webServerTCPServer.ServeTLS(webServerTCPListener, "", "") + defer webServerTCPServer.Close() + + // listen for HTTP/3 requests using the above handler + webServerUDPAddress := &net.UDPAddr{ + IP: net.ParseIP(exampleComAddress), + Port: 443, + Zone: "", + } + webServerUDPListener := runtimex.Try1(webServerStack.ListenUDP("udp", webServerUDPAddress)) + webServerUDPServer := &http3.Server{ + TLSConfig: webServerStack.ServerTLSConfig(), + QuicConfig: &quic.Config{}, + Handler: webServerHandler, + } + go webServerUDPServer.Serve(webServerUDPListener) + defer webServerUDPServer.Close() + + // create the client userspace TCP/IP stack and the corresponding netx + clientStack := runtimex.Try1(topology.AddHost(clientAddress, quad8Address, &netem.LinkConfig{})) + underlyingNetwork := &NetemUnderlyingNetworkAdapter{clientStack} + netx := &Netx{underlyingNetwork} + + t.Run("HTTPS fetch", func(t *testing.T) { + txp := netx.NewHTTPTransportStdlib(log.Log) + client := &http.Client{Transport: txp} + resp, err := client.Get("https://www.example.com/") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatal("unexpected status code") + } + body, err := ReadAllContext(context.Background(), resp.Body) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(bonsoirElliot, body); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("HTTP/3 fetch", func(t *testing.T) { + txp := netx.NewHTTP3TransportStdlib(log.Log) + client := &http.Client{Transport: txp} + resp, err := client.Get("https://www.example.com/") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatal("unexpected status code") + } + body, err := ReadAllContext(context.Background(), resp.Body) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(bonsoirElliot, body); diff != "" { + t.Fatal(diff) + } + }) +} From 683021594cea55e84ebb5f8f9bdfdd4116219165 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 23 Aug 2023 15:50:37 +0200 Subject: [PATCH 25/25] x --- internal/netxlite/netx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/netxlite/netx.go b/internal/netxlite/netx.go index 54b0eb7e7d..dfaf4152b0 100644 --- a/internal/netxlite/netx.go +++ b/internal/netxlite/netx.go @@ -1,7 +1,7 @@ package netxlite // -// Net is a high-level structure that provides constructors for basic netxlite +// Netx is a high-level structure that provides constructors for basic netxlite // network operations using a custom model.UnderlyingNetwork. //