diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/rip_dissector/README.md | 70 | ||||
-rw-r--r-- | util/rip_dissector/cs168_rip.lua | 115 | ||||
-rwxr-xr-x | util/vnet_generate | 340 | ||||
-rwxr-xr-x | util/vnet_run | 224 |
4 files changed, 749 insertions, 0 deletions
diff --git a/util/rip_dissector/README.md b/util/rip_dissector/README.md new file mode 100644 index 0000000..abfd6a5 --- /dev/null +++ b/util/rip_dissector/README.md @@ -0,0 +1,70 @@ +# CS168 RIP Dissector + +This directory contains a dissector (also known as a decoder) for the +RIP protocol implementation for CS168. + +## Installation Instructions + +The dissector is provided as a Lua script in this directory. For +security reasons, Wireshark does not run Lua scripts when run as +root--therefore, you must ensure that you are using Wireshark as your +local user, not with root or sudo. To run wireshark as a standard +user, make sure your user is added to the `wireshark` group. If you +are using the provided VM, the the vagrant user is already in the +wireshark group. However, if you are running Wireshark on your own +system, you will need to configure this yourself. + +Once you have Wireshark running as your user. Add the dissector to +Wireshark, by copying the script into your plugins directory. + +To do this: + 0. Run wireshark as your user (**not with root or sudo**). + 1. Open Wireshark's Help menu and select "About Wireshark". + 2. In the folders tab, find the entry "Personal Lua Plugins". For + example: `~/.config/wireshark/plugins` + 3. Copy the script to this directory (if it doesn't exist, create it) + and restart wireshark + 4. Open the "About Wireshark" window again and look in the Plugins + tab. You should now see cs168_rip.lua in the list of plugins. + +## Using the dissector + +Note: To make sure your dissector is working, please run the reference IP +node with an example network to ensure you are testing with correct +packets. + +Wireshark will automatically invoke the RIP dissector when it +encounters an IP packet using protocol number 200. + +Four our overlay network, however, Wireshark does not automatically +know to interpret our IP-in-UDP packets as IP packets. You can tell +wireshark to do this using its "User-specified decodes" feature: + 1. Start capturing traffic for the IP assignment. In most cases, + you will be capturing on the loopback interface. + 2. Find a UDP packet related to the assignment and select it. These + packets will use the port numbers specified in the lnx files, and + therefore may be different depending on the network you are running. + 3. Look in the lower pane that shows the layers present in ths + packet. Under UDP, you should see a layer "Data" that contains + our Virtual IP packets. Select this field. + 4. Right-click on the field and select "**Decode As...**" This should + open a window and add a rule template to decode UDP traffic on the port + number used in the packet. In the rightmost column ("Current"), + select the IPv4 decoder, then click "**Save**". + 5. Wireshark should now update and decode the UDP packets first as + IP packets, and then decode those with protocol 200 as RIP using + the dissector you installed. + +If you do not see IP packets encapsulated in your UDP packets, check +your "Decode as... rules from Step 5. If you do not see RIP being +decoded, make sure the plugin is loaded in the help menu. + +**Note**: this will only invoke the correct decoder on a single UDP +port. If you want to decode the traffic for multiple nodes, repeat +this process for each port you need to observe. + +## Feedback + +This decoder and the instructions are new. If you have questions or +encounter any issues, please post on Piazza or see the course staff +for help. diff --git a/util/rip_dissector/cs168_rip.lua b/util/rip_dissector/cs168_rip.lua new file mode 100644 index 0000000..4e6e6d9 --- /dev/null +++ b/util/rip_dissector/cs168_rip.lua @@ -0,0 +1,115 @@ +-- CS168 RIP Protocol Dissector +-- +-- The structure of a RIP message is as follows: +-- uint16_t command +-- uint16_t num_entries +-- struct { +-- uint32_t cost +-- uint32_t address +-- } entries[num_entries] + +local RIP_HEADER_LEN = 4 +local ROUTE_ENTRY_LEN = 12 + +rip_protocol = Proto("CS168RIP", "CS168 RIP") + +command = ProtoField.uint16("cs168rip.command", "command", base.DEC) +num_entries = ProtoField.uint16("cs168rip.num_entries", "num_entries", base.DEC) + +rip_entry_cost = ProtoField.uint32("cs168rip.entry.cost", "cost", base.DEC) +rip_entry_addr = ProtoField.ipv4("cs168rip.entry.address", "address") +rip_entry_mask = ProtoField.ipv4("cs168rip.entry.mask", "mask") + + +rip_protocol.fields = { + command, + num_entries, + rip_entry_cost, + rip_entry_addr, + rip_entry_mask, +} + +local ef_bad_entry = ProtoExpert.new("cs168rip.query.entry.expert", + "Route entry missing or malformed", + expert.group.MALFORMED, + expert.severity.WARN) + +rip_protocol.experts = { + ef_bad_entry, +} + +local field_cost = Field.new("cs168rip.entry.cost") +local field_addr = Field.new("cs168rip.entry.address") +local field_mask = Field.new("cs168rip.entry.mask") + + +function rip_protocol.dissector(buffer, pinfo, tree) + length = buffer:len() + if length == 0 then return end + + pinfo.cols.protocol = rip_protocol.name + + local subtree = tree:add(rip_protocol, buffer(), "CS168 RIP Protocol") + + local pktlen = buffer:reported_length_remaining() + + local cmd_num = buffer(0, 2):uint() + local cmd_name = get_command_name(cmd_num) + + -- Add command ID and name + subtree:add(command, buffer(0, 2)):append_text(" (" .. cmd_name .. ") ") + pinfo.cols.info:append("RIP " .. cmd_name) + + -- num_entries + local entry_count = buffer(2, 2):uint() + subtree:add(num_entries, buffer(2, 2)) + pinfo.cols.info:append(" (" .. entry_count .. " entries)") + + local pos = RIP_HEADER_LEN + local e_idx = 0 + + -- Parse each entry + if entry_count > 0 then + local entry_tree = subtree:add("Entries") + + local pkt_remaining = pktlen - pos + + while entry_count > 0 and pkt_remaining > 0 do + if pkt_remaining < ROUTE_ENTRY_LEN then + entry_tree:add_proto_expert_info(ef_bad_entry) + return + end + + local cost = buffer(pos, 4):uint() + local address = buffer(pos + 4, 4):uint() + local mask = buffer(pos + 8, 4):uint() + + -- TODO Add each entry to its own subtree (with helpful summary) + local etree = entry_tree + + etree:add(rip_entry_cost, buffer(pos, 4)) + etree:add(rip_entry_addr, buffer(pos + 4, 4)) + etree:add(rip_entry_mask, buffer(pos + 8, 4)) + + pos = pos + 12 + + pkt_remaining = pkt_remaining - 12 + entry_count = entry_count - 1 + e_idx = e_idx + 1 + end + end + +end + +function get_command_name(cmd) + local name = "UNKNOWN" + if cmd == 1 then name = "REQUEST" + elseif cmd == 2 then name = "RESPONSE" + end + + return name +end + + +local ip_proto = DissectorTable.get("ip.proto") +ip_proto:add(200, rip_protocol.dissector) diff --git a/util/vnet_generate b/util/vnet_generate new file mode 100755 index 0000000..4f3e467 --- /dev/null +++ b/util/vnet_generate @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import pathlib +import argparse +import subprocess + +import ipaddress +from ipaddress import IPv4Address, IPv4Network + +import dataclasses +from dataclasses import dataclass + +UDP_PORT_START = 5000 +UDP_PORT_ALLOC_INC = 100 + +NODE_TYPE_HOST = "host" +NODE_TYPE_ROUTER = "router" +NETWORK_PREFIX_FMT = "10.{}.0.0/24" + +SESSION_PREFIX = "vnet-" +START_SHELL = "/bin/bash" + + +class PortAllocator(): + + UDP_PORT_HEAD = UDP_PORT_START + + def __init__(self): + self.next_port = PortAllocator.UDP_PORT_HEAD + PortAllocator.UDP_PORT_HEAD += UDP_PORT_ALLOC_INC + + def get_next(self): + ret = self.next_port + self.next_port += 1 + + return ret + + +class IPAllocator(): + + IP_PREFIX_HEAD = 0 + + def __init__(self, prefix: IPv4Network): + self.prefix = prefix + self.hosts = self.prefix.hosts() # Generator + + def next_ip(self) -> IPv4Address: + try: + next_addr = next(self.hosts) + return next_addr + except StopIteration: + raise ValueError(f"Out of addresses in prefix {self.prefix}") + + @classmethod + def _next_prefix(cls): + prefix = IPv4Network(NETWORK_PREFIX_FMT.format(cls.IP_PREFIX_HEAD)) + cls.IP_PREFIX_HEAD += 1 + return prefix + + @classmethod + def make_next(cls): + return cls(cls._next_prefix()) + + +@dataclass +class Interface(): + name: str + addr: IPv4Address + network: 'Network' + udp_addr: str = "" + + def prefix(self) -> IPv4Network: + return self.network.alloc.prefix + + def ip_cidr_format(self): + return "{}/{}".format(self.addr, self.prefix().prefixlen) + + +@dataclass +class Node(): + + def __init__(self, name: str, type: str, path: str = ""): + self.name = name + self.node_type = type + self.path = path + + self.interfaces: dict[str, Interface] = {} + self.if_index = 0 + + def _next_ifname(self): + if_name = "if{}".format(self.if_index) + self.if_index += 1 + return if_name + + def add_iface(self, ip: IPv4Address, network: 'Network', udp_addr: str): + if_name = self._next_ifname() + iface = Interface(name=if_name, + addr=ip, + network=network, + udp_addr=udp_addr) + self.interfaces[if_name] = iface + + def is_router(self): + return self.node_type == NODE_TYPE_ROUTER + + def get_ip_on_network(self, target: 'Network') -> tuple[IPv4Address, str]: + for _, iface in self.interfaces.items(): + if iface.network == target: + return iface.addr, iface.udp_addr + + import pdb; pdb.set_trace() + raise ValueError("No matching interface found") + + def get_neighbor_router_ips(self) -> list[IPv4Address]: + ret = [] + + if not self.is_router(): + return ret + + for _, iface in self.interfaces.items(): + for node in iface.network.links: + if node.is_router() and node != self: + neigh_ip, _ = node.get_ip_on_network(iface.network) + ret.append(neigh_ip) + + return ret + + def get_neighboring_routers(self) -> list[tuple['Node', 'Interface']]: + routers = [] + + for _, iface in self.interfaces.items(): + for node in iface.network.links: + if node.is_router() and node != self: + routers.append((node, iface)) + + return routers + + def write_config(self, output_file): + with open(output_file, "w") as fd: + fd.write(f"# Auto-generated configuration for {self.name}\n\n") + + for _, iface in self.interfaces.items(): + this_net = iface.network + fd.write("interface {} {} {} # to network {}\n" + .format(iface.name, + iface.ip_cidr_format(), + iface.udp_addr, + this_net.name)) + for neighbor in this_net.links: + if neighbor.name == self.name: + continue + + neighbor_ip, neighbor_udp_addr = neighbor.get_ip_on_network(this_net) + fd.write("neighbor {} at {} via {} # {}\n" + .format(neighbor_ip, + neighbor_udp_addr, + iface.name, + neighbor.name)) + fd.write("\n") + + fd.write("\n") + if self.node_type == NODE_TYPE_ROUTER: + fd.write("routing rip\n\n") + + prefixes = [i.prefix() for i in self.interfaces.values() \ + if i.network.should_advertise(self)] + #import pdb; pdb.set_trace() + neighbor_router_ips = self.get_neighbor_router_ips() + + if len(prefixes) > 0: + fd.write("# Prefixes this router should advertise\n") + for p in prefixes: + fd.write(f"rip originate prefix {p}\n") + + fd.write("\n") + + if len(neighbor_router_ips) > 0: + fd.write("# Neighbor routers that should be sent RIP messages\n") + for nr in neighbor_router_ips: + fd.write(f"rip advertise-to {nr}\n") + elif self.node_type == NODE_TYPE_HOST: + routers = self.get_neighboring_routers() + if len(routers) == 0: + import pdb; pdb.set_trace() + raise ValueError(f"No neighboring router found for host {self.name}") + elif len(routers) > 1: + print(f"Warning: multiple routers found for host {self.name}, selecting one") + default_router, default_iface = routers[0] + default_ip, _ = default_router.get_ip_on_network(default_iface.network) + fd.write("# Default route\n") + fd.write("route 0.0.0.0/0 via {}\n".format(default_ip)) + else: + import pdb; pdb.set_trace() + raise ValueError("Invalid node type") + + def __eq__(self, other: 'Node'): + return self.name == other.name + + def __str__(self): + return f"Node({self.name})" + + def __repr__(self): + return f"Node({self.name})" + + +@dataclass +class Network(): + name: str + links: list[Node] + advertise_from: list[Node] + alloc: IPAllocator = dataclasses.field(default_factory=IPAllocator.make_next) + + def should_advertise(self, node: Node): + return node in self.advertise_from + + +@dataclass +class NetConfig(): + nodes: list[Node] + networks: list[Network] + + def build(self): + udp_port_alloc = PortAllocator() + + # Assign prefixes for all networks + for net in self.networks: + for node in net.links: + assigned_ip = net.alloc.next_ip() + + udp_port = udp_port_alloc.get_next() + udp_addr = "127.0.0.1:{}".format(udp_port) + + node.add_iface(assigned_ip, net, udp_addr) + + def write_links(self, output_dir): + for node in self.nodes: + target_file = f"{str(output_dir)}/{node.name}.lnx" + print(f"Writing {target_file}") + node.write_config(target_file) + + def write_device_files(self, output_dir): + nodes_file = f"{str(output_dir)}/nodes.json" + binaries_file = f"{str(output_dir)}/binaries.example.json" + + device_types = {n.name: n.node_type for n in self.nodes} + + binary_paths = {} + for node in self.nodes: + binary_paths[node.name] = { + "binary_path": "./host" \ + if node.node_type == NODE_TYPE_HOST else + "./router" if node.node_type == NODE_TYPE_ROUTER + else "<path to binary>" + } + + write_json(device_types, nodes_file) + write_json(binary_paths, binaries_file) + + # Custom parsing because we don't want to require any non-native libraries + @classmethod + def from_json(cls, json_data): + def _get(d, k): + if k not in d: + raise ValueError(f"Missing key {k} in {d}") + return d[k] + + nodes = {_get(d, "name"): Node(**d) for d in _get(json_data, "nodes")} + + def _get_node(name): + if name not in nodes: + raise ValueError(f"No node definition found for {name}") + return nodes[name] + + networks = [] + + for net in _get(json_data, "networks"): + links = [_get_node(n) for n in _get(net, "links")] + advertise_from = [_get_node(n) for n in _get(net, "advertise-routes-from")] \ + if "advertise-routes-from" in json_data else [] + network = Network(name=_get(net, "name"), + links=links, + advertise_from=advertise_from) + networks.append(network) + + return NetConfig(nodes=list(nodes.values()), networks=networks) + + +def load_json(input_file): + with open(input_file, "r") as fd: + json_data = json.load(fd) + return json_data + + +def write_json(d, target_file): + with open(target_file, "w") as fd: + json.dump(d, fd, indent=True, sort_keys=True) + + +def main(input_args): + parser = argparse.ArgumentParser() + # subparsers = parser.add_subparsers(dest="command") + + # commands = [ + # "generate", + # "run", + # ] + + # sp_cmds = {c: subparsers.add_parser(c) for c in commands} + # sp_cmds["generate"].add_argument("net_json_file") + # sp_cmds["generate"].add_argument("output_dir") + # sp_cmds["run"].add_argument("--router", type=str) + # sp_cmds["run"].add_argument("--host", type=str) + # sp_cmds["run"].add_argument("net_file_dir") + + parser.add_argument("net_json_file") + parser.add_argument("output_dir") + + args = parser.parse_args(input_args) + + input_file = args.net_json_file + output_path = pathlib.Path(args.output_dir) + + json_data = load_json(input_file) + nc = NetConfig.from_json(json_data) + + nc.build() + + if not output_path.exists(): + print(f"Output directory {str(output_path)} does not exist, creating") + os.mkdir(output_path) + + nc.write_links(output_path) + nc.write_device_files(output_path) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/util/vnet_run b/util/vnet_run new file mode 100755 index 0000000..0539b23 --- /dev/null +++ b/util/vnet_run @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# vnet_run: Run a virtual IP network in a tmux session + +import sys +import json +import pathlib +import argparse +import subprocess + +from dataclasses import dataclass + +NODES_FILE_NAME = "nodes.json" +SESSION_PREFIX = "vnet" +START_SHELL = "/bin/bash" + +DEVICE_TYPE_ROUTER = "router" +DEVICE_TYPE_HOST = "host" +VERBOSE_MODE = False + + +# Simple wrapper for running a shell command +def do_run(cmd, check=True, shell=True): + global VERBOSE_MODE + + if VERBOSE_MODE: + print("Executing: {}".format(" ".join(cmd) if isinstance(cmd, list) else cmd)) + + proc = subprocess.run(cmd, shell=shell, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + if check and proc.returncode != 0: + do_exit(f"Command exited with {proc.returncode}: {proc.stdout}") + + output = proc.stdout + return output + + +def check_bin_exists(bin_name): + bin_path = pathlib.Path(bin_name) + + if not bin_path.exists(): + print(f"Could not find binary: {bin_path}, exiting") + sys.exit(1) + + +def load_json(input_file): + with open(input_file, "r") as fd: + json_data = json.load(fd) + return json_data + + +def write_json(d, target_file): + with open(target_file, "w") as fd: + json.dump(d, fd, indent=True, sort_keys=True) + + +def kill_open_sessions(): + vnet_sessions = do_run("""tmux list-sessions 2>/dev/null | grep "vnet-" | awk '{ print $1; }' | sed 's/\://g'""") + sessions = vnet_sessions.split(" ") + for session in sessions: + print(f"Killing session {session}") + do_run(f"tmux kill-session -t {session}", check=False) + + +def do_exit(message): + print(message) + sys.exit(1) + + +# Run info for each node +@dataclass +class NodeInfo: + binary_path: str + extra_args: str = "" + + @classmethod + def from_dict(cls, d): + return NodeInfo(**d) + + +# This class abstracts out fetching per-node configurations, +# whether the info was specified with --host/--router/devices.json +# or directly with --bin-config +class BinManager(): + + def __init__(self, device_map: dict[str,NodeInfo]): + self.device_map = device_map + + # Generic per-binary lookup function + def get_info(self, node_name: str) -> NodeInfo: + if node_name not in self.device_map: + raise ValueError(f"Node {node_name} not found in device map!") + + dev = self.device_map[node_name] + return dev + + # Load binary info directly from binaries.json + @classmethod + def from_bin_config(cls, bin_config_file: str): + config_data = load_json(bin_config_file) + + device_map = {str(k): NodeInfo.from_dict(v) for k, v in config_data.items()} + + return cls(device_map) + + # Load binary info from a combination of: devices.json, --host, --router + @classmethod + def from_nodes_file(cls, nodes_file, host_bin: str, router_bin: str): + device_map: dict[str, NodeInfo] = {} + + nodes_data = load_json(nodes_file) + + for _name, _type in nodes_data.items(): + assert (_type == DEVICE_TYPE_HOST) or (_type == DEVICE_TYPE_ROUTER) + + # Create metadata for this node based on the device type + device = NodeInfo( + binary_path=router_bin if _type == DEVICE_TYPE_ROUTER else host_bin, + ) + device_map[_name] = device + + return cls(device_map) + + +def main(input_args): + global VERBOSE_MODE + + parser = argparse.ArgumentParser() + + parser.add_argument("--router", type=str, default="", help="Path to router binary") + parser.add_argument("--host", type=str, default="", help="Path to host binary") + parser.add_argument("--bin-config", type=str, default="", + help="Run nodes using binaries.json Overrides --host and --router") + parser.add_argument("--clean", action="store_true", + help="Terminate any open virtual network sessions before starting") + parser.add_argument("lnx_dir", type=str, help="Directory with lnx files") + parser.add_argument("extra_args", nargs="*", + help="Extra arguments to add when executing each node") + parser.add_argument("--verbose", action="store_true", + help="Print commands as they are run") + + args = parser.parse_args(input_args) + + if args.verbose: + VERBOSE_MODE = True + + if args.clean: + kill_open_sessions() + + lnx_path = pathlib.Path(args.lnx_dir) + if not lnx_path.exists(): + do_exit(f"Could not find net directory {lnx_path}, aborting") + + lnx_files = [f for f in lnx_path.glob("*.lnx")] + if len(lnx_files) == 0: + do_exit(f"No lnx files found in {lnx_path}") + + bin_info = None + if args.bin_config: + bin_info = BinManager.from_bin_config(args.bin_config) + else: + if args.router == "" or args.host == "": + do_exit("Must specify host and router binaries with --host and --router") + + router_bin = args.router + host_bin = args.host + + check_bin_exists(router_bin) + check_bin_exists(host_bin) + + nodes_file = lnx_path / NODES_FILE_NAME + if not nodes_file.exists(): + do_exit(f"Could not find nodes file at {nodes_file}, aborting. If you are missing this, generate the network again.") + + bin_info = BinManager.from_nodes_file(nodes_file, host_bin, router_bin) + + network_name = lnx_path.stem + session_name = "{}-{}".format(SESSION_PREFIX, network_name) + + lnx_first = lnx_files[0] + lnx_rest = lnx_files[1:] + + _extra_args_str = " ".join(args.extra_args) + + # Generate the command to run in each session + # Run each pane as the node + a shell after so that user can press + # Ctrl+C and get a shell, rather than killing the pane + def _cmd(node_name, lnx_file): + node_bin = bin_info.get_info(node_name) # Lookup from bin manager + cmd = f"{node_bin.binary_path} --config {lnx_file} {node_bin.extra_args} {_extra_args_str}; {START_SHELL}" + return cmd + + # Create the session with the first node + first_name = lnx_first.stem + do_run([ + "tmux","new-session", + "-s", session_name, + "-d", _cmd(first_name, lnx_first) + ], shell=False) + do_run(f"tmux select-pane -T {first_name}") + + # Set session options + do_run('tmux set-option -s pane-border-status top') + do_run('tmux set-option -s pane-border-format "#{pane_index}: #{pane_title}"') + + for lnx_file in lnx_rest: + node_name = lnx_file.stem + do_run([ + "tmux", "split-window", + _cmd(node_name, lnx_file) + ], shell=False) + + do_run(f"tmux select-pane -T {node_name}") + + # Even out the layout (use tiled to accommodate the maximum + # number of panes) + do_run(f"tmux select-layout tiled") + + # Finally, attach to the session + do_run(f"tmux attach-session -t {session_name}") + + +if __name__ == "__main__": + main(sys.argv[1:]) |