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:
Simone Basso
2021-02-03 13:20:37 +01:00
committed by GitHub
parent 4eeadd06a5
commit 0c48bc0746
23 changed files with 18 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
*
+3
View File
@@ -0,0 +1,3 @@
/GOPATH
/GOCACHE
/__pycache__
+2
View File
@@ -0,0 +1,2 @@
FROM golang:1.14-alpine
RUN apk add go git musl-dev iptables tmux bind-tools curl sudo python3
+54
View File
@@ -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`.
+72
View File
@@ -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])
+286
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+88
View File
@@ -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
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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()
+855
View File
@@ -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
View File
@@ -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()