1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Philippe Biondi <phil@secdev.org>
5
6"""
7LLMNR (Link Local Multicast Node Resolution).
8
9[RFC 4795]
10
11LLMNR is based on the DNS packet format (RFC1035 Section 4)
12RFC also envisions LLMNR over TCP. Like vista, we don't support it -- arno
13"""
14
15import struct
16
17from scapy.fields import (
18 BitEnumField,
19 BitField,
20 DestField,
21 DestIP6Field,
22 ShortField,
23)
24from scapy.packet import Packet, bind_layers, bind_bottom_up
25from scapy.compat import orb
26from scapy.layers.inet import UDP
27from scapy.layers.dns import (
28 DNSCompressedPacket,
29 DNS_am,
30 DNS,
31 DNSQR,
32 DNSRR,
33)
34
35
36_LLMNR_IPv6_mcast_Addr = "FF02:0:0:0:0:0:1:3"
37_LLMNR_IPv4_mcast_addr = "224.0.0.252"
38
39
40class LLMNRQuery(DNSCompressedPacket):
41 name = "Link Local Multicast Node Resolution - Query"
42 qd = []
43 fields_desc = [
44 ShortField("id", 0),
45 BitField("qr", 0, 1),
46 BitEnumField("opcode", 0, 4, {0: "QUERY"}),
47 BitField("c", 0, 1),
48 BitField("tc", 0, 1),
49 BitField("t", 0, 1),
50 BitField("z", 0, 4)
51 ] + DNS.fields_desc[-9:]
52 overload_fields = {UDP: {"sport": 5355, "dport": 5355}}
53
54 def get_full(self):
55 # Required for DNSCompressedPacket
56 return self.original
57
58 def hashret(self):
59 return struct.pack("!H", self.id)
60
61 def mysummary(self):
62 s = self.__class__.__name__
63 if self.qr:
64 if self.an and isinstance(self.an[0], DNSRR):
65 s += " '%s' is at '%s'" % (
66 self.an[0].rrname.decode(errors="backslashreplace"),
67 self.an[0].rdata,
68 )
69 else:
70 s += " [malformed]"
71 elif self.qd and isinstance(self.qd[0], DNSQR):
72 s += " who has '%s'" % (
73 self.qd[0].qname.decode(errors="backslashreplace"),
74 )
75 else:
76 s += " [malformed]"
77 return s, [UDP]
78
79
80class LLMNRResponse(LLMNRQuery):
81 name = "Link Local Multicast Node Resolution - Response"
82 qr = 1
83
84 def answers(self, other):
85 return (isinstance(other, LLMNRQuery) and
86 self.id == other.id and
87 self.qr == 1 and
88 other.qr == 0)
89
90
91class _LLMNR(Packet):
92 @classmethod
93 def dispatch_hook(cls, _pkt=None, *args, **kargs):
94 if len(_pkt) >= 2:
95 if (orb(_pkt[2]) & 0x80): # Response
96 return LLMNRResponse
97 else: # Query
98 return LLMNRQuery
99 return cls
100
101
102bind_bottom_up(UDP, _LLMNR, dport=5355)
103bind_bottom_up(UDP, _LLMNR, sport=5355)
104bind_layers(UDP, _LLMNR, sport=5355, dport=5355)
105
106DestField.bind_addr(LLMNRQuery, _LLMNR_IPv4_mcast_addr, dport=5355)
107DestField.bind_addr(LLMNRResponse, _LLMNR_IPv4_mcast_addr, dport=5355)
108DestIP6Field.bind_addr(LLMNRQuery, _LLMNR_IPv6_mcast_Addr, dport=5355)
109DestIP6Field.bind_addr(LLMNRResponse, _LLMNR_IPv6_mcast_Addr, dport=5355)
110
111
112class LLMNR_am(DNS_am):
113 """
114 LLMNR answering machine.
115
116 This has the same arguments as DNS_am. See help(DNS_am)
117
118 Example::
119
120 >>> llmnrd(joker="192.168.0.2", iface="eth0")
121 >>> llmnrd(match={"TEST": "192.168.0.2"})
122 """
123 function_name = "llmnrd"
124 filter = "udp port 5355"
125 cls = LLMNRQuery
126
127
128# LLMNRQuery(id=RandShort(), qd=DNSQR(qname="vista.")))