awlane | 1cec233 | 2025-04-24 17:24:47 -0500 | [diff] [blame^] | 1 | import ipaddress |
| 2 | from pathlib import Path |
| 3 | from threading import Thread |
| 4 | |
| 5 | import msgpack |
| 6 | |
| 7 | from mininet.net import Mininet |
| 8 | from mininet.log import error, info |
| 9 | from minindn.minindn_play.socket import PlaySocket |
| 10 | from minindn.minindn_play.consts import Config, WSFunctions, WSKeys |
| 11 | from minindn.util import is_valid_hostid, run_popen, run_popen_readline |
| 12 | |
| 13 | # TShark fields |
| 14 | SHARK_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 | ] |
| 26 | SHARK_FIELDS_STR = " -Tfields -e " + " -e ".join(SHARK_FIELDS) + " -Y ndn.len" |
| 27 | |
| 28 | class 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}") |