2020-11-12-001
- status: experimental
SNI blocking (sni_blocking
)
- An internet connection
Understanding whether there is blocking triggered by the content of the TLS Hello's SNI field. For a given SNI/domain, this nettest uses a test helper server, rather than talking to the server for the specified SNI/domain. This design is especially beneficial when additional blocking rules may cause the DNS to return bogus responses, or the IP address for the SNI/domain to be unreachable/filtered.
-
control_sni
(string
): a SNI to use as control (e.g.example.org
) -
testhelper
(endpoint
; optional): endpoint where TLS is enabled expressed asIPv4:port
,[IPv6]:port
, ordomain:port
(e.g.1.1.1.1:443
) -
targets
([]Target
) one or more SNIs to measure
If testhelper
is not specified we use ${control_sni}:443
.
The default implementation will use a domain such as example.org
as
the control_sni
and the empty string as testhelper endpoint. This will
effectively cause us to use example.org:443
(or whatever domain is
actually used on port 443
) as the testhelper endpoint.
A valid Target
is a valid domain name (e.g. kernel.org
), a valid IP
address (e.g. 1.1.1.1
), or a valid URL (e.g. http://x.org
). When the
input is a URL, the experiment will extract the domain from the URL and
use that as target SNI, ignoring any scheme, port, path, etc.
The user should be able to specify the above parameters from the CLI.
For every target
, this experiment will:
-
if
target
is a URL, parse it and settarget
as the URL's hostname -
determine
testhelper
fromcontrol_sni
if needed -
connect to
testhelper
usingtarget
as SNI -
connect to
testhelper
usingcontrol_sni
as SNI
The implementation may (i) randomly delay the moment where steps 3. and 4. start
such that step 3. does not strictly run before 4.; (ii) cache the result of step 4.
to avoid repeating it for every input target
.
{
"test_keys": {
"control": {},
"result": "",
"target": {}
}
}
-
control
(Subresult
): data collected by step 4 above -
result
(string
): classification of the result -
target
(Subresult
): data collected by step 3 above
A Subresult
data structure looks like:
{
"failure": null,
"network_events": [],
"queries": [],
"sni": "",
"tcp_connect": [],
"th_address": "",
"tls_handshakes": []
}
-
failure
(string
; nullable):null
on success, string on error as documented indf-007-errors.md
; -
network_events
([]NetworkEvent
; nullable): seedf-008-netevents
; -
queries
([]Query
; nullable): seedf-002-dnst
; -
requests
([]Transaction
; nullable): seedf-001-httpt
; -
sni
(string
): SNI being used; -
tcp_connect
([]TCPConnect
; nullable): seedf-005-tcpconnect
; -
th_address
(string
): address of the test helper (see above); -
tls_handshakes
([]Handshake
; nullable): seedf-006-tlshandshake
.
We expect requests
to be null
unless we're using DoH; queries
to
be null
when testhelper
is an IP.
The result
string summarizes what happens during the nettest. Its
value is one of the following:
-
"anomaly.test_helper_unreachable"
: iftesthelper
is a domain we could not resolve the domain, or we could not connect totesthelper
, or we saw a timeout when measuring the target and also the control measurement failed with any error. This is anomaly because we need to look into the data to understand whether the test helper is down, blocked, or what. -
"anomaly.timeout"
: the control measurement succeded, but we did saw an I/O timeout when measuring with thetarget
SNI. This is anomaly because the timeout may be explained by conditions different from blocking. -
"anomaly.unexpected_failure"
: when measuring thetarget
SNI we did saw a failure other than the set of failures we expected. This is anomaly and we want to look into this measurement and improve our implementation. -
"interference.closed"
: the connection was closed during the TLS handshake. We flag this as interference because we expect the test helper to return a TLS Server Hello message to us. It might also be the case that the test helper is very busy and closes incoming connections. When there is doubt, we can increase our confidence by inspecting the control measurement as well as other measurements with the same report ID. -
"interference.reset"
: like"interference.closed"
except that the connection is resetted rather than just being gracefully closed. -
"interference.invalid_certificate"
: we got a TLS Server Hello message back, but the certificate in it is invalid (e.g. expired). This is very likely to be a sign of interference, unless the test helper has an expired certificate. A future version of this document will explain how the probe should handle this specific corner case. -
"interference.unknown_authority"
: we got a TLS Server Hello message back, but we don't know the authority signing the certificate. This is a sign of man in the middle in most cases, even though there may be corner cases leading to false positives. A future version of this document may provide recommendations concerning detecting such false positives. -
"success.got_server_hello"
: we were able to get a TLS Server Hello back from the server, without any of the above errors. This is what we expect to happen when there's no interference.
Any other value should be treated as "anomaly.unexpected_failure"
by
code that is processing the JSON measurement.
See the above fields description.
Fields described above (mind that many are nullable).
We examine the failure
field of control
and target
. Because we're
performing a TLS handshake with a TLS server that may not support the
specified SNIs, we consider null
and ssl_invalid_hostname
as indicators
of success. We consider any other error as potentially an anomaly.
We cannot immediately exclude the presence of a MITM box that forwards
legitimate traffic and returns invalid certificates otherwise, thus causing
ssl_invalid_hostname
replies. However, the sequence of network events
may possibly be useful to detect these specific cases.
For this reason, it is ideal to select as test helper endpoint one that knows how to handle one the control SNI, which is what the implementation of this experiment should be doing by default.
{
"data_format_version": "0.3.4",
"input": "blocked.com",
"measurement_start_time": "2020-01-28 15:27:18",
"test_runtime": 0.335919812,
"probe_asn": "AS30722",
"probe_cc": "IT",
"probe_ip": "127.0.0.1",
"report_id": "20200128T152718Z_AS30722_ppk81LCnhT5Ok1P6909MX9XG8L6jmgSZkJyhx8KTmN4LAVAgGX",
"resolver_asn": "AS30722",
"resolver_ip": "91.80.36.88",
"resolver_network_name": "Vodafone Italia S.p.A.",
"software_name": "miniooni",
"software_version": "0.1.0-dev",
"test_keys": {
"control": {
"failure": null,
"network_events": [
{
"address": "37.218.245.90:443",
"conn_id": 1,
"dial_id": 1,
"failure": null,
"operation": "connect",
"proto": "tcp",
"t": 0.249971
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 262,
"operation": "write",
"proto": "tcp",
"t": 0.250393
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 517,
"operation": "read",
"proto": "tcp",
"t": 0.294079
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 923,
"operation": "read",
"proto": "tcp",
"t": 0.294199
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 1440,
"operation": "read",
"proto": "tcp",
"t": 0.294405
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 294,
"operation": "read",
"proto": "tcp",
"t": 0.295752
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 85,
"operation": "write",
"proto": "tcp",
"t": 0.296475
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 43,
"operation": "read",
"proto": "tcp",
"t": 0.335268
},
{
"conn_id": 1,
"failure": null,
"num_bytes": 23,
"operation": "write",
"proto": "tcp",
"t": 0.335577
}
],
"queries": [
{
"answers": [
{
"answer_type": "A",
"ipv4": "37.218.245.90",
"ttl": null
}
],
"dial_id": 1,
"engine": "system",
"failure": null,
"hostname": "ps.ooni.io",
"query_type": "A",
"resolver_hostname": null,
"resolver_port": null,
"resolver_address": "",
"t": 0.211333
},
{
"answers": null,
"dial_id": 1,
"engine": "system",
"failure": null,
"hostname": "ps.ooni.io",
"query_type": "AAAA",
"resolver_hostname": null,
"resolver_port": null,
"resolver_address": "",
"t": 0.211333
}
],
"requests": null,
"sni": "ps.ooni.io",
"tcp_connect": [
{
"conn_id": 1,
"dial_id": 1,
"ip": "37.218.245.90",
"port": 443,
"status": {
"failure": null,
"success": true
},
"t": 0.249971
}
],
"th_address": "ps.ooni.io:443",
"tls_handshakes": [
{
"cipher_suite": "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
"conn_id": 1,
"failure": null,
"negotiated_protocol": "",
"peer_certificates": [
{
"data": "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",
"format": "base64"
},
{
"data": "MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==",
"format": "base64"
}
],
"t": 0.335446,
"tls_version": "TLSv1.2"
}
]
},
"target": {
"failure": "ssl_invalid_hostname",
"network_events": [
{
"address": "37.218.245.90:443",
"conn_id": 2,
"dial_id": 2,
"failure": null,
"operation": "connect",
"proto": "tcp",
"t": 0.249802
},
{
"conn_id": 2,
"failure": null,
"num_bytes": 263,
"operation": "write",
"proto": "tcp",
"t": 0.250285
},
{
"conn_id": 2,
"failure": null,
"num_bytes": 517,
"operation": "read",
"proto": "tcp",
"t": 0.298164
},
{
"conn_id": 2,
"failure": null,
"num_bytes": 923,
"operation": "read",
"proto": "tcp",
"t": 0.298283
},
{
"conn_id": 2,
"failure": null,
"num_bytes": 1440,
"operation": "read",
"proto": "tcp",
"t": 0.298325
},
{
"conn_id": 2,
"failure": null,
"num_bytes": 7,
"operation": "write",
"proto": "tcp",
"t": 0.299192
}
],
"queries": [
{
"answers": [
{
"answer_type": "A",
"ipv4": "37.218.245.90",
"ttl": null
}
],
"dial_id": 2,
"engine": "system",
"failure": null,
"hostname": "ps.ooni.io",
"query_type": "A",
"resolver_hostname": null,
"resolver_port": null,
"resolver_address": "",
"t": 0.211333
},
{
"answers": null,
"dial_id": 2,
"engine": "system",
"failure": null,
"hostname": "ps.ooni.io",
"query_type": "AAAA",
"resolver_hostname": null,
"resolver_port": null,
"resolver_address": "",
"t": 0.211333
}
],
"requests": null,
"sni": "blocked.com",
"tcp_connect": [
{
"conn_id": 2,
"dial_id": 2,
"ip": "37.218.245.90",
"port": 443,
"status": {
"failure": null,
"success": true
},
"t": 0.249802
}
],
"th_address": "ps.ooni.io:443",
"tls_handshakes": [
{
"cipher_suite": "",
"conn_id": 2,
"failure": "ssl_invalid_hostname",
"negotiated_protocol": "",
"peer_certificates": null,
"t": 0.299247,
"tls_version": ""
}
]
}
},
"test_name": "sni_blocking",
"test_start_time": "2020-01-28 15:27:18",
"test_version": "0.0.1"
}
This nettest may be less intrusive than other nettests that measure blocking of a specific host by connecting directly to it. In particular, we are not issuing DNS queries for the sensitive domain and we are not connecting to the sensitive IP address.
This test does not capture packets by default.
In Autumn 2019, @fortuna proposed, designed, and implemented a comprehensive domain connectivity nettest. The nettest presented here is a slightly modified version of the SNI blocking subtest of @fortuna's nettest. Researchers at CIS India used a similar methodology in November 2019 to measure SNI based blocking.
This nettest is still experimental. We need to define top-level keys, run measurements with blocking using Jafar, and further develop it.