webconnectivity@v0.5: handle successful https chains (#960)
This diff includes a rule to recover from the "measurement failed" state that kicks in when we have a chain of successful redirects from the client side leading to a webpage _and_ any URL in the chain uses HTTPS. See https://github.com/ooni/probe/issues/2307. While there, fix `i/e/w/iox.go` to avoid triggering the `./script/nocopyreadall.bash` script.
This commit is contained in:
		
							parent
							
								
									d289b80386
								
							
						
					
					
						commit
						6815dd8b2f
					
				
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -5,5 +5,6 @@ | |||||||
|             "-GOCACHE", |             "-GOCACHE", | ||||||
|             "-GOPATH" |             "-GOPATH" | ||||||
|         ] |         ] | ||||||
|     } |     }, | ||||||
|  |     "C_Cpp.default.configurationProvider": "ms-vscode.makefile-tools" | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package webconnectivity | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/ooni/probe-cli/v3/internal/model" | 	"github.com/ooni/probe-cli/v3/internal/model" | ||||||
| ) | ) | ||||||
| @ -138,12 +139,20 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { | |||||||
| 		tk.Blocking = false | 		tk.Blocking = false | ||||||
| 		tk.Accessible = true | 		tk.Accessible = true | ||||||
| 		logger.Infof( | 		logger.Infof( | ||||||
| 			"SUCCESS: flags=%d accessible=%+v, blocking=%+v", | 			"ACCESSIBLE: flags=%d accessible=%+v, blocking=%+v", | ||||||
| 			tk.BlockingFlags, tk.Accessible, tk.Blocking, | 			tk.BlockingFlags, tk.Accessible, tk.Blocking, | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 	default: | 	default: | ||||||
| 		if tk.analysisWebsiteDownDetectNoAddrs(logger) { | 		// NullNull remediation | ||||||
|  | 		// | ||||||
|  | 		// If we arrive here, the measurement has failed. However, there are a | ||||||
|  | 		// bunch of cases where we can still explain what happened by applying specific | ||||||
|  | 		// algorithms to detect edge cases. | ||||||
|  | 		// | ||||||
|  | 		// The relative order of these algorithsm matters. | ||||||
|  | 
 | ||||||
|  | 		if tk.analysisNullNullDetectNoAddrs(logger) { | ||||||
| 			tk.Blocking = false | 			tk.Blocking = false | ||||||
| 			tk.Accessible = false | 			tk.Accessible = false | ||||||
| 			logger.Infof( | 			logger.Infof( | ||||||
| @ -152,7 +161,8 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { | |||||||
| 			) | 			) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if tk.analysisWebsiteDownDetectAllConnectsFailed(logger) { | 
 | ||||||
|  | 		if tk.analysisNullNullDetectAllConnectsFailed(logger) { | ||||||
| 			tk.Blocking = false | 			tk.Blocking = false | ||||||
| 			tk.Accessible = false | 			tk.Accessible = false | ||||||
| 			logger.Infof( | 			logger.Infof( | ||||||
| @ -161,7 +171,8 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { | |||||||
| 			) | 			) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if tk.analysisWebsiteDownDetectTLSMisconfigured(logger) { | 
 | ||||||
|  | 		if tk.analysisNullNullDetectTLSMisconfigured(logger) { | ||||||
| 			tk.Blocking = false | 			tk.Blocking = false | ||||||
| 			tk.Accessible = false | 			tk.Accessible = false | ||||||
| 			logger.Infof( | 			logger.Infof( | ||||||
| @ -170,6 +181,17 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { | |||||||
| 			) | 			) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		if tk.analysisNullNullDetectSuccessfulHTTPS(logger) { | ||||||
|  | 			tk.Blocking = false | ||||||
|  | 			tk.Accessible = true | ||||||
|  | 			logger.Infof( | ||||||
|  | 				"ACCESSIBLE_HTTPS: flags=%d, accessible=%+v, blocking=%+v", | ||||||
|  | 				tk.BlockingFlags, tk.Accessible, tk.Blocking, | ||||||
|  | 			) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		tk.Blocking = nil | 		tk.Blocking = nil | ||||||
| 		tk.Accessible = nil | 		tk.Accessible = nil | ||||||
| 		logger.Warnf( | 		logger.Warnf( | ||||||
| @ -180,24 +202,83 @@ func (tk *TestKeys) analysisToplevel(logger model.Logger) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// analysisFlagWebsiteDownNoAddrs indicates neither the probe nor the TH were | 	// analysisFlagNullNullNoAddrs indicates neither the probe nor the TH were | ||||||
| 	// able to get any IP addresses from any resolver. | 	// able to get any IP addresses from any resolver. | ||||||
| 	analysisFlagWebsiteDownNoAddrs = 1 << iota | 	analysisFlagNullNullNoAddrs = 1 << iota | ||||||
| 
 | 
 | ||||||
| 	// analysisFlagWebsiteDownAllConnectsFailed indicates that all the connect | 	// analysisFlagNullNullAllConnectsFailed indicates that all the connect | ||||||
| 	// attempts failed both in the probe and in the test helper. | 	// attempts failed both in the probe and in the test helper. | ||||||
| 	analysisFlagWebsiteDownAllConnectsFailed | 	analysisFlagNullNullAllConnectsFailed | ||||||
| 
 | 
 | ||||||
| 	// analysisFlagWebsiteDownTLSMisconfigured indicates that all the TLS handshake | 	// analysisFlagNullNullTLSMisconfigured indicates that all the TLS handshake | ||||||
| 	// attempts failed both in the probe and in the test helper. | 	// attempts failed both in the probe and in the test helper. | ||||||
| 	analysisFlagWebsiteDownTLSMisconfigured | 	analysisFlagNullNullTLSMisconfigured | ||||||
|  | 
 | ||||||
|  | 	// analysisFlagNullNullSuccessfulHTTPS indicates that we had no TH data | ||||||
|  | 	// but all the HTTP requests used always HTTPS and never failed. | ||||||
|  | 	analysisFlagNullNullSuccessfulHTTPS | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // analysisWebsiteDownDetectTLSMisconfigured runs when .Blocking = nil and | // analysisNullNullDetectSuccessfulHTTPS runs when .Blocking = nil and | ||||||
|  | // .Accessible = nil to flag successul HTTPS measurements chains that | ||||||
|  | // occurred regardless of whatever else could have gone wrong. | ||||||
|  | // | ||||||
|  | // We need all requests to be HTTPS because an HTTP request in the | ||||||
|  | // chain breaks the ~reasonable assumption that our custom CA bundle | ||||||
|  | // is enough to protect against MITM. Of course, when we use this | ||||||
|  | // algorithm, we're not well positioned to flag server-side blocking. | ||||||
|  | // | ||||||
|  | // Version 0.4 of the probe implemented a similar algorithm, which | ||||||
|  | // however ran before other checks. Version, 0.5 on the contrary, runs | ||||||
|  | // this algorithm if any other heuristics failed. | ||||||
|  | // | ||||||
|  | // See https://github.com/ooni/probe/issues/2307 for more info. | ||||||
|  | func (tk *TestKeys) analysisNullNullDetectSuccessfulHTTPS(logger model.Logger) bool { | ||||||
|  | 
 | ||||||
|  | 	// the chain is sorted from most recent to oldest but it does | ||||||
|  | 	// not matter much since we need to walk all of it. | ||||||
|  | 	// | ||||||
|  | 	// CAVEAT: this code assumes we have a single request chain | ||||||
|  | 	// inside the .Requests field, which seems fine because it's | ||||||
|  | 	// what Web Connectivity should be doing. | ||||||
|  | 	for _, req := range tk.Requests { | ||||||
|  | 		URL, err := url.Parse(req.Request.URL) | ||||||
|  | 		if err != nil { | ||||||
|  | 			// this looks like a bug | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if URL.Scheme != "https" { | ||||||
|  | 			// the whole chain must be HTTPS | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if req.Failure != nil { | ||||||
|  | 			// they must all succeed | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		switch req.Response.Code { | ||||||
|  | 		case 200, 301, 302, 307, 308: | ||||||
|  | 		default: | ||||||
|  | 			// the response must be successful or redirect | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// only if we have at least one request | ||||||
|  | 	if len(tk.Requests) > 0 { | ||||||
|  | 		logger.Info("website likely accessible: seen successful chain of HTTPS transactions") | ||||||
|  | 		tk.NullNullFlags |= analysisFlagNullNullSuccessfulHTTPS | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// safety net otherwise | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // analysisNullNullDetectTLSMisconfigured runs when .Blocking = nil and | ||||||
| // .Accessible = nil to check whether by chance we had TLS issues both on the | // .Accessible = nil to check whether by chance we had TLS issues both on the | ||||||
| // probe side and on the TH side. This problem of detecting misconfiguration | // probe side and on the TH side. This problem of detecting misconfiguration | ||||||
| // of the server's TLS stack is discussed at https://github.com/ooni/probe/issues/2300. | // of the server's TLS stack is discussed at https://github.com/ooni/probe/issues/2300. | ||||||
| func (tk *TestKeys) analysisWebsiteDownDetectTLSMisconfigured(logger model.Logger) bool { | func (tk *TestKeys) analysisNullNullDetectTLSMisconfigured(logger model.Logger) bool { | ||||||
| 	if tk.Control == nil || tk.Control.TLSHandshake == nil { | 	if tk.Control == nil || tk.Control.TLSHandshake == nil { | ||||||
| 		// we need TLS control data to say we are in this case | 		// we need TLS control data to say we are in this case | ||||||
| 		return false | 		return false | ||||||
| @ -233,7 +314,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectTLSMisconfigured(logger model.Logge | |||||||
| 	// only if we have had some TLS handshakes for both probe and TH | 	// only if we have had some TLS handshakes for both probe and TH | ||||||
| 	if len(tk.TLSHandshakes) > 0 && len(tk.Control.TLSHandshake) > 0 { | 	if len(tk.TLSHandshakes) > 0 && len(tk.Control.TLSHandshake) > 0 { | ||||||
| 		logger.Info("website likely down: all TLS handshake attempts failed for both probe and TH") | 		logger.Info("website likely down: all TLS handshake attempts failed for both probe and TH") | ||||||
| 		tk.WebsiteDownFlags |= analysisFlagWebsiteDownTLSMisconfigured | 		tk.NullNullFlags |= analysisFlagNullNullTLSMisconfigured | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -241,7 +322,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectTLSMisconfigured(logger model.Logge | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // analysisWebsiteDownDetectAllConnectsFailed attempts to detect whether we are in | // analysisNullNullDetectAllConnectsFailed attempts to detect whether we are in | ||||||
| // the .Blocking = nil, .Accessible = nil case because all the TCP connect | // the .Blocking = nil, .Accessible = nil case because all the TCP connect | ||||||
| // attempts by either the probe or the TH have failed. | // attempts by either the probe or the TH have failed. | ||||||
| // | // | ||||||
| @ -249,7 +330,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectTLSMisconfigured(logger model.Logge | |||||||
| // for an example measurement with this behavior. | // for an example measurement with this behavior. | ||||||
| // | // | ||||||
| // See https://github.com/ooni/probe/issues/2299 for the reference issue. | // See https://github.com/ooni/probe/issues/2299 for the reference issue. | ||||||
| func (tk *TestKeys) analysisWebsiteDownDetectAllConnectsFailed(logger model.Logger) bool { | func (tk *TestKeys) analysisNullNullDetectAllConnectsFailed(logger model.Logger) bool { | ||||||
| 	if tk.Control == nil { | 	if tk.Control == nil { | ||||||
| 		// we need control data to say we're in this case | 		// we need control data to say we're in this case | ||||||
| 		return false | 		return false | ||||||
| @ -275,7 +356,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectAllConnectsFailed(logger model.Logg | |||||||
| 	// only if we have had some addresses to connect | 	// only if we have had some addresses to connect | ||||||
| 	if len(tk.TCPConnect) > 0 && len(tk.Control.TCPConnect) > 0 { | 	if len(tk.TCPConnect) > 0 && len(tk.Control.TCPConnect) > 0 { | ||||||
| 		logger.Info("website likely down: all TCP connect attempts failed for both probe and TH") | 		logger.Info("website likely down: all TCP connect attempts failed for both probe and TH") | ||||||
| 		tk.WebsiteDownFlags |= analysisFlagWebsiteDownAllConnectsFailed | 		tk.NullNullFlags |= analysisFlagNullNullAllConnectsFailed | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -283,7 +364,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectAllConnectsFailed(logger model.Logg | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // analysisWebsiteDownDetectNoAddrs attempts to see whether we | // analysisNullNullDetectNoAddrs attempts to see whether we | ||||||
| // ended up into the .Blocking = nil, .Accessible = nil case because | // ended up into the .Blocking = nil, .Accessible = nil case because | ||||||
| // the domain is expired and all queries returned no addresses. | // the domain is expired and all queries returned no addresses. | ||||||
| // | // | ||||||
| @ -297,7 +378,7 @@ func (tk *TestKeys) analysisWebsiteDownDetectAllConnectsFailed(logger model.Logg | |||||||
| // | // | ||||||
| // See https://github.com/ooni/probe/issues/2029 for more information | // See https://github.com/ooni/probe/issues/2029 for more information | ||||||
| // on Android's getaddrinfo behavior. | // on Android's getaddrinfo behavior. | ||||||
| func (tk *TestKeys) analysisWebsiteDownDetectNoAddrs(logger model.Logger) bool { | func (tk *TestKeys) analysisNullNullDetectNoAddrs(logger model.Logger) bool { | ||||||
| 	if tk.Control == nil { | 	if tk.Control == nil { | ||||||
| 		// we need control data to say we're in this case | 		// we need control data to say we're in this case | ||||||
| 		return false | 		return false | ||||||
| @ -325,6 +406,6 @@ func (tk *TestKeys) analysisWebsiteDownDetectNoAddrs(logger model.Logger) bool { | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	logger.Infof("website likely down: all DNS lookups failed for both probe and TH") | 	logger.Infof("website likely down: all DNS lookups failed for both probe and TH") | ||||||
| 	tk.WebsiteDownFlags |= analysisFlagWebsiteDownNoAddrs | 	tk.NullNullFlags |= analysisFlagNullNullNoAddrs | ||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import ( | |||||||
| // as [ctx] is done or when [reader] is closed, if applicable. | // as [ctx] is done or when [reader] is closed, if applicable. | ||||||
| // | // | ||||||
| // This function transforms an errors.Is(err, io.EOF) to a nil error | // This function transforms an errors.Is(err, io.EOF) to a nil error | ||||||
| // such as the standard library's io.ReadAll does. | // such as the standard library's ReadAll does. | ||||||
| // | // | ||||||
| // This function might return a non-zero-length buffer along with | // This function might return a non-zero-length buffer along with | ||||||
| // an non-nil error in the case in which we could only read a portion | // an non-nil error in the case in which we could only read a portion | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ func (m *Measurer) ExperimentName() string { | |||||||
| 
 | 
 | ||||||
| // ExperimentVersion implements model.ExperimentMeasurer. | // ExperimentVersion implements model.ExperimentMeasurer. | ||||||
| func (m *Measurer) ExperimentVersion() string { | func (m *Measurer) ExperimentVersion() string { | ||||||
| 	return "0.5.14" | 	return "0.5.15" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Run implements model.ExperimentMeasurer. | // Run implements model.ExperimentMeasurer. | ||||||
|  | |||||||
| @ -90,8 +90,9 @@ type TestKeys struct { | |||||||
| 	// BlockingFlags explains why we think that the website is blocked. | 	// BlockingFlags explains why we think that the website is blocked. | ||||||
| 	BlockingFlags int64 `json:"x_blocking_flags"` | 	BlockingFlags int64 `json:"x_blocking_flags"` | ||||||
| 
 | 
 | ||||||
| 	// WebsiteDownFlags explains why we determined that the website is down. | 	// NullNullFlags describes what the algorithm to avoid emitting | ||||||
| 	WebsiteDownFlags int64 `json:"x_website_down_flags"` | 	// blocking = null, accessible = null measurements did | ||||||
|  | 	NullNullFlags int64 `json:"x_null_null_flags"` | ||||||
| 
 | 
 | ||||||
| 	// BodyLength match tells us whether the body length matches. | 	// BodyLength match tells us whether the body length matches. | ||||||
| 	BodyLengthMatch *bool `json:"body_length_match"` | 	BodyLengthMatch *bool `json:"body_length_match"` | ||||||
| @ -337,7 +338,7 @@ func NewTestKeys() *TestKeys { | |||||||
| 		DNSConsistency:        "", | 		DNSConsistency:        "", | ||||||
| 		HTTPExperimentFailure: nil, | 		HTTPExperimentFailure: nil, | ||||||
| 		BlockingFlags:         0, | 		BlockingFlags:         0, | ||||||
| 		WebsiteDownFlags:      0, | 		NullNullFlags:         0, | ||||||
| 		BodyLengthMatch:       nil, | 		BodyLengthMatch:       nil, | ||||||
| 		HeadersMatch:          nil, | 		HeadersMatch:          nil, | ||||||
| 		StatusCodeMatch:       nil, | 		StatusCodeMatch:       nil, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user