Skip to content

Commit

Permalink
feat(webconnectivityqa): port jafar's http-diff test cases (#1242)
Browse files Browse the repository at this point in the history
## Checklist

- [x] I have read the [contribution
guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request:
ooni/probe#1803
- [x] if you changed anything related to how experiments work and you
need to reflect these changes in the ooni/spec repository, please link
to the related ooni/spec pull request: N/A
- [x] if you changed code inside an experiment, make sure you bump its
version number: N/A

## Description

This diff ports jafar's http-diff test cases to webconnectivityqa.
  • Loading branch information
bassosimone authored Sep 6, 2023
1 parent db7c7cf commit e14895a
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 70 deletions.
62 changes: 0 additions & 62 deletions QA/webconnectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,66 +47,6 @@ def assert_status_flags_are(ooni_exe, tk, desired):
assert tk["x_status"] == desired


def webconnectivity_http_diff_with_inconsistent_dns(ooni_exe, outfile):
"""Test case where we get an http-diff and the DNS is inconsistent"""
args = [
"-iptables-hijack-dns-to",
"127.0.0.1:53",
"-dns-proxy-hijack",
"example.org",
"-http-proxy-block",
"example.org",
]
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i http://example.org/ web_connectivity",
"webconnectivity_http_diff_with_inconsistent_dns",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "inconsistent"
assert tk["control_failure"] == None
assert tk["http_experiment_failure"] == None
assert tk["body_length_match"] == False
assert tk["body_proportion"] < 1
assert tk["status_code_match"] == False
assert tk["headers_match"] == True
assert tk["title_match"] == False
assert tk["blocking"] == "dns"
assert tk["accessible"] == False
assert_status_flags_are(ooni_exe, tk, 96)


def webconnectivity_http_diff_with_consistent_dns(ooni_exe, outfile):
"""Test case where we get an http-diff and the DNS is consistent"""
args = [
"-iptables-hijack-http-to",
"127.0.0.1:80",
"-http-proxy-block",
"example.org",
]
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i http://example.org/ web_connectivity",
"webconnectivity_http_diff_with_consistent_dns",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
assert tk["http_experiment_failure"] == None
assert tk["body_length_match"] == False
assert tk["body_proportion"] < 1
assert tk["status_code_match"] == False
assert tk["headers_match"] == True
assert tk["title_match"] == False
assert tk["blocking"] == "http-diff"
assert tk["accessible"] == False
assert_status_flags_are(ooni_exe, tk, 64)


def webconnectivity_https_expired_certificate(ooni_exe, outfile):
"""Test case where the domain's certificate is expired"""
args = []
Expand Down Expand Up @@ -298,8 +238,6 @@ def main():
outfile = "webconnectivity.jsonl"
ooni_exe = sys.argv[1]
tests = [
webconnectivity_http_diff_with_inconsistent_dns,
webconnectivity_http_diff_with_consistent_dns,
webconnectivity_https_expired_certificate,
webconnectivity_https_wrong_host,
webconnectivity_https_self_signed,
Expand Down
100 changes: 100 additions & 0 deletions internal/experiment/webconnectivityqa/httpdiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package webconnectivityqa

import (
"github.com/apex/log"
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/netemx"
)

// httpDiffWithConsistentDNS verifies the case where there is an HTTP diff
// but the addresses returned by the DNS resolver are consistent.
func httpDiffWithConsistentDNS() *TestCase {
return &TestCase{
Name: "httpDiffWithConsistentDNS",
Flags: TestCaseFlagNoLTE, // BUG: LTE does not set whether the headers match
Input: "http://www.example.com/",
Configure: func(env *netemx.QAEnv) {

// spoof the blockpage
env.DPIEngine().AddRule(&netem.DPISpoofBlockpageForString{
HTTPResponse: netem.DPIFormatHTTPResponse([]byte(netemx.Blockpage)),
Logger: log.Log,
ServerIPAddress: netemx.AddressWwwExampleCom,
ServerPort: 80,
String: "www.example.com",
})

},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSExperimentFailure: nil,
DNSConsistency: "consistent",
BodyLengthMatch: false,
BodyProportion: 0.12263535551206783,
StatusCodeMatch: true,
HeadersMatch: false,
TitleMatch: false,
HTTPExperimentFailure: nil,
XStatus: 64, // StatusAnomalyHTTPDiff
XDNSFlags: 0,
XBlockingFlags: 16, // analysisFlagHTTPDiff
Accessible: false,
Blocking: "http-diff",
},
}
}

// httpDiffWithInconsistentDNS verifies the case where there is an HTTP diff
// but the addresses returned by the DNS resolver are inconsistent.
func httpDiffWithInconsistentDNS() *TestCase {
return &TestCase{
Name: "httpDiffWithInconsistentDNS",
Flags: TestCaseFlagNoLTE, // BUG: LTE does not detect any HTTP diff here
Input: "http://www.example.com/",
Configure: func(env *netemx.QAEnv) {

// add DPI rule to force all the cleartext DNS queries to
// point the client to used the ISPProxyAddress
env.DPIEngine().AddRule(&netem.DPISpoofDNSResponse{
Addresses: []string{netemx.ISPProxyAddress},
Logger: env.Logger(),
Domain: "www.example.com",
})

// spoof the blockpage for the legitimate website address as well
env.DPIEngine().AddRule(&netem.DPISpoofBlockpageForString{
HTTPResponse: netem.DPIFormatHTTPResponse([]byte(netemx.Blockpage)),
Logger: log.Log,
ServerIPAddress: netemx.AddressWwwExampleCom,
ServerPort: 80,
String: "www.example.com",
})

// spoof the blockpage for the address that we assume the client would use
env.DPIEngine().AddRule(&netem.DPISpoofBlockpageForString{
HTTPResponse: netem.DPIFormatHTTPResponse([]byte(netemx.Blockpage)),
Logger: log.Log,
ServerIPAddress: netemx.ISPProxyAddress,
ServerPort: 80,
String: "www.example.com",
})

},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSExperimentFailure: nil,
DNSConsistency: "inconsistent",
HTTPExperimentFailure: nil,
BodyLengthMatch: false,
BodyProportion: 0.12263535551206783,
StatusCodeMatch: true,
HeadersMatch: false,
TitleMatch: false,
XStatus: 96, // StatusAnomalyHTTPDiff | StatusAnomalyDNS
XDNSFlags: 4, // AnalysisDNSUnexpectedAddrs
XBlockingFlags: 35, // analysisFlagSuccess | analysisFlagDNSBlocking | analysisFlagTCPIPBlocking
Accessible: false,
Blocking: "dns",
},
}
}
102 changes: 102 additions & 0 deletions internal/experiment/webconnectivityqa/httpdiff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package webconnectivityqa

import (
"context"
"net/http"
"testing"

"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/netemx"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

func TestHTTPDiffWithConsistentDNS(t *testing.T) {
testcases := []*TestCase{
httpDiffWithConsistentDNS(),
}

for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
tc.Configure(env)

env.Do(func() {
client := netxlite.NewHTTPClientStdlib(log.Log)
req := runtimex.Try1(http.NewRequest("GET", "http://www.example.com/", nil))
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := netxlite.ReadAllContext(req.Context(), resp.Body)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff([]byte(netemx.Blockpage), body); diff != "" {
t.Fatal(diff)
}
})
})
}
}

func TestHTTPDiffWithInconsistentDNS(t *testing.T) {
testcases := []*TestCase{
httpDiffWithInconsistentDNS(),
}

for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
tc.Configure(env)

env.Do(func() {
t.Run("there is blockpage spoofing", func(t *testing.T) {
client := netxlite.NewHTTPClientStdlib(log.Log)
req := runtimex.Try1(http.NewRequest("GET", "http://www.example.com/", nil))
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := netxlite.ReadAllContext(req.Context(), resp.Body)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff([]byte(netemx.Blockpage), body); diff != "" {
t.Fatal(diff)
}
})

t.Run("there is DNS spoofing", func(t *testing.T) {
expect := []string{netemx.ISPProxyAddress}

t.Run("with stdlib resolver", func(t *testing.T) {
reso := netxlite.NewStdlibResolver(log.Log)
addrs, err := reso.LookupHost(context.Background(), "www.example.com")
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expect, addrs); diff != "" {
t.Fatal(diff)
}
})

t.Run("with UDP resolver", func(t *testing.T) {
d := netxlite.NewDialerWithoutResolver(log.Log)
reso := netxlite.NewParallelUDPResolver(log.Log, d, "8.8.8.8:53")
addrs, err := reso.LookupHost(context.Background(), "www.example.com")
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expect, addrs); diff != "" {
t.Fatal(diff)
}
})
})
})
})
}
}
3 changes: 3 additions & 0 deletions internal/experiment/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func AllTestCases() []*TestCase {
dnsHijackingToProxyWithHTTPURL(),
dnsHijackingToProxyWithHTTPSURL(),

httpDiffWithConsistentDNS(),
httpDiffWithInconsistentDNS(),

redirectWithConsistentDNSAndThenConnectionRefusedForHTTP(),
redirectWithConsistentDNSAndThenConnectionRefusedForHTTPS(),
redirectWithConsistentDNSAndThenConnectionResetForHTTP(),
Expand Down
48 changes: 43 additions & 5 deletions internal/netemx/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,19 +327,57 @@ func Example_exampleWebServerWithInternetScenario() {
log.Fatalf("netxlite.ReadAllContext failed: %s", err.Error())
}

fmt.Printf("%+v\n", string(body))
// simplify comparison by stripping all the leading whitespaces
simplifyBody := func(body []byte) (output []byte) {
lines := bytes.Split(body, []byte("\n"))
for _, line := range lines {
line = bytes.TrimSpace(line)
line = append(line, '\n')
output = append(output, line...)
}
return output
}

fmt.Printf("%+v\n", string(simplifyBody(body)))
})

// Output:
// <!doctype html>
// <html>
// <head>
// <title>Default Web Page</title>
// <title>Default Web Page</title>
// </head>
// <body>
// <div>
// <h1>Default Web Page</h1>
// <p>This is the default web page of the default domain.</p>
// <h1>Default Web Page</h1>
//
// <p>This is the default web page of the default domain.</p>
//
// <p>We detect webpage blocking by checking for the status code first. If the status
// code is different, we consider the measurement http-diff. On the contrary when
// the status code matches, we say it's all good if one of the following check succeeds:</p>
//
// <p><ol>
// <li>the body length does not match (we say they match is the smaller of the two
// webpages is 70% or more of the size of the larger webpage);</li>
//
// <li>the uncommon headers match;</li>
//
// <li>the webpage title contains mostly the same words.</li>
// </ol></p>
//
// <p>If the three above checks fail, then we also say that there is http-diff. Because
// we need QA checks to work as intended, the size of THIS webpage you are reading
// has been increased, by adding this description, such that the body length check fails. The
// original webpage size was too close to the blockpage in size, and therefore we did see
// that there was no http-diff, as it ought to be.</p>
//
// <p>To make sure we're not going to have this issue in the future, there is now a runtime
// check that causes our code to crash if this web page size is too similar to the one of
// the default blockpage. We chose to add this text for additional clarity.</p>
//
// <p>Also, note that the blockpage MUST be very small, because in some cases we need
// to spoof it into a single TCP segment using ooni/netem's DPI.</p>
// </div>
// </body>
// </html>
Expand Down Expand Up @@ -405,7 +443,7 @@ func Example_oohelperdWithInternetScenario() {
})

// Output:
// {"tcp_connect":{"93.184.216.34:443":{"status":true,"failure":null}},"tls_handshake":{"93.184.216.34:443":{"server_name":"www.example.com","status":true,"failure":null}},"quic_handshake":{},"http_request":{"body_length":194,"discovered_h3_endpoint":"www.example.com:443","failure":null,"title":"Default Web Page","headers":{"Alt-Svc":"h3=\":443\"","Content-Length":"194","Content-Type":"text/html; charset=utf-8","Date":"Thu, 24 Aug 2023 14:35:29 GMT"},"status_code":200},"http3_request":null,"dns":{"failure":null,"addrs":["93.184.216.34"]},"ip_info":{"93.184.216.34":{"asn":15133,"flags":11}}}
// {"tcp_connect":{"93.184.216.34:443":{"status":true,"failure":null}},"tls_handshake":{"93.184.216.34:443":{"server_name":"www.example.com","status":true,"failure":null}},"quic_handshake":{},"http_request":{"body_length":1533,"discovered_h3_endpoint":"www.example.com:443","failure":null,"title":"Default Web Page","headers":{"Alt-Svc":"h3=\":443\"","Content-Length":"1533","Content-Type":"text/html; charset=utf-8","Date":"Thu, 24 Aug 2023 14:35:29 GMT"},"status_code":200},"http3_request":null,"dns":{"failure":null,"addrs":["93.184.216.34"]},"ip_info":{"93.184.216.34":{"asn":15133,"flags":11}}}
}

// This example shows how the [InternetScenario] defines a GeoIP service like Ubuntu's one.
Expand Down
6 changes: 3 additions & 3 deletions internal/netemx/oohelperd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,20 @@ func TestOOHelperDHandler(t *testing.T) {
},
},
HTTPRequest: model.THHTTPRequestResult{
BodyLength: 194,
BodyLength: 1533,
DiscoveredH3Endpoint: "www.example.com:443",
Failure: nil,
Title: "Default Web Page",
Headers: map[string]string{
"Alt-Svc": `h3=":443"`,
"Content-Length": "194",
"Content-Length": "1533",
"Content-Type": "text/html; charset=utf-8",
"Date": "Thu, 24 Aug 2023 14:35:29 GMT",
},
StatusCode: 200,
},
HTTP3Request: &model.THHTTPRequestResult{
BodyLength: 194,
BodyLength: 1533,
DiscoveredH3Endpoint: "",
Failure: nil,
Title: "Default Web Page",
Expand Down
Loading

0 comments on commit e14895a

Please sign in to comment.