Commit e9dc4d65 authored by Stephane Bortzmeyer's avatar Stephane Bortzmeyer
Browse files

Several levels of complicance for the --check option

parent 86ef3293
......@@ -65,8 +65,14 @@ The program stops after the first failed test.
OK
```
A future work would be to define different set of tests to test the
compliance of a DoT or DoH server.
Each test is marked with a level of compliance. There are three
levels, "legal" (compliant with the strict requirments of the RFCs),
"necessary" (in a typical setup) and "nicetohave". The default level
is "necessary" but you can change it with option
`--mandatory-check`. For instance, sending a reply when the request
uses the HEAD method is "nicetohave" for a DoH server (the RFC does
not mandate it). The tests are always performed but are not fatal if
the choosen level is lower than the level of the test.
### Repetition of tests
......@@ -234,7 +240,7 @@ future, to remove servers that do not validate with DNSSEC.)
* `https://doh.libredns.gr/dns-query`
([Documentation](https://libredns.gr/); Also,
`https://doh.libredns.gr/ads` is a lying resolver, blocking ads and trackers)
* `https://dns.switch.ch/` ([Documentation](https://www.switch.ch/security/info/public-dns/))
* `https://dns.switch.ch/dns-query` ([Documentation](https://www.switch.ch/security/info/public-dns/))
* `https://nat64.tuxis.nl`
([Documentation](https://www.tuxis.nl/blog/public-doh-dot-dns64-nat64-service-20191021/);
NAT64, and no IPv4 address)
......
......@@ -33,8 +33,6 @@ import hashlib
import base64
import signal
check_additional = False
# Values that can be changed from the command line
dot = False # DoH by default
verbose = False
......@@ -52,6 +50,8 @@ forceIPv4 = False
forceIPv6 = False
connectTo = None
check = False
mandatory_level = None
check_additional = True
# Monitoring plugin only:
host = None
path = None
......@@ -70,6 +70,8 @@ STATE_DEPENDENT = 4
DOH_GET = 0
DOH_POST = 1
DOH_HEAD = 2
# Is the test mandatory?
mandatory_levels = {"legal": 30, "necessary": 20, "nicetohave": 10}
TIMEOUT_CONN = 2
......@@ -482,7 +484,10 @@ class ConnectionDoH(Connection):
body = self.buffer.getvalue()
body_size = len(body)
http_code = self.curl.getinfo(pycurl.RESPONSE_CODE)
content_type = self.curl.getinfo(pycurl.CONTENT_TYPE)
try:
content_type = self.curl.getinfo(pycurl.CONTENT_TYPE)
except TypeError: # This is the exception we get if there is no Content-Type: (for intance in rsponse to HEAD requests)
content_type = None
request.response = body
request.response_size = body_size
request.rcode = http_code
......@@ -516,7 +521,7 @@ def get_next_domain(input_file):
(name, rtype) = line.split()
return name, rtype
def print_result(connection, request, prefix=None):
def print_result(connection, request, prefix=None, display_err=True):
ok = request.ok
dot = connection.dot
server = connection.server
......@@ -535,16 +540,17 @@ def print_result(connection, request, prefix=None):
sys.exit(STATE_OK)
else:
if not monitoring:
if prefix:
print(prefix, end=': ', file=sys.stderr)
if dot:
print("Error: %s" % msg, file=sys.stderr)
else:
try:
msg = msg.decode()
except (UnicodeDecodeError, AttributeError):
pass # Sometimes, msg can be binary, or Latin-1
print("HTTP error %i: %s" % (rcode, msg), file=sys.stderr)
if display_err:
if prefix:
print(prefix, end=': ', file=sys.stderr)
if dot:
print("Error: %s" % msg, file=sys.stderr)
else:
try:
msg = msg.decode()
except (UnicodeDecodeError, AttributeError):
pass # Sometimes, msg can be binary, or Latin-1
print("HTTP error %i: %s" % (rcode, msg), file=sys.stderr)
else:
if not dot:
print("%s HTTP error - %i: %s" % (server, rcode, msg))
......@@ -568,12 +574,21 @@ def create_request(dot=dot, trunc=False, **req_args):
def create_requests_list(dot=dot, **req_args):
requests = []
if dot:
requests.append(('Test 1', create_request(dot=dot, **req_args)))
requests.append(('Test 2', create_request(dot=dot, **req_args)))
requests.append(('Test 1', create_request(dot=dot, **req_args),
mandatory_levels["legal"]))
requests.append(('Test 2', create_request(dot=dot, **req_args),
mandatory_levels["necessary"])) # RFC 7858,
# section 3.3, SHOULD accept several requests on one connection.
# TODO we miss the tests of pipelining and out-of-order.
else:
requests.append(('Test GET', create_request(**req_args), DOH_GET))
requests.append(('Test POST', create_request(**req_args), DOH_POST))
requests.append(('Test HEAD', create_request(**req_args), DOH_HEAD))
requests.append(('Test GET', create_request(**req_args), DOH_GET,
mandatory_levels["legal"])) # RFC 8484, section 4.1
requests.append(('Test POST', create_request(**req_args), DOH_POST,
mandatory_levels["legal"])) # RFC 8484, section 4.1
requests.append(('Test HEAD', create_request(**req_args), DOH_HEAD,
mandatory_levels["nicetohave"])) # HEAD
# method is not mentioned in RFC 8484 (see section 4.1), so
# just "nice to have".
return requests
def run_check_default(connection):
......@@ -582,9 +597,9 @@ def run_check_default(connection):
requests = create_requests_list(dot=dot, **req_args)
for request_pack in requests:
if dot:
test_name, request = request_pack
test_name, request, mandatory = request_pack
else:
test_name, request, method = request_pack
test_name, request, method, mandatory = request_pack
if verbose:
print(test_name)
if not dot:
......@@ -599,8 +614,10 @@ def run_check_default(connection):
error(e)
break
request.check_response()
if not print_result(connection, request, prefix=test_name):
ok = False
if not print_result(connection, request, prefix=test_name, display_err=False):
if mandatory >= mandatory_level:
print_result(connection, request, prefix=test_name, display_err=True)
ok = False
break
return ok
......@@ -637,23 +654,26 @@ def run_check_trunc(connection):
request = create_request(trunc=True, **req_args)
request.post = True
try:
# 8.8.8.8 replies FORMERR but most DoT servers violently shut down the connection (which is legal)
connection.send_and_receive(request)
except CustomException as e:
ok = False
error(e)
except OpenSSL.SSL.ZeroReturnError: # This is acceptable
return ok
request.check_response()
if not print_result(connection, request, prefix=test_name):
ok = False
if print_result(connection, request, prefix=test_name, display_err=False): # The test must fail, or returns FORMERR.
ok = (request.rcode == dns.rcode.FORMERR)
return ok
def run_check_additionals(connection):
if not run_check_trunc(connection):
return False
if not run_check_mime(connection, accept="text/html"):
return False
if not run_check_mime(connection, content_type="text/html"):
return False
# The DoH server is right to reject these (Example: 'HTTP
# error 415: only Content-Type: application/dns-message is
# supported')
run_check_mime(connection, accept="text/html")
run_check_mime(connection, content_type="text/html")
return True
def run_check(connection):
......@@ -673,7 +693,8 @@ if not monitoring:
optlist, args = getopt.getopt (sys.argv[1:], "hvPkeV:r:f:d:t46",
["help", "verbose", "dot", "head",
"insecure", "POST", "vhost=",
"dnssec", "noedns","repeat=", "file=", "delay=", "v4only", "v6only", "check"])
"dnssec", "noedns","repeat=", "file=", "delay=", "v4only", "v6only",
"check", "mandatory-level="])
for option, value in optlist:
if option == "--help" or option == "-h":
usage()
......@@ -710,6 +731,8 @@ if not monitoring:
forceIPv6 = True
elif option == "--check":
check = True
elif option == "--mandatory-level":
mandatory_level = value
else:
error("Unknown option %s" % option)
except getopt.error as reason:
......@@ -723,6 +746,16 @@ if not monitoring:
if dot and (post or head):
usage("POST or HEAD makes non sense for DoT")
sys.exit(1)
if mandatory_level is not None and \
mandatory_level not in mandatory_levels.keys():
usage("Unknown mandatory level \"%s\"" % mandatory_level)
sys.exit(1)
if mandatory_level is not None and not check:
usage("--mandatory-level only makes sense with --check")
sys.exit(1)
if mandatory_level is None:
mandatory_level = "necessary"
mandatory_level = mandatory_levels[mandatory_level]
if ifile is None and (len(args) != 2 and len(args) != 3):
usage("Wrong number of arguments")
sys.exit(1)
......
......@@ -6,6 +6,7 @@ config:
- "doh: test specific to DoH"
- "monitoring: test using monitoring"
- "exception: test raising an exception"
- "check: test related to the compliance option --check"
tests:
- exe: './homer.py'
......@@ -50,6 +51,64 @@ tests:
partstderr: 'not a name or'
stdout: ''
###############################################################################
- exe: './homer.py'
name: '--check of a correct DoH'
markers:
- 'doh'
- 'check'
args:
- '--check'
- 'https://doh.bortzmeyer.fr/'
- 'ressources-pedagogiques.org'
retcode: 0
stderr: ''
stdout: "OK\n"
- exe: './homer.py'
name: '--check of a broken DoH'
markers:
- 'doh'
- 'check'
args:
- '--check'
- 'https://www.bortzmeyer.org/'
- 'ressources-pedagogiques.org'
retcode: 1
stderr: ''
stdout: "KO\n"
- exe: './homer.py'
name: '--check of a DoH with HEAD unimplemented'
markers:
- 'doh'
- 'check'
args:
- '--check'
- '--mandatory-level'
- 'nicetohave'
- 'https://doh.42l.fr/dns-query'
- 'ressources-pedagogiques.org'
retcode: 1
stderr: "Test HEAD: HTTP error 405: [No details]\n"
stdout: "KO\n"
- exe: './homer.py'
name: '--check of a correct DoT'
markers:
- 'dot'
- 'check'
args:
- '--check'
- '--dot'
- 'dot.bortzmeyer.fr'
- 'ressources-pedagogiques.org'
retcode: 0
stderr: ''
stdout: "OK\n"
###############################################################################
- exe: './homer.py'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment