1 # Copyright (C) Internet Systems Consortium, Inc. ("ISC") 2 # 3 # SPDX-License-Identifier: MPL-2.0 4 # 5 # This Source Code Form is subject to the terms of the Mozilla Public 6 # License, v. 2.0. If a copy of the MPL was not distributed with this 7 # file, you can obtain one at https://mozilla.org/MPL/2.0/. 8 # 9 # See the COPYRIGHT file distributed with this work for additional 10 # information regarding copyright ownership. 11 12 from collections.abc import AsyncGenerator 13 14 import dns.edns 15 import dns.name 16 import dns.rcode 17 import dns.rdatatype 18 import dns.rrset 19 import dns.tsigkeyring 20 21 from isctest.asyncserver import ( 22 AsyncDnsServer, 23 DnsProtocol, 24 DnsResponseSend, 25 QueryContext, 26 ResponseHandler, 27 ) 28 from isctest.name import prepend_label 29 from isctest.vars.algorithms import ALG_VARS 30 31 KEYRING = dns.tsigkeyring.from_text( 32 { 33 "foo": (ALG_VARS["DEFAULT_HMAC"], "aaaaaaaaaaaa"), 34 "fake": (ALG_VARS["DEFAULT_HMAC"], "aaaaaaaaaaaa"), 35 } 36 ) 37 38 39 def _first_label(qctx: QueryContext) -> str: 40 return qctx.qname.labels[0].decode("ascii") 41 42 43 def _add_cookie(qctx: QueryContext) -> None: 44 for o in qctx.query.options: 45 if o.otype == dns.edns.OptionType.COOKIE: 46 cookie = o 47 if len(cookie.server) == 0: 48 cookie.server = cookie.client 49 50 qctx.response.use_edns(options=[cookie]) 51 return 52 53 54 def _tld(qctx: QueryContext) -> dns.name.Name: 55 return dns.name.Name(qctx.qname.labels[-2:]) 56 57 58 def _soa(qctx: QueryContext) -> dns.rrset.RRset: 59 return dns.rrset.from_text( 60 _tld(qctx), 2, qctx.qclass, dns.rdatatype.SOA, ". . 0 0 0 0 2" 61 ) 62 63 64 def _ns_name(qctx: QueryContext) -> dns.name.Name: 65 return prepend_label("ns", _tld(qctx)) 66 67 68 def _ns(qctx: QueryContext) -> dns.rrset.RRset: 69 return dns.rrset.from_text( 70 qctx.qname, 71 1, 72 qctx.qclass, 73 dns.rdatatype.NS, 74 _ns_name(qctx).to_text(), 75 ) 76 77 78 def _legit_a(qctx: QueryContext) -> dns.rrset.RRset: 79 return dns.rrset.from_text(qctx.qname, 1, qctx.qclass, dns.rdatatype.A, "10.53.0.9") 80 81 82 def _spoofed_a(qctx: QueryContext) -> dns.rrset.RRset: 83 return dns.rrset.from_text( 84 qctx.qname, 1, qctx.qclass, dns.rdatatype.A, "10.53.0.10" 85 ) 86 87 88 class _SpoofableHandler(ResponseHandler): 89 def __init__(self, evil_server: bool) -> None: 90 self.evil_server = evil_server 91 92 93 class NsHandler(_SpoofableHandler): 94 def match(self, qctx: QueryContext) -> bool: 95 return qctx.qtype == dns.rdatatype.NS and qctx.qname == _tld(qctx) 96 97 async def get_responses( 98 self, qctx: QueryContext 99 ) -> AsyncGenerator[DnsResponseSend, None]: 100 _add_cookie(qctx) 101 qctx.response.answer.append(_ns(qctx)) 102 if self.evil_server: 103 qctx.response.authority.append(_spoofed_a(qctx)) 104 else: 105 qctx.response.authority.append(_legit_a(qctx)) 106 yield DnsResponseSend(qctx.response) 107 108 109 class GlueHandler(_SpoofableHandler): 110 def match(self, qctx: QueryContext) -> bool: 111 return qctx.qtype == dns.rdatatype.A and qctx.qname == _ns_name(qctx) 112 113 async def get_responses( 114 self, qctx: QueryContext 115 ) -> AsyncGenerator[DnsResponseSend, None]: 116 _add_cookie(qctx) 117 if self.evil_server: 118 qctx.response.answer.append(_spoofed_a(qctx)) 119 else: 120 qctx.response.answer.append(_legit_a(qctx)) 121 yield DnsResponseSend(qctx.response) 122 123 124 class TcpAHandler(ResponseHandler): 125 def match(self, qctx: QueryContext) -> bool: 126 return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.TCP 127 128 async def get_responses( 129 self, qctx: QueryContext 130 ) -> AsyncGenerator[DnsResponseSend, None]: 131 if _first_label(qctx) != "nocookie": 132 _add_cookie(qctx) 133 qctx.response.answer.append(_legit_a(qctx)) 134 yield DnsResponseSend(qctx.response) 135 136 137 class WithtsigUdpAHandler(ResponseHandler): 138 def match(self, qctx: QueryContext) -> bool: 139 return ( 140 qctx.qtype == dns.rdatatype.A 141 and qctx.protocol == DnsProtocol.UDP 142 and _first_label(qctx) == "withtsig" 143 ) 144 145 async def get_responses( 146 self, qctx: QueryContext 147 ) -> AsyncGenerator[DnsResponseSend, None]: 148 qctx.response.answer.append(_legit_a(qctx)) 149 qctx.response.answer.append(_spoofed_a(qctx)) 150 qctx.response.use_tsig(keyring=KEYRING, keyname="fake") 151 yield DnsResponseSend(qctx.response) 152 153 qctx.prepare_new_response() 154 _add_cookie(qctx) 155 qctx.response.answer.append(_legit_a(qctx)) 156 yield DnsResponseSend(qctx.response) 157 158 159 class UdpAHandler(ResponseHandler): 160 def match(self, qctx: QueryContext) -> bool: 161 return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.UDP 162 163 async def get_responses( 164 self, qctx: QueryContext 165 ) -> AsyncGenerator[DnsResponseSend, None]: 166 qctx.response.answer.append(_legit_a(qctx)) 167 if _first_label(qctx) not in ("nocookie", "tcponly"): 168 _add_cookie(qctx) 169 else: 170 qctx.response.answer.append(_spoofed_a(qctx)) 171 172 yield DnsResponseSend(qctx.response) 173 174 175 class FallbackHandler(ResponseHandler): 176 async def get_responses( 177 self, qctx: QueryContext 178 ) -> AsyncGenerator[DnsResponseSend, None]: 179 _add_cookie(qctx) 180 if qctx.qtype == dns.rdatatype.SOA: 181 qctx.response.answer.append(_soa(qctx)) 182 else: 183 qctx.response.authority.append(_soa(qctx)) 184 yield DnsResponseSend(qctx.response) 185 186 187 def cookie_server(evil: bool) -> AsyncDnsServer: 188 server = AsyncDnsServer( 189 keyring=KEYRING, default_aa=True, default_rcode=dns.rcode.NOERROR 190 ) 191 server.install_response_handlers( 192 NsHandler(evil), 193 GlueHandler(evil), 194 TcpAHandler(), 195 WithtsigUdpAHandler(), 196 UdpAHandler(), 197 ) 198 server.install_response_handler(FallbackHandler()) 199 return server 200