aboutsummaryrefslogtreecommitdiff
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/rip_dissector/README.md70
-rw-r--r--util/rip_dissector/cs168_rip.lua115
-rwxr-xr-xutil/update_from_stencil19
-rwxr-xr-xutil/vnet_generate326
-rwxr-xr-xutil/vnet_run234
5 files changed, 764 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/update_from_stencil b/util/update_from_stencil
new file mode 100755
index 0000000..c0048ea
--- /dev/null
+++ b/util/update_from_stencil
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+REPO_DIR=$(realpath "${SCRIPT_DIR}"/..)
+
+STENCIL_REPO=http://github.com/brown-csci1680/ipstack-template
+
+main() {
+ git remote rm stencil || true
+ git remote add stencil "$STENCIL_REPO"
+
+ git config pull.rebase false
+ git pull stencil main --allow-unrelated-histories -s ort -Xtheirs --no-edit
+}
+
+
+main $@
diff --git a/util/vnet_generate b/util/vnet_generate
new file mode 100755
index 0000000..8cfb417
--- /dev/null
+++ b/util/vnet_generate
@@ -0,0 +1,326 @@
+#!/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"
+
+VHOST_BINARY_NAME = "vhost"
+VROUTER_BINARY_NAME = "vrouter"
+
+
+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
+
+ 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)]
+
+ # 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")
+ neighbor_router_ips = self.get_neighbor_router_ips()
+
+ 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:
+ 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:
+ 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": f"./{VHOST_BINARY_NAME}" \
+ if node.node_type == NODE_TYPE_HOST else
+ f"./{VROUTER_BINARY_NAME}" 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()
+
+ 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..39d72a6
--- /dev/null
+++ b/util/vnet_run
@@ -0,0 +1,234 @@
+#!/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"
+
+VHOST_BINARY_NAME = "vhost"
+VROUTER_BINARY_NAME = "vrouter"
+
+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 vrouter binary")
+ parser.add_argument("--host", type=str, default="", help="Path to vhost binary")
+ parser.add_argument("--bin-dir", type=str, default=".",
+ help="Path to directory with vhost/vrouter binaries")
+ 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", default="")
+ 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.bin_dir:
+ host_bin = pathlib.Path(args.bin_dir).resolve() / VHOST_BINARY_NAME
+ router_bin = pathlib.Path(args.bin_dir).resolve() / VROUTER_BINARY_NAME
+ else:
+ if args.router == "" or args.host == "":
+ do_exit("Must specify host and router binaries with --bin-dir or (--host and --router)")
+
+ router_bin = pathlib.Path(args.router).resolve()
+ host_bin = pathlib.Path(args.host).resolve()
+
+ 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:])