package webconnectivity import "github.com/ooni/probe-cli/v3/internal/model" // // Core analysis // // These flags determine the context of TestKeys.Blocking. However, while .Blocking // is an enumeration, these flags allow to describe multiple blocking methods. const ( // analysisFlagDNSBlocking indicates there's blocking at the DNS level. analysisFlagDNSBlocking = 1 << iota // analysisFlagTCPIPBlocking indicates there's blocking at the TCP/IP level. analysisFlagTCPIPBlocking // analysisFlagTLSBlocking indicates there were TLS issues. analysisFlagTLSBlocking // analysisFlagHTTPBlocking indicates there was an HTTP failure. analysisFlagHTTPBlocking // analysisFlagHTTPDiff indicates there's an HTTP diff. analysisFlagHTTPDiff // analysisFlagSuccess indicates we did not detect any blocking. analysisFlagSuccess ) // analysisToplevel is the toplevel function that analyses the results // of the experiment once all network tasks have completed. // // The ultimate objective of this function is to set the toplevel flags // used by the backend to score results. These flags are: // // - blocking (and x_blocking_flags) which contain information about // the detected blocking method (or methods); // // - accessible which contains information on whether we think we // could access the resource somehow. // // Originally, Web Connectivity only had a blocking scalar value so // we could see ourselves in one of the following cases: // // +----------+------------+--------------------------+ // | Blocking | Accessible | Meaning | // +----------+------------+--------------------------+ // | null | null | Probe analysis error | // +----------+------------+--------------------------+ // | false | true | We detected no blocking | // +----------+------------+--------------------------+ // | "..." | false | We detected blocking | // +----------+------------+--------------------------+ // // While it would be possible in this implementation, which has a granular // definition of blocking (x_blocking_flags), to set accessible to mean // whether we could access the resource in some conditions, it seems quite // dangerous to deviate from the original behavior. // // Our code will NEVER set .Blocking or .Accessible outside of this function // and we'll instead rely on XBlockingFlags. This function's job is to call // other functions that compute the .XBlockingFlags and then to assign the value // of .Blocking and .Accessible from the .XBlockingFlags value. // // Accordingly, this is how we map the value of the .XBlockingFlags to the // values of .Blocking and .Accessible: // // +--------------------------------------+----------------+-------------+ // | .BlockingFlags | .Blocking | .Accessible | // +--------------------------------------+----------------+-------------+ // | (& DNSBlocking) != 0 | "dns" | false | // +--------------------------------------+----------------+-------------+ // | (& TCPIPBlocking) != 0 | "tcp_ip" | false | // +--------------------------------------+----------------+-------------+ // | (& (TLSBlocking|HTTPBlocking)) != 0 | "http-failure" | false | // +--------------------------------------+----------------+-------------+ // | (& HTTPDiff) != 0 | "http-diff" | false | // +--------------------------------------+----------------+-------------+ // | == FlagSuccess | false | true | // +--------------------------------------+----------------+-------------+ // | otherwise | null | null | // +--------------------------------------+----------------+-------------+ // // It's a very simple rule, that should preserve previous semantics. func (tk *TestKeys) analysisToplevel(logger model.Logger) { // Since we run after all tasks have completed (or so we assume) we're // not going to use any form of locking here. // these functions compute the value of XBlockingFlags tk.analysisDNSToplevel(logger) tk.analysisTCPIPToplevel(logger) tk.analysisTLSToplevel(logger) tk.analysisHTTPToplevel(logger) // now, let's determine .Accessible and .Blocking switch { case (tk.BlockingFlags & analysisFlagDNSBlocking) != 0: tk.Blocking = "dns" tk.Accessible = false logger.Warnf( "ANOMALY: flags=%d accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) case (tk.BlockingFlags & analysisFlagTCPIPBlocking) != 0: tk.Blocking = "tcp_ip" tk.Accessible = false logger.Warnf( "ANOMALY: flags=%d accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) case (tk.BlockingFlags & (analysisFlagTLSBlocking | analysisFlagHTTPBlocking)) != 0: tk.Blocking = "http-failure" tk.Accessible = false logger.Warnf("ANOMALY: flags=%d accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) case (tk.BlockingFlags & analysisFlagHTTPDiff) != 0: tk.Blocking = "http-diff" tk.Accessible = false logger.Warnf( "ANOMALY: flags=%d accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) case tk.BlockingFlags == analysisFlagSuccess: tk.Blocking = false tk.Accessible = true logger.Infof( "SUCCESS: flags=%d accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) default: if tk.analysisNullNullDetectNoAddrs(logger) { tk.Blocking = false tk.Accessible = false logger.Infof( "NO_AVAILABLE_ADDRS: flags=%d, accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) return } tk.Blocking = nil tk.Accessible = nil logger.Warnf( "UNKNOWN: flags=%d, accessible=%+v, blocking=%+v", tk.BlockingFlags, tk.Accessible, tk.Blocking, ) } } const ( // analysisFlagNullNullNoAddrs indicates neither the probe nor the TH were // able to get any IP addresses from any resolver. analysisFlagNullNullNoAddrs = 1 << iota ) // analysisNullNullDetectNoAddrs attempts to see whether we // ended up into the .Blocking = nil, .Accessible = nil case because // the domain is expired and all queries returned no addresses. // // See https://github.com/ooni/probe/issues/2290 for further // documentation about the issue we're solving here. // // It would be tempting to check specifically for NXDOMAIN here, but we // know it is problematic do that. In fact, on Android the getaddrinfo // resolver always returns EAI_NODATA on error, regardless of the actual // error that may have occurred in the Android DNS backend. // // See https://github.com/ooni/probe/issues/2029 for more information // on Android's getaddrinfo behavior. func (tk *TestKeys) analysisNullNullDetectNoAddrs(logger model.Logger) bool { if tk.Control == nil { // we need control data to say we're in this case return false } for _, query := range tk.Queries { if len(query.Answers) > 0 { // when a query has answers, we're not in the NoAddresses case return false } } if len(tk.TCPConnect) > 0 { // if we attempted TCP connect, we're not in the NoAddresses case return false } if len(tk.TLSHandshakes) > 0 { // if we attempted TLS handshakes, we're not in the NoAddresses case return false } if len(tk.Control.DNS.Addrs) > 0 { // when the TH resolved addresses, we're not in the NoAddresses case return false } if len(tk.Control.TCPConnect) > 0 { // when the TH used addresses, we're not in the NoAddresses case return false } logger.Infof("Neither the probe nor the TH resolved any addresses") tk.NullNullFlags |= analysisFlagNullNullNoAddrs return true }