1#!/usr/bin/python3
2
3# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
4#
5# SPDX-License-Identifier: MPL-2.0
6#
7# This Source Code Form is subject to the terms of the Mozilla Public
8# License, v. 2.0.  If a copy of the MPL was not distributed with this
9# file, you can obtain one at https://mozilla.org/MPL/2.0/.
10#
11# See the COPYRIGHT file distributed with this work for additional
12# information regarding copyright ownership.
13
14# pylint: disable=unused-variable
15
16import socket
17import time
18
19import pytest
20
21pytest.importorskip("dns", minversion="2.0.0")
22import dns.edns
23import dns.message
24import dns.name
25import dns.query
26import dns.rdataclass
27import dns.rdatatype
28
29import pytest_custom_markers  # pylint: disable=import-error
30
31
32TIMEOUT = 10
33
34
35def create_msg(qname, qtype):
36    msg = dns.message.make_query(
37        qname, qtype, want_dnssec=True, use_edns=0, payload=4096
38    )
39    return msg
40
41
42def timeout():
43    return time.time() + TIMEOUT
44
45
46def test_initial_timeout(named_port):
47    #
48    # The initial timeout is 2.5 seconds, so this should timeout
49    #
50    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
51        sock.connect(("10.53.0.1", named_port))
52
53        time.sleep(3)
54
55        msg = create_msg("example.", "A")
56
57        with pytest.raises(EOFError):
58            try:
59                (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
60                (response, rtime) = dns.query.receive_tcp(sock, timeout())
61            except ConnectionError as e:
62                raise EOFError from e
63
64
65def test_idle_timeout(named_port):
66    #
67    # The idle timeout is 5 seconds, so the third message should fail
68    #
69    msg = create_msg("example.", "A")
70    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
71        sock.connect(("10.53.0.1", named_port))
72
73        time.sleep(1)
74
75        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
76        (response, rtime) = dns.query.receive_tcp(sock, timeout())
77
78        time.sleep(2)
79
80        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
81        (response, rtime) = dns.query.receive_tcp(sock, timeout())
82
83        time.sleep(6)
84
85        with pytest.raises(EOFError):
86            try:
87                (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
88                (response, rtime) = dns.query.receive_tcp(sock, timeout())
89            except ConnectionError as e:
90                raise EOFError from e
91
92
93def test_keepalive_timeout(named_port):
94    #
95    # Keepalive is 7 seconds, so the third message should succeed.
96    #
97    msg = create_msg("example.", "A")
98    kopt = dns.edns.GenericOption(11, b"\x00")
99    msg.use_edns(edns=True, payload=4096, options=[kopt])
100
101    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
102        sock.connect(("10.53.0.1", named_port))
103
104        time.sleep(1)
105
106        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
107        (response, rtime) = dns.query.receive_tcp(sock, timeout())
108
109        time.sleep(2)
110
111        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
112        (response, rtime) = dns.query.receive_tcp(sock, timeout())
113
114        time.sleep(6)
115
116        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
117        (response, rtime) = dns.query.receive_tcp(sock, timeout())
118
119
120def test_pipelining_timeout(named_port):
121    #
122    # The pipelining should only timeout after the last message is received
123    #
124    msg = create_msg("example.", "A")
125    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
126        sock.connect(("10.53.0.1", named_port))
127
128        time.sleep(1)
129
130        # Send and receive 25 DNS queries
131        for n in range(25):
132            (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
133        for n in range(25):
134            (response, rtime) = dns.query.receive_tcp(sock, timeout())
135
136        time.sleep(3)
137
138        # Send and receive 25 DNS queries
139        for n in range(25):
140            (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
141        for n in range(25):
142            (response, rtime) = dns.query.receive_tcp(sock, timeout())
143
144        time.sleep(6)
145
146        with pytest.raises(EOFError):
147            try:
148                (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
149                (response, rtime) = dns.query.receive_tcp(sock, timeout())
150            except ConnectionError as e:
151                raise EOFError from e
152
153
154def test_long_axfr(named_port):
155    #
156    # The timers should not fire during AXFR, thus the connection should not
157    # close abruptly
158    #
159    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
160        sock.connect(("10.53.0.1", named_port))
161
162        name = dns.name.from_text("example.")
163        msg = create_msg("example.", "AXFR")
164        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
165
166        # Receive the initial DNS message with SOA
167        (response, rtime) = dns.query.receive_tcp(
168            sock, timeout(), one_rr_per_rrset=True
169        )
170        soa = response.get_rrset(
171            dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
172        )
173        assert soa is not None
174
175        # Pull DNS message from wire until the second SOA is received
176        while True:
177            (response, rtime) = dns.query.receive_tcp(
178                sock, timeout(), one_rr_per_rrset=True
179            )
180            soa = response.get_rrset(
181                dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
182            )
183            if soa is not None:
184                break
185        assert soa is not None
186
187
188@pytest_custom_markers.flaky(max_runs=3)
189def test_send_timeout(named_port):
190    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
191        sock.connect(("10.53.0.1", named_port))
192
193        # Send and receive single large RDATA over TCP
194        msg = create_msg("large.example.", "TXT")
195        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
196        (response, rtime) = dns.query.receive_tcp(sock, timeout())
197
198        # Send and receive 28 large (~32k) DNS queries that should
199        # fill the default maximum 208k TCP send buffer
200        for n in range(28):
201            (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
202
203        # configure idle interval is 5 seconds, sleep 6 to make sure we are
204        # above the interval
205        time.sleep(6)
206
207        with pytest.raises(EOFError):
208            try:
209                (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
210                (response, rtime) = dns.query.receive_tcp(sock, timeout())
211            except ConnectionError as e:
212                raise EOFError from e
213
214
215@pytest_custom_markers.long_test
216def test_max_transfer_idle_out(named_port):
217    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
218        sock.connect(("10.53.0.1", named_port))
219
220        name = dns.name.from_text("example.")
221        msg = create_msg("example.", "AXFR")
222        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
223
224        # Receive the initial DNS message with SOA
225        (response, rtime) = dns.query.receive_tcp(
226            sock, timeout(), one_rr_per_rrset=True
227        )
228        soa = response.get_rrset(
229            dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
230        )
231        assert soa is not None
232
233        time.sleep(61)  # max-transfer-idle-out is 1 minute
234
235        with pytest.raises(ConnectionResetError):
236            # Process queued TCP messages
237            while True:
238                (response, rtime) = dns.query.receive_tcp(
239                    sock, timeout(), one_rr_per_rrset=True
240                )
241                soa = response.get_rrset(
242                    dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
243                )
244                if soa is not None:
245                    break
246            assert soa is None
247
248
249@pytest_custom_markers.long_test
250def test_max_transfer_time_out(named_port):
251    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
252        sock.connect(("10.53.0.1", named_port))
253
254        name = dns.name.from_text("example.")
255        msg = create_msg("example.", "AXFR")
256        (sbytes, stime) = dns.query.send_tcp(sock, msg, timeout())
257
258        # Receive the initial DNS message with SOA
259        (response, rtime) = dns.query.receive_tcp(
260            sock, timeout(), one_rr_per_rrset=True
261        )
262        soa = response.get_rrset(
263            dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
264        )
265        assert soa is not None
266
267        # The loop should timeout at the 5 minutes (max-transfer-time-out)
268        with pytest.raises(EOFError):
269            while True:
270                time.sleep(1)
271                (response, rtime) = dns.query.receive_tcp(
272                    sock, timeout(), one_rr_per_rrset=True
273                )
274                soa = response.get_rrset(
275                    dns.message.ANSWER, name, dns.rdataclass.IN, dns.rdatatype.SOA
276                )
277                if soa is not None:
278                    break
279        assert soa is None
280