monitoring.py 7.61 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3

# Homer is a DoH (DNS-over-HTTPS) and DoT (DNS-over-TLS) client. Its
# main purpose is to test DoH and DoT resolvers. Reference site is
# <https://framagit.org/bortzmeyer/homer/> See author, documentation,
# etc, there, or in the README.md included with the distribution.

import sys
import getopt
import urllib.parse
import time
import socket
import os.path

try:
    # http://pycurl.io/docs/latest
    import pycurl

    # http://www.dnspython.org/
    import dns.message

    # Octobre 2019: the Python GnuTLS bindings don't work with Python 3. So we use OpenSSL.
    # https://www.pyopenssl.org/
    # https://pyopenssl.readthedocs.io/
    import OpenSSL
except ImportError as e:
    print("Error: missing module")
    print(e)
    sys.exit(1)

import homer

# Values that can be changed from the command line
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# "H:n:p:V:t:e:Pih46k:x"
# Options
#   -H <host>   IP address or domain name of the server (necessary). If using
#               DoH, the url will be built as https://<host>/<path>
#   -n <name>   The domain name to resolve (necessary)
#   -V <vhost>  The virtual hostname to use
#   -t <rtype>  The DNS record type to resolve, default AAAA
#   -e <value>  expect (looks for expected string in output)
#   -p <path>   [DoH] URL path of the DoH service
#   -P          [DoH] Use HTTP POST method
#   -h          [DoH] Use HTTP HEAD method
#   -i          Do not check the certificate
#   -x          Do not perform SNI
#   -4          Force IPv4 resolution of url-or-servername
#   -6          Force IPv6 resolution of url-or-servername
#   -k <key>    [DoT] Authenticate a DoT resolver with its public <key> in
#               base64
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class opts:
    dot = False # DoH by default
    dnssec = False
    edns = True
    no_ecs = True
    connectTo = None
    # Monitoring plugin only:
    host = None
    vhostname = None
    rtype = 'AAAA'
    expect = None
    path = None
    post = False
    head = False
    insecure = False
    sni = True
    forceIPv4 = False
    forceIPv6 = False
    key = None # SPKI

# For the monitoring plugin
STATE_OK = 0
STATE_WARNING = 1
STATE_CRITICAL = 2
STATE_UNKNOWN = 3
STATE_DEPENDENT = 4

def error(msg=None):
    if msg is None:
        msg = "Unknown error"
    print("%s: %s" % (url, msg))
    sys.exit(STATE_CRITICAL)

def print_result(connection, request, prefix=None, display_err=True):
    ok = request.ok
    dot = connection.dot
    server = connection.server
    rcode = request.rcode
    msg = request.response
    size = request.response_size
    if (dot and rcode) or (not dot and rcode == 200):
92
        if not request.has_expected_str(opts.expect):
93
            print("%s Cannot find \"%s\" in response" % (server, opts.expect))
94
            ok = False
95
96
        if ok and size is not None and size > 0:
            print("%s OK - %s" % (server, "No error for %s/%s, %i bytes received" % (name, opts.rtype, size)))
97
        elif ok:
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
            print("%s OK - %s" % (server, "No error"))
    else:
        if not dot:
            print("%s HTTP error - %i: %s" % (server, rcode, msg))
        else:
            print("%s Error - %i: %s" % (server, rcode, msg))
        ok = False
    return ok

def parse_opts_monitoring(me, opts):
    name = None
    opts.dot = (me == "check_dot")
    rtype = opts.rtype

    try:
        optlist, args = getopt.getopt (sys.argv[1:], "H:n:p:V:t:e:Pih46k:x")
        for option, value in optlist:
            if option == "-H":
                opts.host = value
            elif option == "-V":
                opts.vhostname = value
            elif option == "-n":
                name = value
            elif option == "-t":
                opts.rtype = value
            elif option == "-e":
                opts.expect = value
            elif option == "-p":
                opts.path = value
            elif option == "-P":
                opts.post = True
            elif option == "-h":
                opts.head = True
            elif option == "-i":
                opts.insecure = True
            elif option == "-x":
                opts.sni = False
            elif option == "-4":
                opts.forceIPv4 = True
            elif option == "-6":
                opts.forceIPv6 = True
            elif option == "-k":
                opts.key = value
            else:
                # Should never occur, it is trapped by getopt
                print("Unknown option %s" % option)
                sys.exit(STATE_UNKNOWN)
    except getopt.error as reason:
        print("Option parsing problem %s" % reason)
        sys.exit(STATE_UNKNOWN)

    if len(args) > 0:
        print("Too many arguments (\"%s\")" % args)
        sys.exit(STATE_UNKNOWN)
    if opts.host is None or name is None:
        print("Host (-H) and name to lookup (-n) are necessary")
        sys.exit(STATE_UNKNOWN)
    if opts.post and opts.head:
        print("POST or HEAD but not both")
        sys.exit(STATE_UNKNOWN)
    if opts.dot and (opts.post or opts.head):
        print("POST or HEAD makes no sense for DoT")
        sys.exit(STATE_UNKNOWN)
    if opts.dot and opts.path:
        print("URL path makes no sense for DoT")
        sys.exit(STATE_UNKNOWN)
    if opts.dot:
        url = opts.host
    else:
        if opts.vhostname is None or opts.vhostname == opts.host:
            opts.connectTo = None
            url = "https://%s/" % opts.host
        else:
            opts.connectTo = opts.host
            url = "https://%s/" % opts.vhostname
        if opts.path is not None:
            if opts.path.startswith("/"):
                opts.path = opts.path[1:]
            url += opts.path

    return (url, name)

def run_default(name, connection, opts):
    request = homer.create_request(name, qtype=opts.rtype, use_edns=opts.edns,
                 want_dnssec=opts.dnssec, no_ecs=opts.no_ecs, dot=opts.dot)
    if not opts.dot:
        request.head = opts.head
        request.post = opts.post
    try:
        connection.do_test(request)
    except (OpenSSL.SSL.Error, homer.DOHException) as e:
        error(e)
        return False
    return print_result(connection, request)

# Main program
194
195
if __name__ == '__main__':
    me = os.path.basename(sys.argv[0])
196

197
    url, name = parse_opts_monitoring(me, opts)
198

199
200
201
202
203
    # The provided host is indeed a valid IP
    # TODO catch ValueError exception if the host is an url as in :
    # ./check_doh -H https://doh.bortzmeyer.fr -n afnic.fr
    if homer.is_valid_ip_address(opts.host)[0]:
        opts.connectTo = opts.host
204

205
206
207
    ok = True
    if opts.dot and opts.vhostname is not None:
        extracheck = opts.vhostname
208
    else:
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
        extracheck = None
    try:
        if opts.dot:
            conn = homer.ConnectionDOT(url, servername=extracheck, connect_to=opts.connectTo,
                                 forceIPv4=opts.forceIPv4, forceIPv6=opts.forceIPv6,
                                 insecure=opts.insecure,
                                 sni=opts.sni, key=opts.key)
        else:
            conn = homer.ConnectionDOH(url, servername=extracheck, connect_to=opts.connectTo,
                                 forceIPv4=opts.forceIPv4, forceIPv6=opts.forceIPv6,
                                 insecure=opts.insecure)
    except TimeoutError:
        error("timeout")
    except ConnectionRefusedError:
        error("Connection to server refused")
    except ValueError:
        error("\"%s\" not a name or an IP address" % url)
    except socket.gaierror:
        error("Could not resolve \"%s\"" % url)
    except (homer.ConnectionException, homer.DOHException) as e:
        error(e)
    if conn.dot and not conn.success:
        ok = False
    else:
        ok = run_default(name, conn, opts)

    conn.end()

    if ok:
        sys.exit(STATE_OK)
    else:
        sys.exit(STATE_CRITICAL)