package webconnectivity // // TestKeys for web_connectivity. // // Note: for historical reasons, we call TestKeys the JSON object // containing the results produced by OONI experiments. // import ( "sort" "sync" "github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/tracex" ) // TestKeys contains the results produced by web_connectivity. type TestKeys struct { // Agent is the HTTP agent we use. Agent string `json:"agent"` // ClientResolver is the IPv4 of the resolver used by getaddrinfo. ClientResolver string `json:"client_resolver"` // Retries is a legacy field always set to nil by web_connectivity@v0.4.x Retries *int64 `json:"retries"` // SOCKSProxy is a legacy field always set to nil by web_connectivity@v0.4.x SOCKSProxy *string `json:"socksproxy"` // NetworkEvents contains network events. NetworkEvents []*model.ArchivalNetworkEvent `json:"network_events"` // DNSWhoami contains results of using the DNS whoami functionality for the // possibly cleartext resolvers that we're using. DNSWoami *DNSWhoamiInfo `json:"x_dns_whoami"` // DoH contains ancillary observations collected by DoH resolvers. DoH *TestKeysDoH `json:"x_doh"` // Do53 contains ancillary observations collected by Do53 resolvers. Do53 *TestKeysDo53 `json:"x_do53"` // DNSDuplicateResponses contains late/duplicate responses we didn't expect to receive from // a resolver (which may raise eyebrows if they're different). DNSDuplicateResponses []*model.ArchivalDNSLookupResult `json:"x_dns_duplicate_responses"` // Queries contains DNS queries. Queries []*model.ArchivalDNSLookupResult `json:"queries"` // Requests contains HTTP results. Requests []*model.ArchivalHTTPRequestResult `json:"requests"` // TCPConnect contains TCP connect results. TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"` // TLSHandshakes contains TLS handshakes results. TLSHandshakes []*model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"` // ControlRequest is the control request we sent. ControlRequest *webconnectivity.ControlRequest `json:"x_control_request"` // Control contains the TH's response. Control *webconnectivity.ControlResponse `json:"control"` // ConnPriorityLog explains why Web Connectivity chose to use a given // ready-to-use HTTP(S) connection among many. ConnPriorityLog []*ConnPriorityLogEntry `json:"x_conn_priority_log"` // ControlFailure contains the failure of the control experiment. ControlFailure *string `json:"control_failure"` // DNSFlags describes specific DNS anomalies we observed. DNSFlags int64 `json:"x_dns_flags"` // DNSExperimentFailure indicates whether there was a failure in any // of the DNS experiments we performed. DNSExperimentFailure *string `json:"dns_experiment_failure"` // DNSConsistency indicates whether there is consistency between // the TH's DNS results and the probe's DNS results. DNSConsistency string `json:"dns_consistency"` // HTTPExperimentFailure indicates whether there was a failure in // the final HTTP request that we recorded. HTTPExperimentFailure *string `json:"http_experiment_failure"` // BlockingFlags explains why we think that the website is blocked. BlockingFlags int64 `json:"x_blocking_flags"` // NullNullFlags describes what the algorithm to avoid emitting // blocking = null, accessible = null measurements did NullNullFlags int64 `json:"x_null_null_flags"` // BodyLength match tells us whether the body length matches. BodyLengthMatch *bool `json:"body_length_match"` // HeadersMatch tells us whether the headers match. HeadersMatch *bool `json:"headers_match"` // StatusCodeMatch tells us whether the status code matches. StatusCodeMatch *bool `json:"status_code_match"` // TitleMatch tells us whether the title matches. TitleMatch *bool `json:"title_match"` // Blocking indicates the reason for blocking. This is notoriously a bad // type because it can be one of the following values: // // - "tcp_ip" // - "dns" // - "http-diff" // - "http-failure" // - false // - null // // In addition to having a ~bad type, this field has the issue that it // reduces the reason for blocking to an enum, whereas it's a set of flags, // hence we introduced the x_blocking_flags field. Blocking any `json:"blocking"` // Accessible indicates whether the resource is accessible. Possible // values for this field are: nil, true, and false. Accessible any `json:"accessible"` // fundamentalFailure indicates that some fundamental error occurred // in a background task. A fundamental error is something like a programmer // such as a failure to parse a URL that was hardcoded in the codebase. When // this class of errors happens, you certainly don't want to submit the // resulting measurement to the OONI collector. fundamentalFailure error // mu provides mutual exclusion for accessing the test keys. mu *sync.Mutex } // ConnPriorityLogEntry is an entry in the TestKeys.ConnPriorityLog slice. type ConnPriorityLogEntry struct { // Msg is the specific log entry Msg string `json:"msg"` // T is when this entry was generated T float64 `json:"t"` } // DNSWhoamiInfoEntry contains an entry for DNSWhoamiInfo. type DNSWhoamiInfoEntry struct { // Address is the IP address Address string `json:"address"` } // DNSWhoamiInfo contains info about DNS whoami. type DNSWhoamiInfo struct { // SystemV4 contains results related to the system resolver using IPv4. SystemV4 []DNSWhoamiInfoEntry `json:"system_v4"` // UDPv4 contains results related to an UDP resolver using IPv4. UDPv4 map[string][]DNSWhoamiInfoEntry `json:"udp_v4"` } // TestKeysDoH contains ancillary observations collected using DoH (e.g., the // DNS lookups, TCP connects, TLS handshakes caused by given DoH lookups). // // They are on a separate hierarchy to simplify processing. type TestKeysDoH struct { // NetworkEvents contains network events. NetworkEvents []*model.ArchivalNetworkEvent `json:"network_events"` // Queries contains DNS queries. Queries []*model.ArchivalDNSLookupResult `json:"queries"` // Requests contains HTTP results. Requests []*model.ArchivalHTTPRequestResult `json:"requests"` // TCPConnect contains TCP connect results. TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"` // TLSHandshakes contains TLS handshakes results. TLSHandshakes []*model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"` } // TestKeysDo53 contains ancillary observations collected using Do53. // // They are on a separate hierarchy to simplify processing. type TestKeysDo53 struct { // NetworkEvents contains network events. NetworkEvents []*model.ArchivalNetworkEvent `json:"network_events"` // Queries contains DNS queries. Queries []*model.ArchivalDNSLookupResult `json:"queries"` } // AppendNetworkEvents appends to NetworkEvents. func (tk *TestKeys) AppendNetworkEvents(v ...*model.ArchivalNetworkEvent) { tk.mu.Lock() tk.NetworkEvents = append(tk.NetworkEvents, v...) tk.mu.Unlock() } // AppendDNSLateReplies appends to DNSLateReplies. func (tk *TestKeys) AppendDNSLateReplies(v ...*model.ArchivalDNSLookupResult) { tk.mu.Lock() tk.DNSDuplicateResponses = append(tk.DNSDuplicateResponses, v...) tk.mu.Unlock() } // AppendQueries appends to Queries. func (tk *TestKeys) AppendQueries(v ...*model.ArchivalDNSLookupResult) { tk.mu.Lock() tk.Queries = append(tk.Queries, v...) tk.mu.Unlock() } // AppendRequests appends to Requests. func (tk *TestKeys) AppendRequests(v ...*model.ArchivalHTTPRequestResult) { tk.mu.Lock() // Implementation note: append at the front since the most recent // request must be at the beginning of the list. tk.Requests = append(v, tk.Requests...) tk.mu.Unlock() } // AppendTCPConnectResults appends to TCPConnect. func (tk *TestKeys) AppendTCPConnectResults(v ...*model.ArchivalTCPConnectResult) { tk.mu.Lock() tk.TCPConnect = append(tk.TCPConnect, v...) tk.mu.Unlock() } // AppendTLSHandshakes appends to TLSHandshakes. func (tk *TestKeys) AppendTLSHandshakes(v ...*model.ArchivalTLSOrQUICHandshakeResult) { tk.mu.Lock() tk.TLSHandshakes = append(tk.TLSHandshakes, v...) tk.mu.Unlock() } // SetControlRequest sets the value of controlRequest. func (tk *TestKeys) SetControlRequest(v *webconnectivity.ControlRequest) { tk.mu.Lock() tk.ControlRequest = v tk.mu.Unlock() } // SetControl sets the value of Control. func (tk *TestKeys) SetControl(v *webconnectivity.ControlResponse) { tk.mu.Lock() tk.Control = v tk.mu.Unlock() } // SetControlFailure sets the value of controlFailure. func (tk *TestKeys) SetControlFailure(err error) { tk.mu.Lock() tk.ControlFailure = tracex.NewFailure(err) tk.mu.Unlock() } // SetFundamentalFailure sets the value of fundamentalFailure. func (tk *TestKeys) SetFundamentalFailure(err error) { tk.mu.Lock() tk.fundamentalFailure = err tk.mu.Unlock() } // WithTestKeysDoH calls the given function with the mutex locked passing to // it as argument the pointer to the DoH field. func (tk *TestKeys) WithTestKeysDoH(f func(*TestKeysDoH)) { tk.mu.Lock() f(tk.DoH) tk.mu.Unlock() } // WithTestKeysDo53 calls the given function with the mutex locked passing to // it as argument the pointer to the Do53 field. func (tk *TestKeys) WithTestKeysDo53(f func(*TestKeysDo53)) { tk.mu.Lock() f(tk.Do53) tk.mu.Unlock() } // WithDNSWhoami calls the given function with the mutex locked passing to // it as argument the pointer to the DNSWhoami field. func (tk *TestKeys) WithDNSWhoami(fun func(*DNSWhoamiInfo)) { tk.mu.Lock() fun(tk.DNSWoami) tk.mu.Unlock() } // SetClientResolver sets the ClientResolver field. func (tk *TestKeys) SetClientResolver(value string) { tk.mu.Lock() tk.ClientResolver = value tk.mu.Unlock() } // AppendConnPriorityLogEntry appends an entry to ConnPriorityLog. func (tk *TestKeys) AppendConnPriorityLogEntry(entry *ConnPriorityLogEntry) { tk.mu.Lock() tk.ConnPriorityLog = append(tk.ConnPriorityLog, entry) tk.mu.Unlock() } // NewTestKeys creates a new instance of TestKeys. func NewTestKeys() *TestKeys { return &TestKeys{ Agent: "redirect", ClientResolver: "", Retries: nil, SOCKSProxy: nil, NetworkEvents: []*model.ArchivalNetworkEvent{}, DNSWoami: &DNSWhoamiInfo{ SystemV4: []DNSWhoamiInfoEntry{}, UDPv4: map[string][]DNSWhoamiInfoEntry{}, }, DoH: &TestKeysDoH{ NetworkEvents: []*model.ArchivalNetworkEvent{}, Queries: []*model.ArchivalDNSLookupResult{}, Requests: []*model.ArchivalHTTPRequestResult{}, TCPConnect: []*model.ArchivalTCPConnectResult{}, TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{}, }, Do53: &TestKeysDo53{ NetworkEvents: []*model.ArchivalNetworkEvent{}, Queries: []*model.ArchivalDNSLookupResult{}, }, DNSDuplicateResponses: []*model.ArchivalDNSLookupResult{}, Queries: []*model.ArchivalDNSLookupResult{}, Requests: []*model.ArchivalHTTPRequestResult{}, TCPConnect: []*model.ArchivalTCPConnectResult{}, TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{}, Control: nil, ConnPriorityLog: []*ConnPriorityLogEntry{}, ControlFailure: nil, DNSFlags: 0, DNSExperimentFailure: nil, DNSConsistency: "", HTTPExperimentFailure: nil, BlockingFlags: 0, NullNullFlags: 0, BodyLengthMatch: nil, HeadersMatch: nil, StatusCodeMatch: nil, TitleMatch: nil, Blocking: nil, Accessible: nil, ControlRequest: nil, fundamentalFailure: nil, mu: &sync.Mutex{}, } } // Finalize performs any delayed computation on the test keys. This function // must be called from the measurer after all the tasks have completed. func (tk *TestKeys) Finalize(logger model.Logger) { tk.analysisToplevel(logger) // Note: sort.SliceStable is WAI when the input slice is nil // as demonstrated by https://go.dev/play/p/znA4MyGFVHC sort.SliceStable(tk.NetworkEvents, func(i, j int) bool { return tk.NetworkEvents[i].T < tk.NetworkEvents[j].T }) }