blob: 3db0170c8b326be1a7230bb9083d18fd604e1c3f [file] [log] [blame]
awlane1cec2332025-04-24 17:24:47 -05001import ipaddress
2from pathlib import Path
3from threading import Thread
4
5import msgpack
6
7from mininet.net import Mininet
8from mininet.log import error, info
9from minindn.minindn_play.socket import PlaySocket
10from minindn.minindn_play.consts import Config, WSFunctions, WSKeys
11from minindn.util import is_valid_hostid, run_popen, run_popen_readline
12
13# TShark fields
14SHARK_FIELDS = [
15 "frame.number",
16 "frame.time_epoch",
17 "ndn.len",
18 "ndn.type",
19 "ndn.name",
20 "ip.src",
21 "ip.dst",
22 "ipv6.src",
23 "ipv6.dst",
24 # "ndn.bin", # binary data
25]
26SHARK_FIELDS_STR = " -Tfields -e " + " -e ".join(SHARK_FIELDS) + " -Y ndn.len"
27
28class SharkExecutor:
29 _ip_map: dict = None
30 _lua_script: str = None
31
32 def __init__(self, net: Mininet, socket: PlaySocket):
33 self.net = net
34 self.socket = socket
35
36 def _get_pcap_file(self, name):
37 return f"./{name}-interfaces.pcap"
38
39 def _get_lua(self):
40 if self._lua_script is not None:
41 return self._lua_script
42
43 lua_path = Path(__file__).parent.parent.absolute() / "ndn.lua"
44 if lua_path.exists():
45 luafile = str(lua_path)
46 self._lua_script = 'lua_script:' + luafile
47 return self._lua_script
48
49 luafile = '/usr/local/share/ndn-dissect-wireshark/ndn.lua'
50 if Path(luafile).exists():
51 self._lua_script = 'lua_script:' + luafile
52 return self._lua_script
53
54 raise RuntimeError('NDN Wireshark dissector not found (ndn-tools/ndn.lua)')
55
56 def _convert_to_full_ip_address(self, ip_address: str):
57 try:
58 ip_obj = ipaddress.ip_address(ip_address)
59 except ValueError:
60 return ip_address
61
62 if isinstance(ip_obj, ipaddress.IPv6Address):
63 return str(ip_obj)
64 else:
65 return ip_address
66
67 def _get_hostname_from_ip(self, ip):
68 #TODO: This is probably overcomplicated given the Mininet API
69 """
70 Get the hostname of a node given its IP address.
71 node: the node to check on (e.g. for local addresses)
72 This function runs once and caches the result, since we need to visit
73 each node to get its list of IP addresses.
74 """
75 if self._ip_map is None:
76 # Map of IP address to hostname
77 self._ip_map = {}
78
79 # Extract all addresses including localhost
80 cmd = "ip addr show | grep -E 'inet' | awk '{print $2}' | cut -d '/' -f1"
81
82 hosts = self.net.hosts
83 hosts += getattr(self.net, "stations", []) # mininet-wifi
84 for host in hosts:
85 for ip in host.cmd(cmd).splitlines():
86 if full_ip := self._convert_to_full_ip_address(ip):
87 self._ip_map[full_ip] = host.name
88 info(f"Created IP map for PCAP (will be cached): {self._ip_map}\n")
89
90 if full_ip := self._convert_to_full_ip_address(ip):
91 return self._ip_map.get(full_ip, ip)
92 return ip
93
94 def _send_pcap_chunks(self, node_id: str, known_frame: int, include_wire: bool):
95 """
96 Get, process and send chunks of pcap to UI
97 Blocking; should run in its own thread.
98 """
99
100 node = self.net[node_id]
101 file = self._get_pcap_file(node_id)
102
103 # We don't want to load and process the entire pcap file
104 # every time the user wants to recheck. Instead, use editcap
105 # to cut the part the user knows
106
107 # Look back by upto 12 frames in case the last packet was fragmented
108 known_frame = max(1, known_frame - 12)
109
110 # Get everything after known frame
111 editcap_cmd = f"editcap -r {file} /dev/stdout {known_frame}-0"
112
113 # Shark using NDN dissector
114 extra_fields = "-e ndn.bin " if include_wire else ""
115 list_cmd = f"tshark {SHARK_FIELDS_STR} {extra_fields} -r /dev/stdin -X '{self._get_lua()}'"
116
117 # Pipe editcap to tshark
118 piped_cmd = ["bash", "-c", f"{editcap_cmd} | {list_cmd}"]
119
120 # Collected packets (one chunk)
121 packets = []
122
123 def _send_packets(last=False):
124 """Send the current chunk to the UI (including empty)"""
125 res = {
126 "id": node_id,
127 "packets": packets,
128 }
129 if last:
130 res["last"] = True
131
132 self.socket.send_all(msgpack.dumps({
133 WSKeys.MSG_KEY_FUN: WSFunctions.GET_PCAP,
134 WSKeys.MSG_KEY_RESULT: res,
135 }))
136
137 # Iterate each line of output
138 for line in run_popen_readline(node, piped_cmd):
139 parts: list[str] = line.decode("utf-8").strip("\n").split("\t")
140
141 if len(parts) < 8:
142 error(f"Invalid line in pcap: {parts}\n")
143 continue
144
145 is_ipv6 = parts[7] != "" and parts[8] != ""
146 from_ip = parts[7] if is_ipv6 else parts[5]
147 to_ip = parts[8] if is_ipv6 else parts[6]
148
149 packets.append([
150 int(parts[0]) + known_frame - 1, # frame number
151 float(parts[1]) * 1000, # timestamp
152 int(parts[2]), # length
153 str(parts[3]), # type
154 str(parts[4]), # NDN name
155 str(self._get_hostname_from_ip(from_ip)), # from
156 str(self._get_hostname_from_ip(to_ip)), # to
157 bytes.fromhex(parts[9]) if include_wire else 0, # packet content
158 ])
159
160 if len(packets) >= Config.PCAP_CHUNK_SIZE:
161 _send_packets()
162 packets = []
163
164 # Send the last chunk
165 _send_packets(last=True)
166
167 async def get_pcap(self, node_id: str, known_frame: int, include_wire=False):
168 """UI Function: Get list of packets for one node"""
169 if not is_valid_hostid(self.net, node_id):
170 return
171
172 # Run processing in separate thread
173 t = Thread(target=self._send_pcap_chunks, args=(node_id, known_frame, include_wire), daemon=True)
174 t.start()
175
176 async def get_pcap_wire(self, node_id, frame):
177 """UI Function: Get wire of one packet"""
178 if not is_valid_hostid(self.net, node_id):
179 return
180 file = self._get_pcap_file(node_id)
181
182 # chop the file to the frame
183 # include the last 12 frames in case of fragmentation
184 start_frame = max(1, frame - 12)
185 new_frame = frame - start_frame + 1
186
187 try:
188 # Get last 12 frames
189 editcap_cmd = f"editcap -r {file} /dev/stdout {start_frame}-{frame}"
190
191 # Filter for this packet only
192 wire_cmd = f"tshark -r - -e ndn.bin -Tfields -X {self._get_lua()} frame.number == {new_frame}"
193
194 # Pipe editcap to tshark
195 piped_cmd = ["bash", "-c", f"{editcap_cmd} | {wire_cmd}"]
196 hex_output = run_popen(self.net[node_id], piped_cmd).decode("utf-8").strip()
197 return bytes.fromhex(hex_output)
198 except Exception:
199 error(f"Error getting pcap wire for {node_id}")