def _create_and_mirred_to_ifb(self, dev_name): """ Creates a IFB for the interface so that a Qdisc can be installed on it Mirrors packets to be sent out of the interface first to itself (IFB) Assumes the interface has already invoked _set_structure() Parameters ---------- dev_name : string The interface to which the ifb was added """ ifb_name = "ifb-" + dev_name if config.get_value("assign_random_names") is False: if len(ifb_name) > MAX_CUSTOM_NAME_LEN: raise ValueError( f"Auto-generated IFB interface name {ifb_name} is too long. " f"The length of name should not exceed 15 characters.") self.ifb = Interface(ifb_name) engine.create_ifb(self.ifb.id) # Add ifb to namespace self.node._add_interface(self.ifb) # Set interface up self.ifb.set_mode("UP") default_route = {"default": "1"} # TODO: find how to set a good bandwidth default_bandwidth = {"rate": config.get_value("default_bandwidth")} # TODO: Standardize this; seems like arbitrary handle values # were chosen. self.ifb.add_qdisc("htb", "root", "1:", **default_route) self.ifb.add_class("htb", "1:", "1:1", **default_bandwidth) self.ifb.add_qdisc("pfifo", "1:1", "11:") action_redirect = { "match": "u32 0 0", # from man page examples "action": "mirred", "egress": "redirect", "dev": self.ifb.id, } # NOTE: Use Filter API engine.add_filter(self.node.id, self.id, "all", "1", "u32", parent="1:", **action_redirect)
def setup_flow_workers(exp_runners, exp_stop_time): """ Setup flow generation and stats collection processes(netperf, ss, tc, iperf3...). Also add a progress bar process for showing experiment progress. Parameters ---------- exp_runners: collections.NamedTuple all(netperf, ping, ss, tc..) the runners exp_stop_time: int Time when experiment stops (in seconds) Returns ------- List[multiprocessing.Process] flow generation and stats collection processes + progress bar process """ workers = [] for runners in exp_runners: workers.extend([Process(target=runner.run) for runner in runners]) # Add progress bar process if config.get_value("show_progress_bar"): workers.extend([Process(target=progress_bar, args=(exp_stop_time,))]) return workers
def __init__(self, name, node_id, veth_end_id): """ Constructor for an IFB Created only if bandwidth is given or default bandwidth is set in case of adding qdisc and delay buy bandwidth not specified Parameters ---------- name : str User given name for the interface node_id : str This is the id of the node that the device belongs to veth_end_id : str This is the id of the veth that the IFB is attached to """ super().__init__(name, node_id) self.veth_end_id = veth_end_id # Create Ifb and add to namespace engine.create_ifb(self._id) engine.add_int_to_ns(node_id, self._id) self.set_mode("UP") self.current_bandwidth = config.get_value("default_bandwidth") self._add_default_qdisc_and_mirror_packets()
def create_config(self): """ Creates config file on disk from `self.conf` """ with open(self.conf_file, "w") as conf: shutil.chown(self.conf_file, user=config.get_value("routing_suite")) self.conf.seek(0) shutil.copyfileobj(self.conf, conf)
def _create_directory(self, dir_path): """ Creates a quagga/frr owned directory at `dir_path` Parmeters --------- dir_path: path of the directory to be created """ mkdir(dir_path) chown(dir_path, user=config.get_value("routing_suite"))
def _autogenerate_interface_name(node1, node2, connections): """ Auto-generate interface names based on respective node names and number of connections """ interface_name = node1.name + "-" + node2.name + "-" + str(connections) if config.get_value("assign_random_names") is False: if len(interface_name) > MAX_CUSTOM_NAME_LEN: raise ValueError( f"Auto-generated device name {interface_name} is too long. " f"The length of name should not exceed 15 characters.") return interface_name
def _create_conf_directory(self): """ Creates a directory for holding routing related config and pid files. Override this to create directory at a location other than /tmp Returns ------- str: path of the created directory """ salt = config.get_value("routing_suite") + str(time.clock_gettime(0)) dir_path = f"/tmp/{salt}-configs_{IdGen.topology_id}" self._create_directory(dir_path) return dir_path
def _run_dyn_routing(self): """ Run zebra and `self.protocol` """ logger.info("Running zebra and %s on routers", self.protocol) self.conf_dir = self._create_conf_directory() if config.get_value("routing_logs"): self.log_dir = self._create_log_directory() for router in self.routers: self._run_zebra(router) self._run_routing_protocol(router) if router in self.ldp_routers: self._run_ldp(router) self._check_for_convergence()
def _set_structure(self): """ Sets a proper structure to the interface by creating HTB class with default bandwidth and a netem qdisc as a child. (default bandwidth = 1024mbit) """ self.set_structure = True default_route = {"default": "1"} self.add_qdisc("htb", "root", "1:", **default_route) # TODO: find how to set a good bandwidth default_bandwidth = {"rate": config.get_value("default_bandwidth")} self.add_class("htb", "1:", "1:1", **default_bandwidth) self.add_qdisc("netem", "1:1", "11:")
def __init__(self, interface_name): """ Constructor of Interface. *Note*: Unlike Node object, the creation of Interface object does not actually create an interface in the backend. This has to be done seperately by invoking engine. [See `Veth` class for an example] Parameters ---------- interface_name : str Name of the interface """ if config.get_value("assign_random_names") is False: if len(interface_name) > MAX_CUSTOM_NAME_LEN: raise ValueError( f"Interface name {interface_name} is too long. Interface names " f"should not exceed 15 characters") # TODO: name and address should be the only public members self._name = interface_name self._id = IdGen.get_id(interface_name) self._address = None self._node = None self._pair = None # Normally this is the default mtu value. self._mtu = 1500 self.set_structure = False self.ifb = None # TODO: These are not rightly updated # set_delay and set_bandwidth invoke the # engine function directly self.qdisc_list = [] self.class_list = [] self.filter_list = [] # mpls input self._is_mpls_enabled = False
def __init__(self, router_ns_id, interfaces, daemon, conf_dir, **kwargs): """ Parameters ---------- conf_dir : str Directory to store config files **kwargs Key worded arguments for other daemon specific parameters ``log_dir``: Directory to store log files. (`str`) """ self.logger = logging.getLogger(__name__) self.daemon = daemon if not any( isinstance(filter, DepedencyCheckFilter) for filter in self.logger.filters ): # Duplicate filter is added to avoid logging of same error # message incase any of the routing daemon is not installed self.logger.addFilter(DepedencyCheckFilter()) if not any( isinstance(filter, DuplicateRoutingLogsFilter) for filter in self.logger.filters ): self.logger.addFilter(DuplicateRoutingLogsFilter()) if not supports_dynamic_routing(daemon): self.handle_dependecy_error() self.conf = io.StringIO() self.router_ns_id = router_ns_id self.conf_file = f"{conf_dir}/{self.router_ns_id}_{daemon}.conf" self.pid_file = f"{conf_dir}/{self.router_ns_id}_{daemon}.pid" self.log_file = None if kwargs["log_dir"] is not None: self.logger.info( "%s logging enabled. Log files can found in %s directory", config.get_value("routing_suite"), kwargs["log_dir"], ) self.log_file = f"{kwargs['log_dir']}/{self.router_ns_id}_{daemon}.log" self.interfaces = interfaces self.ipv6 = interfaces[0].address.is_ipv6()
def create_basic_config(self): """ Creates a file with basic configuration for OSPF. Use base `add_to_config` directly for more complex configurations """ if self.ipv6: for interface in self.interfaces: self.add_to_config(f"interface {interface.id}") # send hello packets every 1 second for faster convergence self.add_to_config("ipv6 ospf6 hello-interval 1") self.add_to_config("router ospf6") # Generates random router-id in A.B.C.D format router_id = ".".join( map(str, (random.randint(0, 255) for _ in range(0, 4)))) # for quagga if config.get_value("routing_suite") == "quagga": self.add_to_config(f"router-id {router_id}") # for frr else: self.add_to_config(f"ospf6 router-id {router_id}") for interface in self.interfaces: self.add_to_config(f" interface {interface.id} area 0.0.0.0") else: for interface in self.interfaces: self.add_to_config(f"interface {interface.id}") # send hello packets every 1 second for faster convergence self.add_to_config("ip ospf hello-interval 1") self.add_to_config("router ospf") self.add_to_config( f"ospf router-id {self.interfaces[0].address.get_addr(with_subnet=False)}" ) for interface in self.interfaces: self.add_to_config( f" network {interface.address.get_subnet()} area 0.0.0.0") if self.log_file is not None: self.add_to_config(f"log file {self.log_file}") self.create_config()
def wrapper_dad_check(*args, **kwargs): """ Wrapper function for DAD Parameters ---------- *args : Non-Keyword Arguments passes variable number of arguments to a function **kwargs : Keyword Arguments passes keyworded, variable-length argument list to a function Returns ------- func Function to be executed after wrapper is executed """ if (g_var.IS_IPV6 is True and config.get_value("disable_dad") is not True and g_var.IS_DAD_CHECKED is not True): namespaces = TopologyMap.get_namespaces() # Verifies if IPv6 states are addressable or not while True: status = check_ipv6_states(namespaces) # IPv6 state will be both in tentative and dadfailed together if status["dadfailed"][0] is True: raise Exception( "Duplicate address found " f"at interface of node {status['dadfailed'][1]}." "\nExiting ....") # Wait if IPv6 interface is in tentative state if status["tentative"] is True: sleep(1) else: break g_var.IS_DAD_CHECKED = True return func(*args, **kwargs)
def __init__(self, name): """ Create a node with given `name`. An unique `id` is assigned to this node which is used by `engine` module to create the network namespace. This ensures that there is no naming conflict between any two nodes. Parameters ---------- name: str The name of the node to be created """ if name == "": raise ValueError("Node name can't be an empty string") if config.get_value("assign_random_names") is False and len(name) > 3: # We chose 3 because: 'ifb-ns1-ns2-20' is a potential IFB interface name # and it's already 14 character long. Note that here node names # are 'ns1' and 'ns2'. The `ip` utility won't accept interface names # longer than 15 characters logger.warning( "%s is longer than 3 characters. It's safer to use " "node names with atmost 3 characters with the current config.", name, ) self._name = name self._id = IdGen.get_id(name) self._interfaces = [] # mpls max platform label kernel parameter self._mpls_max_label = 0 # Global variable disables when any new node is created # to ensure DAD check (if applicable) g_var.IS_DAD_CHECKED = False engine.create_ns(self.id) engine.set_interface_mode(self.id, "lo", "up") TopologyMap.add_namespace(self.id, self.name) TopologyMap.add_host(self)
def _add_default_qdisc_and_mirror_packets(self): """ Sets default bandwidth to the IFB """ default_route = {"default": "1"} # TODO: find how to set a good bandwidth default_bandwidth = {"rate": config.get_value("default_bandwidth")} # TODO: Standardize this; seems like arbitrary handle values # were chosen. # HTB class is added since, to use a filter and redirect traffic, # a classid is needed and htb gives it that, since it's a class self.add_qdisc("htb", "root", "1:", **default_route) self.add_class("htb", "1:", "1:1", **default_bandwidth) self.add_qdisc("pfifo", "1:1", "11:") action_redirect = { "match": "u32 0 0", # from man page examples "action": "mirred", "egress": "redirect", "dev": self.id, } # NOTE: Use Filter API # Action mirred, redicting traffic, etc is needed since netem and # the user giver qdisc are both classless and cannot be added to # the same device engine.add_filter( self.node_id, self.veth_end_id, "all", "1", "u32", parent="1:", **action_redirect, )
def __init__(self, name, node_id): """ Constructore for a device Parameters ---------- name : str User given name for the device node_id : str This is the id of the node that the device belongs to """ if config.get_value("assign_random_names") is False: if len(name) > MAX_CUSTOM_NAME_LEN: raise ValueError( f"Device name {name} is too long. Device names " f"should not exceed 15 characters") self._name = name self._id = IdGen.get_id(name) self._traffic_control_handler = TrafficControlHandler( node_id, self._id) if node_id is not None: TopologyMap.add_interface(self.node_id, self._id, self._name)
def run_experiment(exp): """ Run experiment Parameters ----------- exp : Experiment The experiment attributes """ tools = ["netperf", "ss", "tc", "iperf3", "ping"] Runners = namedtuple("runners", tools) exp_runners = Runners(netperf=[], ss=[], tc=[], iperf3=[], ping=[]) # Runner objects # Keep track of all destination nodes [to ensure netperf and iperf3 # server is run at most once] destination_nodes = {"netperf": set(), "iperf3": set()} # Contains start time and end time to run respective command # from a source netns to destination address ss_schedules = defaultdict(lambda: (float("inf"), float("-inf"))) ping_schedules = defaultdict(lambda: (float("inf"), float("-inf"))) exp_end_t = float("-inf") dependencies = get_dependency_status(tools) ss_required = False ss_filters = set() # Traffic generation for flow in exp.flows: # Get flow attributes [ src_ns, dst_ns, dst_addr, start_t, stop_t, _, options, ] = flow._get_props() # pylint: disable=protected-access exp_end_t = max(exp_end_t, stop_t) (min_start, max_stop) = ping_schedules[(src_ns, dst_addr)] ping_schedules[(src_ns, dst_addr)] = ( min(min_start, start_t), max(max_stop, stop_t), ) # Setup TCP/UDP flows if options["protocol"] == "TCP": # * Ignore netperf tcp control connections # * Destination port of netperf control connection is 12865 # * We also have "sport" (source port) in the below condition since # there can be another flow in the reverse direction whose control # connection also we must ignore. ss_filters.add("sport != 12865 and dport != 12865") ss_required = True ( tcp_runners, ss_schedules, ) = setup_tcp_flows( dependencies["netperf"], flow, ss_schedules, destination_nodes["netperf"], ) exp_runners.netperf.extend(tcp_runners) # Update destination nodes destination_nodes["netperf"].add(dst_ns) elif options["protocol"] == "UDP": # * Ignore iperf3 tcp control connections # * Destination port of iperf3 control connection is 5201 # * We also have "sport" (source port) in the below condition since # there can be another flow in the reverse direction whose control # connection also we must ignore. ss_filters.add("sport != 5201 and dport != 5201") udp_runners = setup_udp_flows(dependencies["iperf3"], flow, destination_nodes["iperf3"]) exp_runners.iperf3.extend(udp_runners) # Update destination nodes destination_nodes["iperf3"].add(dst_ns) if ss_required: ss_filter = " and ".join(ss_filters) ss_runners = setup_ss_runners(dependencies["ss"], ss_schedules, ss_filter) exp_runners.ss.extend(ss_runners) tc_runners = setup_tc_runners(dependencies["tc"], exp.qdisc_stats, exp_end_t) exp_runners.tc.extend(tc_runners) ping_runners = setup_ping_runners(dependencies["ping"], ping_schedules) exp_runners.ping.extend(ping_runners) # Start traffic generation run_workers(setup_flow_workers(exp_runners)) logger.info("Experiment complete!") logger.info("Parsing statistics...") # Parse the stored statistics run_workers(setup_parser_workers(exp_runners)) logger.info("Output results as JSON dump") # Output results as JSON dumps dump_json_ouputs() if config.get_value("plot_results"): logger.info("Plotting results...") # Plot results and dump them as images run_workers(setup_plotter_workers()) logger.info("Plotting complete!") if config.get_value("readme_in_stats_folder"): # Copying README.txt to stats folder relative_path = os.path.join("info", "README.txt") readme_path = os.path.join(os.path.dirname(__file__), relative_path) Pack.copy_files(readme_path) cleanup()
def connect( node1: topology.Node, node2: topology.Node, interface1_name: str = "", interface2_name: str = "", network: Network = None, ): """ Connects two nodes `node1` and `node2`. Creates two paired Virtual Ethernet interfaces (veth) and returns them as a 2-element tuple. The first interface belongs to `node1`, and the second interface belongs to `node2`. Parameters ---------- node1 : Node First veth interface added in this node node2 : Node Second veth interface added in this node interface1_name : str Name of first veth interface interface2_name : str Name of second veth interface network : Network Object of the Network to add interfaces Returns ------- (interface1, interface2) 2 tuple of created (paired) veth interfaces. `interface1` is in `n1` and `interface2` is in `n2`. """ # Number of connections between `node1` and `node2`, set to `None` # initially since it hasn't been computed yet connections = None # Check interface names if interface1_name == "": connections = _number_of_connections(node1, node2) interface1_name = _autogenerate_interface_name(node1, node2, connections) if interface2_name == "": if connections is None: connections = _number_of_connections(node1, node2) # pylint: disable=arguments-out-of-order interface2_name = _autogenerate_interface_name(node2, node1, connections) # Create 2 interfaces (interface1, interface2) = create_veth_pair(interface1_name, interface2_name) node1._add_interface(interface1) node2._add_interface(interface2) interface1.set_mode("UP") interface2.set_mode("UP") # Disabling Duplicate Address Detection(DAD) at the interfaces if config.get_value("disable_dad") is True: interface1.disable_ip_dad() interface2.disable_ip_dad() # The network parameter takes precedence over "global" network level if network is None: network = Network.current_network # Add the interfaces to the network if mentioned if network is not None: network.add_interface(interface1) network.add_interface(interface2) return (interface1, interface2)