Skip to content

Commit

Permalink
Add mDNS daemon (#4385)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpotter2 committed May 13, 2024
1 parent f17e8da commit b44f9a2
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 12 deletions.
9 changes: 9 additions & 0 deletions doc/scapy/usage.rst
Expand Up @@ -1449,6 +1449,15 @@ By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers

You can also use ``relay=True`` to replace the joker behavior with a forward to a server included in ``conf.nameservers``.

mDNS server
------------

See :class:`~scapy.layers.dns.mDNS_am`::

>>> mdnsd(iface="eth0", joker="192.168.1.1")

Note that ``mdnsd`` extends the ``dnsd`` API.

LLMNR server
------------

Expand Down
74 changes: 62 additions & 12 deletions scapy/layers/dns.py
Expand Up @@ -1246,7 +1246,13 @@ def mysummary(self):
type = "Qry"
if self.qd and isinstance(self.qd[0], DNSQR):
name = ' %s' % self.qd[0].qname
return 'DNS %s%s' % (type, name)
return "%sDNS %s%s" % (
"m"
if isinstance(self.underlayer, UDP) and self.underlayer.dport == 5353
else "",
type,
name,
)

def post_build(self, pkt, pay):
if isinstance(self.underlayer, TCP) and self.length is None:
Expand Down Expand Up @@ -1418,12 +1424,13 @@ def dyndns_del(nameserver, name, type="ALL", ttl=10):
class DNS_am(AnsweringMachine):
function_name = "dnsd"
filter = "udp port 53"
cls = DNS # We also use this automaton for llmnrd
cls = DNS # We also use this automaton for llmnrd / mdnsd

def parse_options(self, joker=None,
match=None,
srvmatch=None,
joker6=False,
send_error=False,
relay=False,
from_ip=None,
from_ip6=None,
Expand All @@ -1438,6 +1445,8 @@ def parse_options(self, joker=None,
set to False to disable, None to mirror the interface's IPv6.
:param jokerarpa: answer for .in-addr.arpa PTR requests. (Default: None)
:param relay: relay unresolved domains to conf.nameservers (Default: False).
:param send_error: send an error message when this server can't answer
(Default: False)
:param match: a dictionary of {name: val} where name is a string representing
a domain name (A, AAAA) and val is a tuple of 2 elements, each
representing an IP or a list of IPs. If val is a single element,
Expand All @@ -1449,7 +1458,7 @@ def parse_options(self, joker=None,
:param src_ip: override the source IP
:param src_ip6:
Example:
Example::
$ sudo iptables -I OUTPUT -p icmp --icmp-type 3/3 -j DROP
>>> dnsd(match={"google.com": "1.1.1.1"}, joker="192.168.0.2", iface="eth0")
Expand Down Expand Up @@ -1481,6 +1490,7 @@ def normk(k):
self.joker = joker
self.joker6 = joker6
self.jokerarpa = jokerarpa
self.send_error = send_error
self.relay = relay
if isinstance(from_ip, str):
self.from_ip = Net(from_ip)
Expand Down Expand Up @@ -1510,19 +1520,37 @@ def is_request(self, req):
)

def make_reply(self, req):
mDNS = isinstance(self, mDNS_am)
llmnr = self.cls != DNS
# Build reply from the request
resp = req.copy()
if Ether in req:
resp[Ether].src, resp[Ether].dst = (
None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst,
req[Ether].src,
)
if mDNS:
resp[Ether].src, resp[Ether].dst = None, None
elif llmnr:
resp[Ether].src, resp[Ether].dst = None, req[Ether].src
else:
resp[Ether].src, resp[Ether].dst = (
None if req[Ether].dst in "ff:ff:ff:ff:ff:ff" else req[Ether].dst,
req[Ether].src,
)
from scapy.layers.inet6 import IPv6
if IPv6 in req:
resp[IPv6].underlayer.remove_payload()
resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst)
if mDNS:
resp /= IPv6(dst="ff02::fb", src=self.src_ip6)
elif llmnr:
resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6)
else:
resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst)
elif IP in req:
resp[IP].underlayer.remove_payload()
resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst)
if mDNS:
resp /= IP(dst="224.0.0.251", src=self.src_ip)
elif llmnr:
resp /= IP(dst=req[IP].src, src=self.src_ip)
else:
resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst)
else:
warning("No IP or IPv6 layer in %s", req.command())
return
Expand All @@ -1531,6 +1559,7 @@ def make_reply(self, req):
except IndexError:
warning("No UDP layer in %s", req.command(), exc_info=True)
return
# Now process each query and store its answer in 'ans'
ans = []
try:
req = req[self.cls]
Expand All @@ -1548,6 +1577,7 @@ def make_reply(self, req):
warning("No qd attribute in %s", req.command(), exc_info=True)
return
for rq in queries:
# For each query
if isinstance(rq, Raw):
warning("Cannot parse qd element %s", rq.command(), exc_info=True)
continue
Expand Down Expand Up @@ -1626,8 +1656,28 @@ def make_reply(self, req):
# No rq was actually answered, as none was valid. Discard.
return
# All rq were answered
resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
if mDNS:
# in mDNS mode, don't repeat the question
resp /= self.cls(id=req.id, qr=1, qd=[], an=ans)
else:
resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
return resp
# An error happened
resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3)
return resp
if self.send_error:
resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3)
return resp


class mDNS_am(DNS_am):
"""
mDNS answering machine.
This has the same arguments as DNS_am. See help(DNS_am)
Example::
>>> mdnsd(joker="192.168.0.2", iface="eth0")
>>> mdnsd(match={"TEST.local": "192.168.0.2"})
"""
function_name = "mdnsd"
filter = "udp port 5353"
10 changes: 10 additions & 0 deletions scapy/layers/llmnr.py
Expand Up @@ -110,6 +110,16 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs):


class LLMNR_am(DNS_am):
"""
LLMNR answering machine.
This has the same arguments as DNS_am. See help(DNS_am)
Example::
>>> llmnrd(joker="192.168.0.2", iface="eth0")
>>> llmnrd(match={"TEST": "192.168.0.2"})
"""
function_name = "llmnrd"
filter = "udp port 5355"
cls = LLMNRQuery
Expand Down
37 changes: 37 additions & 0 deletions test/answering_machines.uts
Expand Up @@ -126,6 +126,43 @@ assert DNS_am().make_reply(
Ether()/IP()/UDP()/DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y')
) is None

= LLMNR_am
def check_LLMNR_am_am_reply(packet):
assert packet[Ether].src == get_if_hwaddr(conf.iface)
assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa"
assert packet[IP].src == get_if_addr(conf.iface)
assert packet[IP].dst == "192.168.0.1"
assert packet[UDP].dport == 51938
assert packet[UDP].sport == 5355
assert LLMNRResponse in packet and packet[LLMNRResponse].ancount == 1 and packet[LLMNRResponse].qdcount == 1
assert packet[LLMNRResponse].qd[0].qname == b"TEST."
assert packet[LLMNRResponse].an[0].rdata == "192.168.1.1"
assert packet[LLMNRResponse].an[0].rrname == b"TEST."
assert packet[LLMNRResponse].an[0].ttl == 10

test_am(LLMNR_am,
Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fc")/IP(src="192.168.0.1", dst="224.0.0.252")/UDP(dport=5355, sport=51938)/LLMNRQuery(qd=DNSQR(qname=b"TEST.", qtype="A")),
check_LLMNR_am_am_reply,
match={"TEST": "192.168.1.1"})

= mDNS_am
def check_mDNS_am_reply(packet):
assert packet[Ether].src == get_if_hwaddr(conf.iface)
assert packet[Ether].dst == "01:00:5e:00:00:fb"
assert packet[IP].src == get_if_addr(conf.iface)
assert packet[IP].dst == "224.0.0.251"
assert packet[UDP].dport == 5353
assert packet[UDP].sport == 5353
assert DNS in packet and packet[DNS].ancount == 1 and packet[DNS].qdcount == 0
assert packet[DNS].an[0].rdata == "192.168.1.1"
assert packet[DNS].an[0].rrname == b"TEST.local."
assert packet[DNS].an[0].ttl == 10

test_am(mDNS_am,
Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fb")/IP(src="192.168.0.1", dst="224.0.0.251")/UDP(dport=5353, sport=5353)/DNS(qd=DNSQR(qname=b"TEST.local.", qtype="A")),
check_mDNS_am_reply,
joker="192.168.1.1")

= DHCPv6_am - Basic Instantiaion
~ osx netaccess
a = DHCPv6_am()
Expand Down

0 comments on commit b44f9a2

Please sign in to comment.