refactor: enable QA tests and jafar self test (#208)
* refactor: enable QA tests and jafar self test Part of https://github.com/ooni/probe/issues/1335 * chore: make sure all workflows run on release branches
This commit is contained in:
@@ -0,0 +1 @@
|
||||
*
|
||||
@@ -0,0 +1,3 @@
|
||||
/GOPATH
|
||||
/GOCACHE
|
||||
/__pycache__
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM golang:1.14-alpine
|
||||
RUN apk add go git musl-dev iptables tmux bind-tools curl sudo python3
|
||||
@@ -0,0 +1,54 @@
|
||||
# Quality Assurance scripts
|
||||
|
||||
This directory contains quality assurance scripts that use Jafar to
|
||||
ensure that OONI implementations behave. These scripts take on the
|
||||
command line as argument the path to a binary with a OONI Probe v2.x
|
||||
like command line interface. We do not care about full compatibility
|
||||
but rather about having enough similar flags that running these tools
|
||||
in parallel is not too much of a burden for us.
|
||||
|
||||
Tools with this shallow-compatible CLI are:
|
||||
|
||||
1. `github.com/ooni/probe-legacy`
|
||||
2. `github.com/measurement-kit/measurement-kit/src/measurement_kit`
|
||||
3. `github.com/ooni/probe-engine/cmd/miniooni`
|
||||
|
||||
## Run QA on a Linux system
|
||||
|
||||
These scripts assume you're on a Linux system with `iptables`, `bash`,
|
||||
`python3`, and possibly a bunch of other tools installed.
|
||||
|
||||
To start the QA script, run this command:
|
||||
|
||||
```bash
|
||||
sudo ./QA/$nettest.py $ooni_exe
|
||||
```
|
||||
|
||||
where `$nettest` is the nettest name (e.g. `telegram`) and `$ooni_exe`
|
||||
is the OONI Probe v2.x compatible binary to test.
|
||||
|
||||
The Python script needs to run as root. Note however that sudo will also
|
||||
be used to run `$ooni_exe` with the privileges of the `nobody` user.
|
||||
|
||||
## Run QA using a docker container
|
||||
|
||||
Run test in a suitable Docker container using:
|
||||
|
||||
```bash
|
||||
./QA/rundocker.sh $nettest
|
||||
```
|
||||
|
||||
Note that this will run a `--privileged` docker container. This will
|
||||
eventually run the Python script you would run on Linux.
|
||||
|
||||
For now, the docker scripts only perform QA of `miniooni`.
|
||||
|
||||
## Diagnosing issues
|
||||
|
||||
The Python script that performs the QA runs a specific OONI test under
|
||||
different failure conditions and stops at the first unexpected value found
|
||||
in the resulting JSONL report. You can infer what went wrong by reading
|
||||
the output of the `$ooni_exe` command itself, which should be above the point
|
||||
where the Python script stopped, as well as by inspecting the JSONL file on
|
||||
disk. By convention such file is named `$nettest.jsonl` and only contains
|
||||
the result of the last run of `$nettest`.
|
||||
@@ -0,0 +1,72 @@
|
||||
""" ./QA/common.py - common code for QA """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def execute(args):
|
||||
""" Execute a specified command """
|
||||
subprocess.run(args)
|
||||
|
||||
|
||||
def execute_jafar_and_miniooni(ooni_exe, outfile, experiment, tag, args):
|
||||
""" Executes jafar and miniooni. Returns the test keys. """
|
||||
tmpoutfile = "/tmp/{}".format(outfile)
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.remove(tmpoutfile) # just in case
|
||||
execute(
|
||||
[
|
||||
"./jafar",
|
||||
"-main-command",
|
||||
"./QA/minioonilike.py {} -n -o '{}' --home /tmp {}".format(
|
||||
ooni_exe, tmpoutfile, experiment
|
||||
),
|
||||
"-main-user",
|
||||
"nobody", # should be present on Unix
|
||||
"-tag",
|
||||
tag,
|
||||
]
|
||||
+ args
|
||||
)
|
||||
shutil.copy(tmpoutfile, outfile)
|
||||
result = read_result(outfile)
|
||||
assert isinstance(result, dict)
|
||||
assert isinstance(result["test_keys"], dict)
|
||||
return result["test_keys"]
|
||||
|
||||
|
||||
def read_result(outfile):
|
||||
""" Reads the result of an experiment """
|
||||
return json.load(open(outfile, "rb"))
|
||||
|
||||
|
||||
def test_keys(result):
|
||||
""" Returns just the test keys of a specific result """
|
||||
return result["test_keys"]
|
||||
|
||||
|
||||
def check_maybe_binary_value(value):
|
||||
""" Make sure a maybe binary value is correct """
|
||||
assert isinstance(value, str) or (
|
||||
isinstance(value, dict)
|
||||
and value["format"] == "base64"
|
||||
and isinstance(value["data"], str)
|
||||
)
|
||||
|
||||
|
||||
def with_free_port(func):
|
||||
""" This function executes |func| passing it a port number on localhost
|
||||
which is bound but not listening for new connections """
|
||||
# See <https://stackoverflow.com/a/45690594>
|
||||
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
func(sock.getsockname()[1])
|
||||
Executable
+286
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/fbmessenger.py - main QA script for fbmessenger
|
||||
|
||||
This script performs a bunch of fbmessenger tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
services = {
|
||||
"stun": "stun.fbsbx.com",
|
||||
"b_api": "b-api.facebook.com",
|
||||
"b_graph": "b-graph.facebook.com",
|
||||
"edge": "edge-mqtt.facebook.com",
|
||||
"external_cdn": "external.xx.fbcdn.net",
|
||||
"scontent_cdn": "scontent.xx.fbcdn.net",
|
||||
"star": "star.c10r.facebook.com",
|
||||
}
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(ooni_exe, outfile, tag, args):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(
|
||||
ooni_exe, outfile, "facebook_messenger", tag, args
|
||||
)
|
||||
assert tk["requests"] is None
|
||||
if tk["tcp_connect"] is not None:
|
||||
assert isinstance(tk["tcp_connect"], list)
|
||||
assert len(tk["tcp_connect"]) > 0
|
||||
for entry in tk["tcp_connect"]:
|
||||
assert isinstance(entry, dict)
|
||||
assert isinstance(entry["ip"], str)
|
||||
assert isinstance(entry["port"], int)
|
||||
assert isinstance(entry["status"], dict)
|
||||
failure = entry["status"]["failure"]
|
||||
success = entry["status"]["success"]
|
||||
assert isinstance(failure, str) or failure is None
|
||||
assert isinstance(success, bool)
|
||||
return tk
|
||||
|
||||
|
||||
def helper_for_blocking_services_via_dns(service):
|
||||
""" Helper for hijacking a service via dns """
|
||||
args = []
|
||||
args.append("-iptables-hijack-dns-to")
|
||||
args.append("127.0.0.1:53")
|
||||
args.append("-dns-proxy-block")
|
||||
args.append(service)
|
||||
return args
|
||||
|
||||
|
||||
def helper_for_hijacking_services_via_dns(service):
|
||||
""" Helper for hijacking a service via dns """
|
||||
args = []
|
||||
args.append("-iptables-hijack-dns-to")
|
||||
args.append("127.0.0.1:53")
|
||||
args.append("-dns-proxy-hijack")
|
||||
args.append(service)
|
||||
return args
|
||||
|
||||
|
||||
def helper_for_blocking_services_via_tcp(service):
|
||||
""" Helper for blocking a service via tcp """
|
||||
args = []
|
||||
args.append("-iptables-reset-ip")
|
||||
args.append(service)
|
||||
return args
|
||||
|
||||
|
||||
def fbmessenger_dns_hijacked_for_all(ooni_exe, outfile):
|
||||
""" Test case where everything we measure is DNS hijacked """
|
||||
args = []
|
||||
for _, value in services.items():
|
||||
args.extend(helper_for_hijacking_services_via_dns(value))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_dns_hijacked_for_all", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == False
|
||||
assert tk["facebook_b_api_reachable"] == None
|
||||
assert tk["facebook_b_graph_dns_consistent"] == False
|
||||
assert tk["facebook_b_graph_reachable"] == None
|
||||
assert tk["facebook_edge_dns_consistent"] == False
|
||||
assert tk["facebook_edge_reachable"] == None
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == False
|
||||
assert tk["facebook_external_cdn_reachable"] == None
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == False
|
||||
assert tk["facebook_scontent_cdn_reachable"] == None
|
||||
assert tk["facebook_star_dns_consistent"] == False
|
||||
assert tk["facebook_star_reachable"] == None
|
||||
assert tk["facebook_stun_dns_consistent"] == False
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == True
|
||||
assert tk["facebook_tcp_blocking"] == False
|
||||
|
||||
|
||||
def fbmessenger_dns_hijacked_for_some(ooni_exe, outfile):
|
||||
""" Test case where some endpoints are DNS hijacked """
|
||||
args = []
|
||||
args.extend(helper_for_hijacking_services_via_dns(services["star"]))
|
||||
args.extend(helper_for_hijacking_services_via_dns(services["edge"]))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_dns_hijacked_for_some", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == True
|
||||
assert tk["facebook_b_api_reachable"] == True
|
||||
assert tk["facebook_b_graph_dns_consistent"] == True
|
||||
assert tk["facebook_b_graph_reachable"] == True
|
||||
assert tk["facebook_edge_dns_consistent"] == False
|
||||
assert tk["facebook_edge_reachable"] == None
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_external_cdn_reachable"] == True
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_scontent_cdn_reachable"] == True
|
||||
assert tk["facebook_star_dns_consistent"] == False
|
||||
assert tk["facebook_star_reachable"] == None
|
||||
assert tk["facebook_stun_dns_consistent"] == True
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == True
|
||||
assert tk["facebook_tcp_blocking"] == False
|
||||
|
||||
|
||||
def fbmessenger_dns_blocked_for_all(ooni_exe, outfile):
|
||||
""" Test case where everything we measure is DNS blocked """
|
||||
args = []
|
||||
for _, value in services.items():
|
||||
args.extend(helper_for_blocking_services_via_dns(value))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_dns_blocked_for_all", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == False
|
||||
assert tk["facebook_b_api_reachable"] == None
|
||||
assert tk["facebook_b_graph_dns_consistent"] == False
|
||||
assert tk["facebook_b_graph_reachable"] == None
|
||||
assert tk["facebook_edge_dns_consistent"] == False
|
||||
assert tk["facebook_edge_reachable"] == None
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == False
|
||||
assert tk["facebook_external_cdn_reachable"] == None
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == False
|
||||
assert tk["facebook_scontent_cdn_reachable"] == None
|
||||
assert tk["facebook_star_dns_consistent"] == False
|
||||
assert tk["facebook_star_reachable"] == None
|
||||
assert tk["facebook_stun_dns_consistent"] == False
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == True
|
||||
assert tk["facebook_tcp_blocking"] == False
|
||||
|
||||
|
||||
def fbmessenger_dns_blocked_for_some(ooni_exe, outfile):
|
||||
""" Test case where some endpoints are DNS blocked """
|
||||
args = []
|
||||
args.extend(helper_for_blocking_services_via_dns(services["b_graph"]))
|
||||
args.extend(helper_for_blocking_services_via_dns(services["stun"]))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_dns_blocked_for_some", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == True
|
||||
assert tk["facebook_b_api_reachable"] == True
|
||||
assert tk["facebook_b_graph_dns_consistent"] == False
|
||||
assert tk["facebook_b_graph_reachable"] == None
|
||||
assert tk["facebook_edge_dns_consistent"] == True
|
||||
assert tk["facebook_edge_reachable"] == True
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_external_cdn_reachable"] == True
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_scontent_cdn_reachable"] == True
|
||||
assert tk["facebook_star_dns_consistent"] == True
|
||||
assert tk["facebook_star_reachable"] == True
|
||||
assert tk["facebook_stun_dns_consistent"] == False
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == True
|
||||
assert tk["facebook_tcp_blocking"] == False
|
||||
|
||||
|
||||
def fbmessenger_tcp_blocked_for_all(ooni_exe, outfile):
|
||||
""" Test case where everything we measure is TCP blocked """
|
||||
args = []
|
||||
for _, value in services.items():
|
||||
args.extend(helper_for_blocking_services_via_tcp(value))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_tcp_blocked_for_all", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == True
|
||||
assert tk["facebook_b_api_reachable"] == False
|
||||
assert tk["facebook_b_graph_dns_consistent"] == True
|
||||
assert tk["facebook_b_graph_reachable"] == False
|
||||
assert tk["facebook_edge_dns_consistent"] == True
|
||||
assert tk["facebook_edge_reachable"] == False
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_external_cdn_reachable"] == False
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_scontent_cdn_reachable"] == False
|
||||
assert tk["facebook_star_dns_consistent"] == True
|
||||
assert tk["facebook_star_reachable"] == False
|
||||
assert tk["facebook_stun_dns_consistent"] == True
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == False
|
||||
assert tk["facebook_tcp_blocking"] == True
|
||||
|
||||
|
||||
def fbmessenger_tcp_blocked_for_some(ooni_exe, outfile):
|
||||
""" Test case where only some endpoints are TCP blocked """
|
||||
args = []
|
||||
args.extend(helper_for_blocking_services_via_tcp(services["edge"]))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_tcp_blocked_for_some", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == True
|
||||
assert tk["facebook_b_api_reachable"] == True
|
||||
assert tk["facebook_b_graph_dns_consistent"] == True
|
||||
assert tk["facebook_b_graph_reachable"] == True
|
||||
assert tk["facebook_edge_dns_consistent"] == True
|
||||
assert tk["facebook_edge_reachable"] == False
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_external_cdn_reachable"] == True
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_scontent_cdn_reachable"] == True
|
||||
assert tk["facebook_star_dns_consistent"] == True
|
||||
assert tk["facebook_star_reachable"] == True
|
||||
assert tk["facebook_stun_dns_consistent"] == True
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == False
|
||||
assert tk["facebook_tcp_blocking"] == True
|
||||
|
||||
|
||||
def fbmessenger_mixed_results(ooni_exe, outfile):
|
||||
""" Test case where only some endpoints are TCP blocked """
|
||||
args = []
|
||||
args.extend(helper_for_blocking_services_via_tcp(services["edge"]))
|
||||
args.extend(helper_for_blocking_services_via_dns(services["b_api"]))
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "fbmessenger_tcp_blocked_for_some", args,
|
||||
)
|
||||
assert tk["facebook_b_api_dns_consistent"] == False
|
||||
assert tk["facebook_b_api_reachable"] == None
|
||||
assert tk["facebook_b_graph_dns_consistent"] == True
|
||||
assert tk["facebook_b_graph_reachable"] == True
|
||||
assert tk["facebook_edge_dns_consistent"] == True
|
||||
assert tk["facebook_edge_reachable"] == False
|
||||
assert tk["facebook_external_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_external_cdn_reachable"] == True
|
||||
assert tk["facebook_scontent_cdn_dns_consistent"] == True
|
||||
assert tk["facebook_scontent_cdn_reachable"] == True
|
||||
assert tk["facebook_star_dns_consistent"] == True
|
||||
assert tk["facebook_star_reachable"] == True
|
||||
assert tk["facebook_stun_dns_consistent"] == True
|
||||
assert tk["facebook_stun_reachable"] == None
|
||||
assert tk["facebook_dns_blocking"] == True
|
||||
assert tk["facebook_tcp_blocking"] == True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "fbmessenger.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
fbmessenger_dns_hijacked_for_all,
|
||||
fbmessenger_dns_hijacked_for_some,
|
||||
fbmessenger_dns_blocked_for_all,
|
||||
fbmessenger_dns_blocked_for_some,
|
||||
fbmessenger_tcp_blocked_for_all,
|
||||
fbmessenger_tcp_blocked_for_some,
|
||||
fbmessenger_mixed_results,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/hhfm.py - main QA script for hhfm
|
||||
|
||||
This script performs a bunch of hhfm tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(ooni_exe, outfile, tag, args):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(
|
||||
ooni_exe, outfile, "http_header_field_manipulation", tag, args
|
||||
)
|
||||
# TODO(bassosimone): what checks to put here?
|
||||
return tk
|
||||
|
||||
|
||||
def hhfm_transparent_proxy(ooni_exe, outfile):
|
||||
""" Test case where we're passing through a transparent proxy """
|
||||
args = ["-iptables-hijack-http-to", "127.0.0.1:80"]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "hhfm_transparent_proxy", args,
|
||||
)
|
||||
# The proxy sees a domain that does not make any sense and does not
|
||||
# otherwise know where to connect to. Hence the most likely result is
|
||||
# a `dns_nxdomain_error` with total tampering.
|
||||
assert tk["tampering"]["header_field_name"] == False
|
||||
assert tk["tampering"]["header_field_number"] == False
|
||||
assert tk["tampering"]["header_field_value"] == False
|
||||
assert tk["tampering"]["header_name_capitalization"] == False
|
||||
assert tk["tampering"]["header_name_diff"] == []
|
||||
assert tk["tampering"]["request_line_capitalization"] == False
|
||||
assert tk["tampering"]["total"] == True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "hhfm.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
hhfm_transparent_proxy,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/hirl.py - main QA script for hirl
|
||||
|
||||
This script performs a bunch of hirl tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(ooni_exe, outfile, tag, args):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(
|
||||
ooni_exe, outfile, "http_invalid_request_line", tag, args
|
||||
)
|
||||
# TODO(bassosimone): what checks to put here?
|
||||
return tk
|
||||
|
||||
|
||||
def hirl_transparent_proxy(ooni_exe, outfile):
|
||||
""" Test case where we're passing through a transparent proxy """
|
||||
args = ["-iptables-hijack-http-to", "127.0.0.1:80"]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "hirl_transparent_proxy", args,
|
||||
)
|
||||
count = 0
|
||||
for entry in tk["failure_list"]:
|
||||
if entry is None:
|
||||
count += 1
|
||||
elif entry == "eof_error":
|
||||
count += 1e03
|
||||
else:
|
||||
count += 1e06
|
||||
assert count == 3002
|
||||
assert tk["tampering_list"] == [True, True, True, True, True]
|
||||
assert tk["tampering"] == True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "hirl.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
hirl_transparent_proxy,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" This script takes in input the name of the tool to run followed by
|
||||
arguments and followed by the nettest name. The format recognized is
|
||||
the same of miniooni. Depending on the tool that we want to run, we
|
||||
reorder arguments so that they make sense for the tool.
|
||||
|
||||
This is necessary because, albeit miniooni, MK, and OONI v2.x have
|
||||
more or less the same arguments, there are some differences. We could
|
||||
modify other tools to match miniooni, but this seems useless. """
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def file_must_exist(pathname):
|
||||
""" Throws an exception if the given file does not actually exist. """
|
||||
if not os.path.isfile(pathname):
|
||||
raise RuntimeError("missing {}: please run miniooni first".format(pathname))
|
||||
return pathname
|
||||
|
||||
|
||||
def main():
|
||||
apa = argparse.ArgumentParser()
|
||||
apa.add_argument("command", nargs=1, help="command to execute")
|
||||
|
||||
# subset of arguments accepted by miniooni
|
||||
apa.add_argument(
|
||||
"-n", "--no-collector", action="count", help="don't submit measurement"
|
||||
)
|
||||
apa.add_argument("-o", "--reportfile", help="specify report file to use")
|
||||
apa.add_argument("-i", "--input", help="input for nettests taking an input")
|
||||
apa.add_argument("--home", help="override home directory")
|
||||
apa.add_argument("nettest", nargs=1, help="nettest to run")
|
||||
out = apa.parse_args()
|
||||
command, nettest = out.command[0], out.nettest[0]
|
||||
|
||||
if "miniooni" not in command and "measurement_kit" not in command:
|
||||
raise RuntimeError("unrecognized tool")
|
||||
|
||||
args = []
|
||||
args.append(command)
|
||||
if "miniooni" in command:
|
||||
args.extend(["--yes"]) # make sure we have informed consent
|
||||
if "measurement_kit" in command:
|
||||
args.extend(
|
||||
[
|
||||
"--ca-bundle-path",
|
||||
file_must_exist("{}/.miniooni/assets/ca-bundle.pem".format(out.home)),
|
||||
]
|
||||
)
|
||||
args.extend(
|
||||
[
|
||||
"--geoip-country-path",
|
||||
file_must_exist("{}/.miniooni/assets/country.mmdb".format(out.home)),
|
||||
]
|
||||
)
|
||||
args.extend(
|
||||
[
|
||||
"--geoip-asn-path",
|
||||
file_must_exist("{}/.miniooni/assets/asn.mmdb".format(out.home)),
|
||||
]
|
||||
)
|
||||
if out.home and "miniooni" in command:
|
||||
args.extend(["--home", out.home]) # home applies to miniooni only
|
||||
if out.input:
|
||||
if "miniooni" in command:
|
||||
args.extend(["-i", out.input]) # input is -i for miniooni
|
||||
if out.no_collector:
|
||||
args.append("-n")
|
||||
if out.reportfile:
|
||||
args.extend(["-o", out.reportfile])
|
||||
args.append(nettest)
|
||||
if out.input and "measurement_kit" in command:
|
||||
if nettest == "web_connectivity":
|
||||
args.extend(["-u", out.input]) # MK's Web Connectivity uses -u for input
|
||||
|
||||
sys.stderr.write("minioonilike.py: {}\n".format(shlex.join(args)))
|
||||
common.execute(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/probeasn.py - QA script for the -g miniooni option. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def execute_miniooni(ooni_exe, outfile, arguments):
|
||||
""" Executes miniooni and returns the whole measurement. """
|
||||
if "miniooni" not in ooni_exe:
|
||||
return None
|
||||
tmpoutfile = "/tmp/{}".format(outfile)
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.remove(tmpoutfile) # just in case
|
||||
cmdline = [
|
||||
ooni_exe,
|
||||
arguments,
|
||||
"-o",
|
||||
tmpoutfile,
|
||||
"--home",
|
||||
"/tmp",
|
||||
"example",
|
||||
]
|
||||
print("exec: {}".format(cmdline))
|
||||
common.execute(cmdline)
|
||||
shutil.copy(tmpoutfile, outfile)
|
||||
result = common.read_result(outfile)
|
||||
assert isinstance(result, dict)
|
||||
assert isinstance(result["test_keys"], dict)
|
||||
return result
|
||||
|
||||
|
||||
def probeasn_without_g_option(ooni_exe, outfile):
|
||||
""" Test case where we're not passing to miniooni the -g option """
|
||||
m = execute_miniooni(ooni_exe, outfile, "-n")
|
||||
if m is None:
|
||||
return
|
||||
assert m["probe_cc"] != "ZZ"
|
||||
assert m["probe_ip"] == "127.0.0.1"
|
||||
assert m["probe_asn"] != "AS0"
|
||||
assert m["probe_network_name"] != ""
|
||||
assert m["resolver_ip"] == "127.0.0.2"
|
||||
assert m["resolver_asn"] != "AS0"
|
||||
assert m["resolver_network_name"] != ""
|
||||
|
||||
|
||||
def probeasn_with_g_option(ooni_exe, outfile):
|
||||
""" Test case where we're passing the -g option """
|
||||
m = execute_miniooni(ooni_exe, outfile, "-gn")
|
||||
if m is None:
|
||||
return
|
||||
assert m["probe_cc"] != "ZZ"
|
||||
assert m["probe_ip"] == "127.0.0.1"
|
||||
assert m["probe_asn"] == "AS0"
|
||||
assert m["probe_network_name"] == ""
|
||||
assert m["resolver_ip"] == "127.0.0.2"
|
||||
assert m["resolver_asn"] == "AS0"
|
||||
assert m["resolver_network_name"] == ""
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "probeasn.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
probeasn_with_g_option,
|
||||
probeasn_without_g_option,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
export GOPATH=/jafar/QA/GOPATH GOCACHE=/jafar/QA/GOCACHE GO111MODULE=on
|
||||
go build -v ./internal/cmd/miniooni
|
||||
go build -v ./internal/cmd/jafar
|
||||
sudo ./QA/$1.py ./miniooni
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
DOCKER=${DOCKER:-docker}
|
||||
$DOCKER build -t jafar-qa ./QA/
|
||||
$DOCKER run --privileged -v`pwd`:/jafar -w/jafar jafar-qa ./QA/pyrun.sh "$@"
|
||||
Executable
+218
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/telegram.py - main QA script for telegram
|
||||
|
||||
This script performs a bunch of telegram tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
ALL_POP_IPS = (
|
||||
"149.154.175.50",
|
||||
"149.154.167.51",
|
||||
"149.154.175.100",
|
||||
"149.154.167.91",
|
||||
"149.154.171.5",
|
||||
)
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(ooni_exe, outfile, tag, args):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(ooni_exe, outfile, "telegram", tag, args)
|
||||
assert isinstance(tk["requests"], list)
|
||||
assert len(tk["requests"]) > 0
|
||||
for entry in tk["requests"]:
|
||||
assert isinstance(entry, dict)
|
||||
failure = entry["failure"]
|
||||
assert isinstance(failure, str) or failure is None
|
||||
assert isinstance(entry["request"], dict)
|
||||
req = entry["request"]
|
||||
common.check_maybe_binary_value(req["body"])
|
||||
assert isinstance(req["headers"], dict)
|
||||
for key, value in req["headers"].items():
|
||||
assert isinstance(key, str)
|
||||
common.check_maybe_binary_value(value)
|
||||
assert isinstance(req["method"], str)
|
||||
assert isinstance(entry["response"], dict)
|
||||
resp = entry["response"]
|
||||
common.check_maybe_binary_value(resp["body"])
|
||||
assert isinstance(resp["code"], int)
|
||||
if resp["headers"] is not None:
|
||||
for key, value in resp["headers"].items():
|
||||
assert isinstance(key, str)
|
||||
common.check_maybe_binary_value(value)
|
||||
assert isinstance(tk["tcp_connect"], list)
|
||||
assert len(tk["tcp_connect"]) > 0
|
||||
for entry in tk["tcp_connect"]:
|
||||
assert isinstance(entry, dict)
|
||||
assert isinstance(entry["ip"], str)
|
||||
assert isinstance(entry["port"], int)
|
||||
assert isinstance(entry["status"], dict)
|
||||
failure = entry["status"]["failure"]
|
||||
success = entry["status"]["success"]
|
||||
assert isinstance(failure, str) or failure is None
|
||||
assert isinstance(success, bool)
|
||||
return tk
|
||||
|
||||
|
||||
def args_for_blocking_all_pop_ips():
|
||||
""" Returns the arguments useful for blocking all POPs IPs """
|
||||
args = []
|
||||
for ip in ALL_POP_IPS:
|
||||
args.append("-iptables-reset-ip")
|
||||
args.append(ip)
|
||||
return args
|
||||
|
||||
|
||||
def args_for_blocking_web_telegram_org_http():
|
||||
""" Returns arguments for blocking web.telegram.org over http """
|
||||
return ["-iptables-reset-keyword", "Host: web.telegram.org"]
|
||||
|
||||
|
||||
def args_for_blocking_web_telegram_org_https():
|
||||
""" Returns arguments for blocking web.telegram.org over https """
|
||||
#
|
||||
# 00 00 <SNI extension ID>
|
||||
# 00 15 <full extension length>
|
||||
# 00 13 <first entry length>
|
||||
# 00 <DNS hostname type>
|
||||
# 00 10 <string length>
|
||||
# 77 65 ... 67 web.telegram.org
|
||||
#
|
||||
return [
|
||||
"-iptables-reset-keyword-hex",
|
||||
"|00 00 00 15 00 13 00 00 10 77 65 62 2e 74 65 6c 65 67 72 61 6d 2e 6f 72 67|",
|
||||
]
|
||||
|
||||
|
||||
def telegram_block_everything(ooni_exe, outfile):
|
||||
""" Test case where everything we measure is blocked """
|
||||
args = []
|
||||
args.extend(args_for_blocking_all_pop_ips())
|
||||
args.extend(args_for_blocking_web_telegram_org_https())
|
||||
args.extend(args_for_blocking_web_telegram_org_http())
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_block_everything", args,
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == True
|
||||
assert tk["telegram_http_blocking"] == True
|
||||
assert tk["telegram_web_failure"] == "connection_reset"
|
||||
assert tk["telegram_web_status"] == "blocked"
|
||||
|
||||
|
||||
def telegram_tcp_blocking_all(ooni_exe, outfile):
|
||||
""" Test case where all POPs are TCP/IP blocked """
|
||||
args = args_for_blocking_all_pop_ips()
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_tcp_blocking_all", args
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == True
|
||||
assert tk["telegram_http_blocking"] == True
|
||||
assert tk["telegram_web_failure"] == None
|
||||
assert tk["telegram_web_status"] == "ok"
|
||||
|
||||
|
||||
def telegram_tcp_blocking_some(ooni_exe, outfile):
|
||||
""" Test case where some POPs are TCP/IP blocked """
|
||||
args = [
|
||||
"-iptables-reset-ip",
|
||||
ALL_POP_IPS[0],
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_tcp_blocking_some", args
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == False
|
||||
assert tk["telegram_http_blocking"] == False
|
||||
assert tk["telegram_web_failure"] == None
|
||||
assert tk["telegram_web_status"] == "ok"
|
||||
|
||||
|
||||
def telegram_http_blocking_all(ooni_exe, outfile):
|
||||
""" Test case where all POPs are HTTP blocked """
|
||||
args = []
|
||||
for ip in ALL_POP_IPS:
|
||||
args.append("-iptables-reset-keyword")
|
||||
args.append(ip)
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_http_blocking_all", args,
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == False
|
||||
assert tk["telegram_http_blocking"] == True
|
||||
assert tk["telegram_web_failure"] == None
|
||||
assert tk["telegram_web_status"] == "ok"
|
||||
|
||||
|
||||
def telegram_http_blocking_some(ooni_exe, outfile):
|
||||
""" Test case where some POPs are HTTP blocked """
|
||||
args = [
|
||||
"-iptables-reset-keyword",
|
||||
ALL_POP_IPS[0],
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_http_blocking_some", args,
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == False
|
||||
assert tk["telegram_http_blocking"] == False
|
||||
assert tk["telegram_web_failure"] == None
|
||||
assert tk["telegram_web_status"] == "ok"
|
||||
|
||||
|
||||
def telegram_web_failure_http(ooni_exe, outfile):
|
||||
""" Test case where the web HTTP endpoint is blocked """
|
||||
args = args_for_blocking_web_telegram_org_http()
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_web_failure_http", args,
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == False
|
||||
assert tk["telegram_http_blocking"] == False
|
||||
assert tk["telegram_web_failure"] == "connection_reset"
|
||||
assert tk["telegram_web_status"] == "blocked"
|
||||
|
||||
|
||||
def telegram_web_failure_https(ooni_exe, outfile):
|
||||
""" Test case where the web HTTPS endpoint is blocked """
|
||||
args = args_for_blocking_web_telegram_org_https()
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "telegram_web_failure_https", args,
|
||||
)
|
||||
assert tk["telegram_tcp_blocking"] == False
|
||||
assert tk["telegram_http_blocking"] == False
|
||||
assert tk["telegram_web_failure"] == "connection_reset"
|
||||
assert tk["telegram_web_status"] == "blocked"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "telegram.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
telegram_block_everything,
|
||||
telegram_tcp_blocking_all,
|
||||
telegram_tcp_blocking_some,
|
||||
telegram_http_blocking_all,
|
||||
telegram_http_blocking_some,
|
||||
telegram_web_failure_http,
|
||||
telegram_web_failure_https,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+855
@@ -0,0 +1,855 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/webconnectivity.py - main QA script for webconnectivity
|
||||
|
||||
This script performs a bunch of webconnectivity tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, experiment_args, tag, args
|
||||
):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(
|
||||
ooni_exe, outfile, experiment_args, tag, args
|
||||
)
|
||||
return tk
|
||||
|
||||
|
||||
def assert_status_flags_are(ooni_exe, tk, desired):
|
||||
""" Checks whether the status flags are what we expect them to
|
||||
be when we're running miniooni. This check only makes sense
|
||||
with miniooni b/c status flags are a miniooni extension. """
|
||||
if "miniooni" not in ooni_exe:
|
||||
return
|
||||
assert tk["x_status"] == desired
|
||||
|
||||
|
||||
def webconnectivity_https_ok_with_control_failure(ooni_exe, outfile):
|
||||
""" Successful HTTPS measurement but control failure. """
|
||||
args = [
|
||||
"-iptables-reset-keyword",
|
||||
"wcth.ooni.io",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.com/ web_connectivity",
|
||||
"webconnectivity_https_ok_with_control_failure",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == None
|
||||
assert tk["control_failure"] == "connection_reset"
|
||||
assert tk["http_experiment_failure"] == None
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
else:
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
assert_status_flags_are(ooni_exe, tk, 1)
|
||||
|
||||
|
||||
def webconnectivity_http_ok_with_control_failure(ooni_exe, outfile):
|
||||
""" Successful HTTP measurement but control failure. """
|
||||
args = [
|
||||
"-iptables-reset-keyword",
|
||||
"wcth.ooni.io",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://example.org/ web_connectivity",
|
||||
"webconnectivity_http_ok_with_control_failure",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == None
|
||||
assert tk["control_failure"] == "connection_reset"
|
||||
assert tk["http_experiment_failure"] == None
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
assert_status_flags_are(ooni_exe, tk, 8)
|
||||
|
||||
|
||||
def webconnectivity_transparent_http_proxy(ooni_exe, outfile):
|
||||
""" Test case where we pass through a transparent HTTP proxy """
|
||||
args = []
|
||||
args.append("-iptables-hijack-https-to")
|
||||
args.append("127.0.0.1:443")
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.org web_connectivity",
|
||||
"webconnectivity_transparent_http_proxy",
|
||||
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"] == True
|
||||
assert tk["body_proportion"] == 1
|
||||
assert tk["status_code_match"] == True
|
||||
assert tk["headers_match"] == True
|
||||
assert tk["title_match"] == True
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 1)
|
||||
|
||||
|
||||
def webconnectivity_dns_hijacking(ooni_exe, outfile):
|
||||
""" Test case where there is DNS hijacking towards a transparent proxy. """
|
||||
args = []
|
||||
args.append("-iptables-hijack-dns-to")
|
||||
args.append("127.0.0.1:53")
|
||||
args.append("-dns-proxy-hijack")
|
||||
args.append("example.org")
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.org web_connectivity",
|
||||
"webconnectivity_dns_hijacking",
|
||||
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"] == True
|
||||
assert tk["body_proportion"] == 1
|
||||
assert tk["status_code_match"] == True
|
||||
assert tk["headers_match"] == True
|
||||
assert tk["title_match"] == True
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 1)
|
||||
|
||||
|
||||
def webconnectivity_control_unreachable_and_using_http(ooni_exe, outfile):
|
||||
""" Test case where the control is unreachable and we're using the
|
||||
plaintext HTTP protocol rather than HTTPS """
|
||||
args = []
|
||||
args.append("-iptables-reset-keyword")
|
||||
args.append("wcth.ooni.io")
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://example.org web_connectivity",
|
||||
"webconnectivity_control_unreachable_and_using_http",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == None
|
||||
assert tk["control_failure"] == "connection_reset"
|
||||
assert tk["http_experiment_failure"] == None
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
assert_status_flags_are(ooni_exe, tk, 8)
|
||||
|
||||
|
||||
def webconnectivity_nonexistent_domain(ooni_exe, outfile):
|
||||
""" Test case where the domain does not exist """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://antani.ooni.io web_connectivity",
|
||||
"webconnectivity_nonexistent_domain",
|
||||
args,
|
||||
)
|
||||
# TODO(bassosimone): Debateable result. We need to do better here.
|
||||
# See <https://github.com/ooni/probe-engine/issues/579>.
|
||||
#
|
||||
# Note that MK is not doing it right here because it's suppressing the
|
||||
# dns_nxdomain_error that instead is very informative. Yet, it is reporting
|
||||
# a failure in HTTP, which miniooni does not because it does not make
|
||||
# sense to perform HTTP when there are no IP addresses.
|
||||
#
|
||||
# The following seems indeed a bug in MK where we don't properly record the
|
||||
# actual error that occurred when performing the DNS experiment.
|
||||
#
|
||||
# See <https://github.com/measurement-kit/measurement-kit/issues/1931>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["dns_experiment_failure"] == "dns_nxdomain_error"
|
||||
else:
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "consistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == None
|
||||
else:
|
||||
assert tk["http_experiment_failure"] == "dns_lookup_error"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 2052)
|
||||
|
||||
|
||||
def webconnectivity_tcpip_blocking_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's TCP/IP blocking w/ consistent DNS """
|
||||
ip = socket.gethostbyname("nexa.polito.it")
|
||||
args = [
|
||||
"-iptables-drop-ip",
|
||||
ip,
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://nexa.polito.it web_connectivity",
|
||||
"webconnectivity_tcpip_blocking_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"] == "generic_timeout_error"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "tcp_ip"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 4224)
|
||||
|
||||
|
||||
def webconnectivity_tcpip_blocking_with_inconsistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's TCP/IP blocking w/ inconsistent DNS """
|
||||
|
||||
def runner(port):
|
||||
args = [
|
||||
"-dns-proxy-hijack",
|
||||
"nexa.polito.it",
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-iptables-hijack-http-to",
|
||||
"127.0.0.1:{}".format(port),
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://nexa.polito.it web_connectivity",
|
||||
"webconnectivity_tcpip_blocking_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"] == "connection_refused"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "dns"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 4256)
|
||||
|
||||
common.with_free_port(runner)
|
||||
|
||||
|
||||
def webconnectivity_http_connection_refused_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's TCP/IP blocking w/ consistent DNS that occurs
|
||||
while we're following the chain of redirects. """
|
||||
# We use a bit.ly link redirecting to nexa.polito.it. We block the IP address
|
||||
# used by nexa.polito.it. So the error should happen in the redirect chain.
|
||||
ip = socket.gethostbyname("nexa.polito.it")
|
||||
args = [
|
||||
"-iptables-reset-ip",
|
||||
ip,
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://bit.ly/3h9EJR3 web_connectivity",
|
||||
"webconnectivity_http_connection_refused_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"] == "connection_refused"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "http-failure"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8320)
|
||||
|
||||
|
||||
def webconnectivity_http_connection_reset_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's RST-based blocking blocking w/ consistent DNS that
|
||||
occurs while we're following the chain of redirects. """
|
||||
# We use a bit.ly link redirecting to nexa.polito.it. We block the Host header
|
||||
# used for nexa.polito.it. So the error should happen in the redirect chain.
|
||||
args = [
|
||||
"-iptables-reset-keyword",
|
||||
"Host: nexa",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://bit.ly/3h9EJR3 web_connectivity",
|
||||
"webconnectivity_http_connection_reset_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"] == "connection_reset"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "http-failure"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8448)
|
||||
|
||||
|
||||
def webconnectivity_http_nxdomain_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's a redirection and the redirected request cannot
|
||||
continue because a NXDOMAIN error occurs. """
|
||||
# We use a bit.ly link redirecting to nexa.polito.it. We block the DNS request
|
||||
# for nexa.polito.it. So the error should happen in the redirect chain.
|
||||
args = [
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-block",
|
||||
"nexa.polito.it",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://bit.ly/3h9EJR3 web_connectivity",
|
||||
"webconnectivity_http_nxdomain_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"] == "dns_nxdomain_error" # miniooni
|
||||
or tk["http_experiment_failure"] == "dns_lookup_error" # MK
|
||||
)
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "dns"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8224)
|
||||
|
||||
|
||||
def webconnectivity_http_eof_error_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's a redirection and the redirected request cannot
|
||||
continue because an eof_error error occurs. """
|
||||
# We use a bit.ly link redirecting to nexa.polito.it. We block the HTTP request
|
||||
# for nexa.polito.it using the cleartext bad proxy. So the error should happen in
|
||||
# the redirect chain and should be EOF.
|
||||
args = [
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-hijack",
|
||||
"nexa.polito.it",
|
||||
"-iptables-hijack-http-to",
|
||||
"127.0.0.1:7117", # this is badproxy's cleartext endpoint
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://bit.ly/3h9EJR3 web_connectivity", # bit.ly uses https
|
||||
"webconnectivity_http_eof_error_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"] == "eof_error"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "http-failure"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8448)
|
||||
|
||||
|
||||
def webconnectivity_http_generic_timeout_error_with_consistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's a redirection and the redirected request cannot
|
||||
continue because a generic_timeout_error error occurs. """
|
||||
# We use a bit.ly link redirecting to nexa.polito.it. We block the HTTP request
|
||||
# for nexa.polito.it by dropping packets using DPI. So the error should happen in
|
||||
# the redirect chain and should be timeout.
|
||||
args = [
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-hijack",
|
||||
"nexa.polito.it",
|
||||
"-iptables-drop-keyword",
|
||||
"Host: nexa",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://bit.ly/3h9EJR3 web_connectivity",
|
||||
"webconnectivity_http_generic_timeout_error_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"] == "generic_timeout_error"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "http-failure"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8704)
|
||||
|
||||
|
||||
def webconnectivity_http_connection_reset_with_inconsistent_dns(ooni_exe, outfile):
|
||||
""" Test case where there's inconsistent DNS and the connection is RST when
|
||||
we're executing HTTP code. """
|
||||
args = [
|
||||
"-iptables-reset-keyword",
|
||||
"nexa.polito.it",
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-hijack",
|
||||
"polito",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://nexa.polito.it/ web_connectivity",
|
||||
"webconnectivity_http_connection_reset_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"] == "connection_reset"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "dns"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 8480)
|
||||
|
||||
|
||||
def webconnectivity_http_successful_website(ooni_exe, outfile):
|
||||
""" Test case where we succeed with an HTTP only webpage """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i http://example.org/ web_connectivity",
|
||||
"webconnectivity_http_successful_website",
|
||||
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"] == True
|
||||
assert tk["body_proportion"] == 1
|
||||
assert tk["status_code_match"] == True
|
||||
assert tk["headers_match"] == True
|
||||
assert tk["title_match"] == True
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 2)
|
||||
|
||||
|
||||
def webconnectivity_https_successful_website(ooni_exe, outfile):
|
||||
""" Test case where we succeed with an HTTPS only webpage """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.com/ web_connectivity",
|
||||
"webconnectivity_https_successful_website",
|
||||
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"] == True
|
||||
assert tk["body_proportion"] == 1
|
||||
assert tk["status_code_match"] == True
|
||||
assert tk["headers_match"] == True
|
||||
assert tk["title_match"] == True
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 1)
|
||||
|
||||
|
||||
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"] == False
|
||||
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"] == False
|
||||
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 = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://expired.badssl.com/ web_connectivity",
|
||||
"webconnectivity_https_expired_certificate",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "consistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == "ssl_invalid_certificate"
|
||||
else:
|
||||
assert "certificate verify failed" in tk["http_experiment_failure"]
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
# The following strikes me as a measurement_kit bug. We are saying
|
||||
# that all is good with a domain where actually we don't know why the
|
||||
# control is failed and that is clearly not accessible according to
|
||||
# our measurement of the domain (certificate expired).
|
||||
#
|
||||
# See <https://github.com/ooni/probe-engine/issues/858>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
else:
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 16)
|
||||
|
||||
|
||||
def webconnectivity_https_wrong_host(ooni_exe, outfile):
|
||||
""" Test case where the hostname is wrong for the certificate """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://wrong.host.badssl.com/ web_connectivity",
|
||||
"webconnectivity_https_wrong_host",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "consistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == "ssl_invalid_hostname"
|
||||
else:
|
||||
assert "certificate verify failed" in tk["http_experiment_failure"]
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
# The following strikes me as a measurement_kit bug. We are saying
|
||||
# that all is good with a domain where actually we don't know why the
|
||||
# control is failed and that is clearly not accessible according to
|
||||
# our measurement of the domain (wrong host for certificate).
|
||||
#
|
||||
# See <https://github.com/ooni/probe-engine/issues/858>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
else:
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 16)
|
||||
|
||||
|
||||
def webconnectivity_https_self_signed(ooni_exe, outfile):
|
||||
""" Test case where the certificate is self signed """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://self-signed.badssl.com/ web_connectivity",
|
||||
"webconnectivity_https_self_signed",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "consistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == "ssl_unknown_authority"
|
||||
else:
|
||||
assert "certificate verify failed" in tk["http_experiment_failure"]
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
# The following strikes me as a measurement_kit bug. We are saying
|
||||
# that all is good with a domain where actually we don't know why the
|
||||
# control is failed and that is clearly not accessible according to
|
||||
# our measurement of the domain (self signed certificate).
|
||||
#
|
||||
# See <https://github.com/ooni/probe-engine/issues/858>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
else:
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 16)
|
||||
|
||||
|
||||
def webconnectivity_https_untrusted_root(ooni_exe, outfile):
|
||||
""" Test case where the certificate has an untrusted root """
|
||||
args = []
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://untrusted-root.badssl.com/ web_connectivity",
|
||||
"webconnectivity_https_untrusted_root",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "consistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == "ssl_unknown_authority"
|
||||
else:
|
||||
assert "certificate verify failed" in tk["http_experiment_failure"]
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
# The following strikes me as a measurement_kit bug. We are saying
|
||||
# that all is good with a domain where actually we don't know why the
|
||||
# control is failed and that is clearly not accessible according to
|
||||
# our measurement of the domain (untrusted root certificate).
|
||||
#
|
||||
# See <https://github.com/ooni/probe-engine/issues/858>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["blocking"] == None
|
||||
assert tk["accessible"] == None
|
||||
else:
|
||||
assert tk["blocking"] == False
|
||||
assert tk["accessible"] == True
|
||||
assert_status_flags_are(ooni_exe, tk, 16)
|
||||
|
||||
|
||||
def webconnectivity_dns_blocking_nxdomain(ooni_exe, outfile):
|
||||
""" Test case where there is blocking using NXDOMAIN """
|
||||
args = [
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-block",
|
||||
"example.com",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.com/ web_connectivity",
|
||||
"webconnectivity_dns_blocking_nxdomain",
|
||||
args,
|
||||
)
|
||||
# The following seems a bug in MK where we don't properly record the
|
||||
# actual error that occurred when performing the DNS experiment.
|
||||
#
|
||||
# See <https://github.com/measurement-kit/measurement-kit/issues/1931>.
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["dns_experiment_failure"] == "dns_nxdomain_error"
|
||||
else:
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "inconsistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == None
|
||||
else:
|
||||
assert tk["http_experiment_failure"] == "dns_lookup_error"
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "dns"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 2080)
|
||||
|
||||
|
||||
def webconnectivity_https_unknown_authority_with_inconsistent_dns(ooni_exe, outfile):
|
||||
""" Test case where the DNS is sending us towards a website where
|
||||
we're served an invalid certificate """
|
||||
args = [
|
||||
"-iptables-hijack-dns-to",
|
||||
"127.0.0.1:53",
|
||||
"-dns-proxy-hijack",
|
||||
"example.org",
|
||||
"-bad-proxy-address-tls",
|
||||
"127.0.0.1:443",
|
||||
"-tls-proxy-address",
|
||||
"127.0.0.1:4114",
|
||||
]
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe,
|
||||
outfile,
|
||||
"-i https://example.org/ web_connectivity",
|
||||
"webconnectivity_https_unknown_authority_with_inconsistent_dns",
|
||||
args,
|
||||
)
|
||||
assert tk["dns_experiment_failure"] == None
|
||||
assert tk["dns_consistency"] == "inconsistent"
|
||||
assert tk["control_failure"] == None
|
||||
if "miniooni" in ooni_exe:
|
||||
assert tk["http_experiment_failure"] == "ssl_unknown_authority"
|
||||
else:
|
||||
assert "certificate verify failed" in tk["http_experiment_failure"]
|
||||
assert tk["body_length_match"] == None
|
||||
assert tk["body_proportion"] == 0
|
||||
assert tk["status_code_match"] == None
|
||||
assert tk["headers_match"] == None
|
||||
assert tk["title_match"] == None
|
||||
assert tk["blocking"] == "dns"
|
||||
assert tk["accessible"] == False
|
||||
assert_status_flags_are(ooni_exe, tk, 9248)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "webconnectivity.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
webconnectivity_https_ok_with_control_failure,
|
||||
webconnectivity_http_ok_with_control_failure,
|
||||
webconnectivity_transparent_http_proxy,
|
||||
webconnectivity_dns_hijacking,
|
||||
webconnectivity_control_unreachable_and_using_http,
|
||||
webconnectivity_nonexistent_domain,
|
||||
webconnectivity_tcpip_blocking_with_consistent_dns,
|
||||
webconnectivity_tcpip_blocking_with_inconsistent_dns,
|
||||
webconnectivity_http_connection_refused_with_consistent_dns,
|
||||
webconnectivity_http_connection_reset_with_consistent_dns,
|
||||
webconnectivity_http_nxdomain_with_consistent_dns,
|
||||
webconnectivity_http_eof_error_with_consistent_dns,
|
||||
webconnectivity_http_generic_timeout_error_with_consistent_dns,
|
||||
webconnectivity_http_connection_reset_with_inconsistent_dns,
|
||||
webconnectivity_http_successful_website,
|
||||
webconnectivity_https_successful_website,
|
||||
webconnectivity_http_diff_with_inconsistent_dns,
|
||||
webconnectivity_http_diff_with_consistent_dns,
|
||||
webconnectivity_https_expired_certificate,
|
||||
webconnectivity_https_wrong_host,
|
||||
webconnectivity_https_self_signed,
|
||||
webconnectivity_https_untrusted_root,
|
||||
webconnectivity_dns_blocking_nxdomain,
|
||||
webconnectivity_https_unknown_authority_with_inconsistent_dns,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
""" ./QA/whatsapp.py - main QA script for whatsapp
|
||||
|
||||
This script performs a bunch of whatsapp tests under censored
|
||||
network conditions and verifies that the measurement is consistent
|
||||
with the expectations, by parsing the resulting JSONL. """
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import common
|
||||
|
||||
|
||||
def execute_jafar_and_return_validated_test_keys(ooni_exe, outfile, tag, args):
|
||||
""" Executes jafar and returns the validated parsed test keys, or throws
|
||||
an AssertionError if the result is not valid. """
|
||||
tk = common.execute_jafar_and_miniooni(ooni_exe, outfile, "whatsapp", tag, args)
|
||||
assert isinstance(tk["requests"], list)
|
||||
assert len(tk["requests"]) > 0
|
||||
for entry in tk["requests"]:
|
||||
assert isinstance(entry, dict)
|
||||
failure = entry["failure"]
|
||||
assert isinstance(failure, str) or failure is None
|
||||
assert isinstance(entry["request"], dict)
|
||||
req = entry["request"]
|
||||
common.check_maybe_binary_value(req["body"])
|
||||
assert isinstance(req["headers"], dict)
|
||||
for key, value in req["headers"].items():
|
||||
assert isinstance(key, str)
|
||||
common.check_maybe_binary_value(value)
|
||||
assert isinstance(req["method"], str)
|
||||
assert isinstance(entry["response"], dict)
|
||||
resp = entry["response"]
|
||||
common.check_maybe_binary_value(resp["body"])
|
||||
assert isinstance(resp["code"], int)
|
||||
if resp["headers"] is not None:
|
||||
for key, value in resp["headers"].items():
|
||||
assert isinstance(key, str)
|
||||
common.check_maybe_binary_value(value)
|
||||
assert isinstance(tk["tcp_connect"], list)
|
||||
assert len(tk["tcp_connect"]) > 0
|
||||
for entry in tk["tcp_connect"]:
|
||||
assert isinstance(entry, dict)
|
||||
assert isinstance(entry["ip"], str)
|
||||
assert isinstance(entry["port"], int)
|
||||
assert isinstance(entry["status"], dict)
|
||||
failure = entry["status"]["failure"]
|
||||
success = entry["status"]["success"]
|
||||
assert isinstance(failure, str) or failure is None
|
||||
assert isinstance(success, bool)
|
||||
return tk
|
||||
|
||||
|
||||
def helper_for_blocking_endpoints(start, stop):
|
||||
""" Helper function for generating args for blocking endpoints """
|
||||
args = []
|
||||
for num in range(start, stop):
|
||||
args.append("-iptables-reset-ip")
|
||||
args.append("e{}.whatsapp.net".format(num))
|
||||
return args
|
||||
|
||||
|
||||
def args_for_blocking_all_endpoints():
|
||||
""" Returns the arguments useful for blocking all endpoints """
|
||||
return helper_for_blocking_endpoints(1, 17)
|
||||
|
||||
|
||||
def args_for_blocking_some_endpoints():
|
||||
""" Returns the arguments useful for blocking some endpoints """
|
||||
# Implementation note: apparently all the endpoints are now using just
|
||||
# four IP addresses, hence here we block some endpoints via DNS.
|
||||
#
|
||||
# TODO(bassosimone): this fact calls for creating an issue for making
|
||||
# the whatsapp experiment implementation more efficient.
|
||||
args = []
|
||||
args.append("-iptables-hijack-dns-to")
|
||||
args.append("127.0.0.1:53")
|
||||
for n in range(1, 7):
|
||||
args.append("-dns-proxy-block")
|
||||
args.append("e{}.whatsapp.net".format(n))
|
||||
return args
|
||||
|
||||
|
||||
def args_for_blocking_v_whatsapp_net_https():
|
||||
""" Returns arguments for blocking v.whatsapp.net over https """
|
||||
#
|
||||
# 00 00 <SNI extension ID>
|
||||
# 00 13 <full extension length>
|
||||
# 00 11 <first entry length>
|
||||
# 00 <DNS hostname type>
|
||||
# 00 0e <string length>
|
||||
# 76 2e ... 74 v.whatsapp.net
|
||||
#
|
||||
return [
|
||||
"-iptables-reset-keyword-hex",
|
||||
"|00 00 00 13 00 11 00 00 0e 76 2e 77 68 61 74 73 61 70 70 2e 6e 65 74|",
|
||||
]
|
||||
|
||||
|
||||
def args_for_blocking_web_whatsapp_com_http():
|
||||
""" Returns arguments for blocking web.whatsapp.com over http """
|
||||
return ["-iptables-reset-keyword", "Host: web.whatsapp.com"]
|
||||
|
||||
|
||||
def args_for_blocking_web_whatsapp_com_https():
|
||||
""" Returns arguments for blocking web.whatsapp.com over https """
|
||||
#
|
||||
# 00 00 <SNI extension ID>
|
||||
# 00 15 <full extension length>
|
||||
# 00 13 <first entry length>
|
||||
# 00 <DNS hostname type>
|
||||
# 00 10 <string length>
|
||||
# 77 65 ... 6d web.whatsapp.com
|
||||
#
|
||||
return [
|
||||
"-iptables-reset-keyword-hex",
|
||||
"|00 00 00 15 00 13 00 00 10 77 65 62 2e 77 68 61 74 73 61 70 70 2e 63 6f 6d|",
|
||||
]
|
||||
|
||||
|
||||
def whatsapp_block_everything(ooni_exe, outfile):
|
||||
""" Test case where everything we measure is blocked """
|
||||
args = []
|
||||
args.extend(args_for_blocking_all_endpoints())
|
||||
args.extend(args_for_blocking_v_whatsapp_net_https())
|
||||
args.extend(args_for_blocking_web_whatsapp_com_https())
|
||||
args.extend(args_for_blocking_web_whatsapp_com_http())
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_everything", args,
|
||||
)
|
||||
assert tk["registration_server_failure"] == "connection_reset"
|
||||
assert tk["registration_server_status"] == "blocked"
|
||||
assert tk["whatsapp_endpoints_status"] == "blocked"
|
||||
assert tk["whatsapp_web_failure"] == "connection_reset"
|
||||
assert tk["whatsapp_web_status"] == "blocked"
|
||||
|
||||
|
||||
def whatsapp_block_all_endpoints(ooni_exe, outfile):
|
||||
""" Test case where we only block whatsapp endpoints """
|
||||
args = args_for_blocking_all_endpoints()
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_all_endpoints", args
|
||||
)
|
||||
assert tk["registration_server_failure"] == None
|
||||
assert tk["registration_server_status"] == "ok"
|
||||
assert tk["whatsapp_endpoints_status"] == "blocked"
|
||||
assert tk["whatsapp_web_failure"] == None
|
||||
assert tk["whatsapp_web_status"] == "ok"
|
||||
|
||||
|
||||
def whatsapp_block_some_endpoints(ooni_exe, outfile):
|
||||
""" Test case where we block some whatsapp endpoints """
|
||||
args = args_for_blocking_some_endpoints()
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_some_endpoints", args
|
||||
)
|
||||
assert tk["registration_server_failure"] == None
|
||||
assert tk["registration_server_status"] == "ok"
|
||||
assert tk["whatsapp_endpoints_status"] == "ok"
|
||||
assert tk["whatsapp_web_failure"] == None
|
||||
assert tk["whatsapp_web_status"] == "ok"
|
||||
|
||||
|
||||
def whatsapp_block_registration_server(ooni_exe, outfile):
|
||||
""" Test case where we block the registration server """
|
||||
args = []
|
||||
args.extend(args_for_blocking_v_whatsapp_net_https())
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_registration_server", args,
|
||||
)
|
||||
assert tk["registration_server_failure"] == "connection_reset"
|
||||
assert tk["registration_server_status"] == "blocked"
|
||||
assert tk["whatsapp_endpoints_status"] == "ok"
|
||||
assert tk["whatsapp_web_failure"] == None
|
||||
assert tk["whatsapp_web_status"] == "ok"
|
||||
|
||||
|
||||
def whatsapp_block_web_http(ooni_exe, outfile):
|
||||
""" Test case where we block the HTTP web chat """
|
||||
args = []
|
||||
args.extend(args_for_blocking_web_whatsapp_com_http())
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_web_http", args,
|
||||
)
|
||||
assert tk["registration_server_failure"] == None
|
||||
assert tk["registration_server_status"] == "ok"
|
||||
assert tk["whatsapp_endpoints_status"] == "ok"
|
||||
assert tk["whatsapp_web_failure"] == "connection_reset"
|
||||
assert tk["whatsapp_web_status"] == "blocked"
|
||||
|
||||
|
||||
def whatsapp_block_web_https(ooni_exe, outfile):
|
||||
""" Test case where we block the HTTPS web chat """
|
||||
args = []
|
||||
args.extend(args_for_blocking_web_whatsapp_com_https())
|
||||
tk = execute_jafar_and_return_validated_test_keys(
|
||||
ooni_exe, outfile, "whatsapp_block_web_https", args,
|
||||
)
|
||||
assert tk["registration_server_failure"] == None
|
||||
assert tk["registration_server_status"] == "ok"
|
||||
assert tk["whatsapp_endpoints_status"] == "ok"
|
||||
assert tk["whatsapp_web_failure"] == "connection_reset"
|
||||
assert tk["whatsapp_web_status"] == "blocked"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
|
||||
outfile = "whatsapp.jsonl"
|
||||
ooni_exe = sys.argv[1]
|
||||
tests = [
|
||||
whatsapp_block_everything,
|
||||
whatsapp_block_all_endpoints,
|
||||
whatsapp_block_some_endpoints,
|
||||
whatsapp_block_registration_server,
|
||||
whatsapp_block_web_http,
|
||||
whatsapp_block_web_https,
|
||||
]
|
||||
for test in tests:
|
||||
test(ooni_exe, outfile)
|
||||
time.sleep(7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user