def filter_paths(network, paths, min_hops=None, max_hops=None, path_count=None): dijkstra = Dijkstra(network) if min_hops is None: min_hops = 1 if max_hops is None: max_hops = math.inf filtered = [] for path in paths: d = dijkstra.find_shortest_distance(path[0], path[1]) if d >= min_hops and d <= max_hops and d != math.inf: filtered.append(path) if path_count is not None: if len(filtered) < path_count: eprint( f"Only {len(filtered)} paths left after filtering. Required were at least {path_count}." ) exit(1) if len(filtered) > path_count: filtered = filtered[:path_count] return filtered
def start_yggdrasil_instances(ids, rmap): if ip_protocol == '4': eprint('warning: yggdrasil does not support IPv4') config = 'AdminListen: none' for id in ids: remote = rmap[id] if verbosity == 'verbose': address = remote.get('address', 'local') print(f'start yggdrasil in {address}/ns-{id}') # Create a configuration file exec(remote, f'printf "{config}" > /tmp/yggdrasil-{id}.conf') # yggdrasil uses a tun0 interface, uplink only needs an fe80:* address interface_down(remote, id, 'uplink') interface_up(remote, id, 'uplink') if ip_protocol == '4': # yggdrasil does not support IPv4! pass exec( remote, f'ip netns exec "ns-{id}" nohup yggdrasil -useconffile /tmp/yggdrasil-{id}.conf > /dev/null 2> /dev/null < /dev/null &' )
def wait(beg_ms, until_sec): now_ms = millis() # wait until time is over if (now_ms - beg_ms) < (until_sec * 1000): time.sleep(((until_sec * 1000) - (now_ms - beg_ms)) / 1000.0) else: eprint('Wait timeout already passed by {:.2f}sec'.format( ((now_ms - beg_ms) - (until_sec * 1000)) / 1000)) exit(1)
def main(): parser = argparse.ArgumentParser(description='Measure mean traffic.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) parser.add_argument('--interface', help='Interface to measure traffic on.') parser.add_argument('--duration', type=int, help='Measurement duration in seconds.') args = parser.parse_args() if args.remotes: if not os.path.isfile(args.remotes): eprint(f'File not found: {args.remotes}') stop_all_terminals() exit(1) with open(args.remotes) as file: args.remotes = [Remote.from_json(obj) for obj in json.load(file)] else: args.remotes = default_remotes # need root for local setup for remote in args.remotes: if remote.address is None: if os.geteuid() != 0: eprint('Need to run as root.') exit(1) rmap = get_remote_mapping(args.remotes) if args.duration: ds = args.duration ts_beg = traffic(args.remotes, interface=args.interface, rmap=rmap) time.sleep(ds) ts_end = traffic(args.remotes, interface=args.interface, rmap=rmap) ts = ts_end - ts_beg n = ds * len(rmap) print( f'rx: {format_size(ts.rx_bytes / n)}/s, {ts.rx_packets / n:.2f} packets/s, {ts.rx_dropped / n:.2f} dropped/s (avg. per node)' ) print( f'tx: {format_size(ts.tx_bytes / n)}/s, {ts.tx_packets / n:.2f} packets/s, {ts.tx_dropped / n:.2f} dropped/s (avg. per node)' ) else: ts = traffic(args.remotes, interface=args.interface, rmap=rmap) print( f'rx: {format_size(ts.rx_bytes)} / {ts.rx_packets} packets / {ts.rx_dropped} dropped' ) print( f'tx: {format_size(ts.tx_bytes)} / {ts.tx_packets} packets / {ts.tx_dropped} dropped' ) stop_all_terminals()
def start_routing_protocol(protocol, rmap, ids, ignore_error=False): beg_count = count_instances(protocol, rmap) beg_ms = millis() for id in ids: remote = rmap[id] interface_up(remote, id, 'uplink', ignore_error) if protocol == 'babel': start_babel_instances(ids, rmap) elif protocol == 'batman-adv': start_batmanadv_instances(ids, rmap) elif protocol == 'bmx6': start_bmx6_instances(ids, rmap) elif protocol == 'cjdns': start_cjdns_instances(ids, rmap) elif protocol == 'olsr1': start_olsr1_instances(ids, rmap) elif protocol == 'olsr2': start_olsr2_instances(ids, rmap) elif protocol == 'ospf': start_ospf_instances(ids, rmap) elif protocol == 'yggdrasil': start_yggdrasil_instances(ids, rmap) elif protocol == 'none': return else: eprint(f'Error: unknown routing protocol: {protocol}') exit(1) wait_for_completion() # wait for last started process to fork # otherwise we might have one extra counted instance time.sleep(0.5) end_ms = millis() end_count = count_instances(protocol, rmap) count = end_count - beg_count if count != len(ids): eprint( f'Error: Failed to start {protocol} instances: {count}/{len(ids)} started' ) stop_all_terminals() exit(1) if verbosity != 'quiet': print('Started {} {} instances in {}'.format( count, protocol, format_duration(end_ms - beg_ms)))
def stop_routing_protocol(protocol, rmap, ids, ignore_error=False): beg_count = count_instances(protocol, rmap) beg_ms = millis() if protocol == 'babel': stop_babel_instances(ids, rmap) elif protocol == 'batman-adv': stop_batmanadv_instances(ids, rmap) elif protocol == 'bmx6': stop_bmx6_instances(ids, rmap) elif protocol == 'cjdns': stop_cjdns_instances(ids, rmap) elif protocol == 'olsr1': stop_olsr1_instances(ids, rmap) elif protocol == 'olsr2': stop_olsr2_instances(ids, rmap) elif protocol == 'ospf': stop_ospf_instances(ids, rmap) elif protocol == 'yggdrasil': stop_yggdrasil_instances(ids, rmap) elif protocol == 'none': pass else: eprint('Error: unknown routing protocol: {}'.format(protocol)) exit(1) for id in ids: remote = rmap[id] interface_down(remote, id, 'uplink', ignore_error=ignore_error) wait_for_completion() # wait for last stopped process to disappear # otherwise we might have one extra counted instance time.sleep(0.5) end_ms = millis() end_count = count_instances(protocol, rmap) count = beg_count - end_count if count != len(ids): eprint( f'Error: Failed to stop {protocol} instances: {count}/{len(ids)} left' ) exit(1) if not ignore_error and verbosity != 'quiet': print('Stopped {} {} instances in {}'.format( len(ids), protocol, format_duration(end_ms - beg_ms)))
def _stop_protocol(protocol, rmap, ids): base = os.path.dirname(os.path.realpath(__file__)) path = f'{base}/protocols/{protocol}_stop.sh' if not os.path.isfile(path): eprint(f'File does not exist: {path}') stop_all_terminals() exit(1) for id in ids: remote = rmap[id] label = remote.address or 'local' cmd = f'ip netns exec ns-{id} sh -s {label} {id} < {path}' _exec_verbose(remote, cmd) wait_for_completion()
def remove_link(link, rmap={}): source = str(link['source']) target = str(link['target']) remote1 = rmap.get(source) remote2 = rmap.get(target) ifname1 = f've-{source}-{target}' ifname2 = f've-{target}-{source}' if source == target: eprint( f'Warning: Cannot remove link with identical source ({source}) and target ({target}) => ignore' ) return if remote1 == remote2: exec( remote1, f'ip netns exec "switch" ip link del "{ifname1}" type veth peer name "{ifname2}"' ) else: # multiple remotes always have address set addr1 = remote1.address addr2 = remote2.address # ids do not have to be the same on both remotes - but it is simpler that way tunnel_id = link_num(addr1, addr2, min=1, max=2**32) session_id = link_num(source, target, min=1, max=2**32) exec( remote1, f'ip l2tp del session tunnel_id {tunnel_id} session_id {session_id}' ) if l2tp_session_count(remote1, tunnel_id) == 0: exec(remote1, f'ip l2tp del tunnel tunnel_id {tunnel_id}') exec( remote2, f'ip l2tp del session tunnel_id {tunnel_id} session_id {session_id}' ) if l2tp_session_count(remote2, tunnel_id) == 0: exec(remote2, f'ip l2tp del tunnel tunnel_id {tunnel_id}')
def run(command, rmap, quiet=False): for (id, remote) in rmap.items(): cmd = command.replace('{name}', id[3:]) if quiet: exec(remote, f'ip netns exec "ns-{id}" {cmd}', get_output=False, ignore_error=True) else: stdout, stderr, rcode = exec(remote, f'ip netns exec "ns-{id}" {cmd}', get_output=True, ignore_error=True) if stdout or stderr: print(f'{id}') if stdout: print(stdout) if stderr: eprint(stderr)
def partition_into_subgraph_nodes(neighbor_map, nodes, rmap, remotes): random.shuffle(nodes) # remote_id => [<node_ids>] partitions = {} # keep running nodes on the same partition for node_id, remote in rmap.items(): remote_id = remotes.index(remote) partitions.setdefault(remote_id, []).append(node_id) if node_id not in nodes: eprint(f'Node {node_id} not in previous state!') stop_all_terminals() exit(1) nodes.remove(node_id) for remote_id in range(len(remotes)): if len(nodes) == 0: break if remote_id not in partitions: partitions[remote_id] = [nodes.pop()] # find neighbor node of cluster def grow_cluster(cluster, nodes): for node in nodes: for cluster_node in cluster: if node in neighbor_map[cluster_node]: cluster.append(node) nodes.remove(node) return # cannot extend cluster via neighbor => use left over node cluster.append(nodes.pop()) while len(nodes) > 0: # get smallest cluster (remote) key key = min(partitions.keys(), key=lambda k: len(partitions[k])) grow_cluster(partitions[key], nodes) return partitions
def _get_random_paths(nodes, count=10, seed=None, sample_without_replacement=False): if sample_without_replacement: if count > (len(nodes) / 2): eprint( f"Not enough nodes ({len(nodes)}) to generate {count} unique paths." ) stop_all_terminals() exit(1) else: if len(nodes) < 2: eprint( f"Not enough nodes ({len(nodes)}) to generate {count} paths.") stop_all_terminals() exit(1) if seed is not None: random.seed(seed) paths = [] s = list(range(0, len(nodes))) for i in range(count): a = random.choice(s[:-1]) a_index = s.index(a) b = random.choice(s[(a_index + 1):]) b_index = s.index(b) if sample_without_replacement: s = s[:a_index] + s[(a_index + 1):b_index] + s[(b_index + 1):] if random.uniform(0, 1) > 0.5: paths.append((nodes[a], nodes[b])) else: paths.append((nodes[b], nodes[a])) return paths
def start_olsr1_instances(ids, rmap): if ip_protocol == '6': eprint('warning: IPv6 support for olsr1 is broken/buggy') for id in ids: remote = rmap[id] if verbosity == 'verbose': address = remote.get('address', 'local') print(f'start olsr1 in {address}/ns-{id}') interface_down(remote, id, 'uplink') interface_up(remote, id, 'uplink') interface_flush(remote, id, 'uplink') if ip_protocol == '6': # IPv6 support seems to be broken/buggy! set_addr6(remote, id, 'uplink', 128) else: set_addr4(remote, id, 'uplink', 32) exec(remote, f'ip netns exec "ns-{id}" olsrd -d 0 -i "uplink" -f /dev/null')
def update_link(link, link_command=None, rmap={}): source = str(link['source']) target = str(link['target']) remote1 = rmap.get(source) remote2 = rmap.get(target) ifname1 = f've-{source}-{target}' ifname2 = f've-{target}-{source}' if source == target: eprint( f'Warning: Cannot update link with identical source ({source}) and target ({target}) => ignore' ) return if link_command is not None: # source -> target exec( remote1, 'ip netns exec "switch" ' + format_link_command(link_command, link, 'source', ifname1)) # target -> source exec( remote2, 'ip netns exec "switch" ' + format_link_command(link_command, link, 'target', ifname2))
def _get_random_paths(nodes, count=10, seed=None): if count > (len(nodes) * (len(nodes) - 1) // 2): eprint(f'Path count ({count}) too big to generate unique paths.') stop_all_terminals() exit(1) if seed is not None: random.seed(seed) def decode(items, i): k = math.floor((1 + math.sqrt(1 + 8 * i)) / 2) return (items[k], items[i - k * (k - 1) // 2]) def rand_pair(n): return decode(random.randrange(n * (n - 1) // 2)) def rand_pairs(items, npairs): n = len(items) return [ decode(items, i) for i in random.sample(range(n * (n - 1) // 2), npairs) ] return rand_pairs(nodes, count)
def _process_json(json_data): # in reality, only '@', ':', '/' and whitespace should cause problems name_re = re.compile(r'^[\w-]{1,6}$') links = {} nodes = {} for node in json_data.get('nodes', []): name = str(node['id']) if not name_re.match(name): eprint(f'Invalid node name: {name}') stop_all_terminals() exit(1) nodes[name] = node for link in json_data.get('links', []): source = str(link['source']) target = str(link['target']) if len(source) > 6: eprint(f'Node name too long: {source}') stop_all_terminals() exit(1) if len(target) > 6: eprint(f'Node name too long: {target}') stop_all_terminals() exit(1) if source not in nodes: nodes[source] = {'id': source} if target not in nodes: nodes[target] = {'id': target} if source > target: links[f'{source}-{target}'] = link else: links[f'{target}-{source}'] = link return (links, nodes)
def main(): parser = argparse.ArgumentParser() parser.add_argument('--verbosity', choices=['verbose', 'normal', 'quiet'], default='normal', help='Set verbosity.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) parser.add_argument('--ip-protocol', choices=['4', '6'], help='Use IPv4/IPv6 only.') parser.set_defaults(to_state=None) subparsers = parser.add_subparsers(dest='action', required=True, help='Action') parser_start = subparsers.add_parser( 'start', help='Start protocol daemons in every namespace.') parser_start.add_argument('protocol', choices=protocol_choices, help='Routing protocol to start.') parser_start.add_argument('to_state', nargs='?', default=None, help='To state') parser_stop = subparsers.add_parser( 'stop', help='Stop protocol daemons in every namespace.') parser_stop.add_argument('protocol', choices=protocol_choices, help='Routing protocol to stop.') parser_stop.add_argument('to_state', nargs='?', default=None, help='To state') parser_change = subparsers.add_parser( 'apply', help='Stop/Start protocol daemons in every namespace.') parser_change.add_argument('protocol', choices=protocol_choices, help='Routing protocol to change.') parser_change.add_argument('to_state', nargs='?', default=None, help='To state') parser_run = subparsers.add_parser( 'run', help='Execute any command in every namespace.') parser_run.add_argument( 'command', nargs=argparse.REMAINDER, help='Shell command that is run. {name} is replaced by the nodes name.' ) parser_run.add_argument('--quiet', action='store_true', help='Do not output stdout and stderr.') parser_run.add_argument('to_state', nargs='?', default=None, help='To state') parser_clear = subparsers.add_parser('clear', help='Stop all routing protocols.') args = parser.parse_args() global ip_protocol ip_protocol = args.ip_protocol if args.remotes: with open(args.remotes) as file: args.remotes = json.load(file) else: args.remotes = default_remotes check_access(args.remotes) global verbosity verbosity = args.verbosity # get nodes that have been added or will be removed (old_ids, new_ids, rmap) = _get_update(args.to_state, args.remotes) if args.action == 'start': if args.to_state: start_routing_protocol(args.protocol, rmap, new_ids) else: all = list(rmap.keys()) start_routing_protocol(args.protocol, rmap, all) elif args.action == 'stop': if args.to_state: stop_routing_protocol(args.protocol, rmap, old_ids) else: all = list(rmap.keys()) stop_routing_protocol(args.protocol, rmap, all) elif args.action == 'apply': stop_routing_protocol(args.protocol, rmap, old_ids) start_routing_protocol(args.protocol, rmap, new_ids) elif args.action == 'clear': clear(args.remotes) elif args.action == 'run': run(' '.join(args.command), rmap, args.quiet) else: eprint('Unknown action: {}'.format(args.action)) exit(1) stop_all_terminals()
def _get_remote_mapping(cur_state, new_state, remotes, cur_state_rmap): def partition_into_subgraph_nodes(neighbor_map, nodes, rmap, remotes): random.shuffle(nodes) # remote_id => [<node_ids>] partitions = {} # keep running nodes on the same partition for node_id, remote in rmap.items(): remote_id = remotes.index(remote) partitions.setdefault(remote_id, []).append(node_id) if node_id not in nodes: eprint(f'Node {node_id} not in previous state!') stop_all_terminals() exit(1) nodes.remove(node_id) for remote_id in range(len(remotes)): if len(nodes) == 0: break if remote_id not in partitions: partitions[remote_id] = [nodes.pop()] # find neighbor node of cluster def grow_cluster(cluster, nodes): for node in nodes: for cluster_node in cluster: if node in neighbor_map[cluster_node]: cluster.append(node) nodes.remove(node) return # cannot extend cluster via neighbor => use left over node cluster.append(nodes.pop()) while len(nodes) > 0: # get smallest cluster (remote) key key = min(partitions.keys(), key=lambda k: len(partitions[k])) grow_cluster(partitions[key], nodes) return partitions # get node distribution balance def get_variance(partition): median = 0 for remote_id, cluster in partition.items(): median += len(cluster) median /= len(partition) q = 0 for remote_id, cluster in partition.items(): q = (len(cluster) - median)**2 return math.sqrt(q / len(partition)) def partition_to_map(partition, remotes): node_to_remote_map = {} for remote_id, node_ids in partition.items(): for node_id in node_ids: node_to_remote_map[node_id] = remotes[remote_id] return node_to_remote_map ''' # debug output def debug_partition(partition, remotes): print('partitioning:') for remote_id, cluster in partition.items(): print(' {}: {} nodes'.format(remotes[remote_id].get('address', 'local'), len(cluster))) interconnects = 0 node_to_remote_map = partition_to_map(partition, remotes) for link in new_state.get('links', []): if node_to_remote_map[str(link['source'])] is not node_to_remote_map[str(link['target'])]: interconnects += 1 print(f' l2tp links: {interconnects}') ''' # try multiple random (connected) partitions and select the best neighbor_map = convert_to_neighbors(cur_state, new_state) tries = 20 lowest_variance = math.inf best_partition = [] # shortcut: if no mapping on multiple remotes is needed if len(remotes) == 1 and len(cur_state_rmap) == 0: return partition_to_map({0: neighbor_map.keys()}, remotes) for _ in range(tries): partition = partition_into_subgraph_nodes(neighbor_map, list(neighbor_map.keys()), cur_state_rmap, remotes) if partition: variance = get_variance(partition) if variance < lowest_variance: lowest_variance = variance best_partition = partition if len(best_partition) == 0: eprint('No network partition found.') stop_all_terminals() exit(1) #if verbosity in ['verbose', 'normal']: # debug_partition(best_partition, remotes) # node_id => remote return partition_to_map(best_partition, remotes)
def create_link(link, link_command=None, rmap={}): source = str(link['source']) target = str(link['target']) remote1 = rmap.get(source) remote2 = rmap.get(target) ifname1 = f've-{source}-{target}' ifname2 = f've-{target}-{source}' brname1 = f'br-{source}' brname2 = f'br-{target}' if source == target: eprint( f'Warning: Cannot create link with identical source ({source}) and target ({target}) => ignore' ) return if remote1 == remote2: # create veth interface pair exec( remote1, f'ip netns exec "switch" ip link add "{ifname1}" type veth peer name "{ifname2}"' ) else: # create l2tp connection addr1 = remote1.address addr2 = remote2.address # ids and port do not have to be the same on both remotes - but it is simpler that way tunnel_id = link_num(addr1, addr2, min=1, max=2**32) session_id = link_num(source, target, min=1, max=2**32) port = link_num(addr1, addr2, min=1024, max=2**16) if not l2tp_tunnel_exists(remote1, tunnel_id): exec( remote1, f'ip l2tp add tunnel tunnel_id {tunnel_id} peer_tunnel_id {tunnel_id} encap udp local {addr1} remote {addr2} udp_sport {port} udp_dport {port}' ) exec( remote1, f'ip l2tp add session name {ifname1} tunnel_id {tunnel_id} session_id {session_id} peer_session_id {session_id}' ) exec(remote1, f'ip link set "{ifname1}" netns "switch"') if not l2tp_tunnel_exists(remote2, tunnel_id): exec( remote2, f'ip l2tp add tunnel tunnel_id {tunnel_id} peer_tunnel_id {tunnel_id} encap udp local {addr2} remote {addr1} udp_sport {port} udp_dport {port}' ) exec( remote2, f'ip l2tp add session name {ifname2} tunnel_id {tunnel_id} session_id {session_id} peer_session_id {session_id}' ) exec(remote2, f'ip link set "{ifname2}" netns "switch"') configure_interface(remote1, 'switch', ifname1) configure_interface(remote2, 'switch', ifname2) # put into bridge exec(remote1, f'ip netns exec "switch" ip link set "{ifname1}" master "{brname1}"') exec(remote2, f'ip netns exec "switch" ip link set "{ifname2}" master "{brname2}"') # isolate interfaces (they can only speak to the downlink interface in the bridge they are) exec( remote1, f'ip netns exec "switch" bridge link set dev "{ifname1}" isolated on') exec( remote2, f'ip netns exec "switch" bridge link set dev "{ifname2}" isolated on') # e.g. execute tc command on link if link_command is not None: # source -> target exec( remote1, 'ip netns exec "switch" ' + format_link_command(link_command, link, 'source', ifname1)) # target -> source exec( remote2, 'ip netns exec "switch" ' + format_link_command(link_command, link, 'target', ifname2))
def main(): parser = argparse.ArgumentParser(description='Ping various nodes.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) parser.add_argument('--input', help='JSON state of the network.') parser.add_argument('--interface', help='Interface to send data over (autodetected).') parser.add_argument('--min-hops', type=int, help='Minimum hops to ping. Needs --input.') parser.add_argument('--max-hops', type=int, help='Maximum hops to ping. Needs --input.') parser.add_argument( '--pings', type=int, default=10, help='Number of pings (unique, no self, no reverse paths).') parser.add_argument('--duration', type=int, default=1000, help='Spread pings over duration in ms.') parser.add_argument('-4', action='store_true', help='Force use of IPv4 addresses.') parser.add_argument('-6', action='store_true', help='Force use of IPv6 addresses.') args = parser.parse_args() if args.remotes: if not os.path.isfile(args.remotes): eprint(f'File not found: {args.remotes}') stop_all_terminals() exit(1) with open(args.remotes) as file: args.remotes = [Remote.from_json(obj) for obj in json.load(file)] else: args.remotes = default_remotes # need root for local setup for remote in args.remotes: if remote.address is None: if os.geteuid() != 0: eprint('Need to run as root.') exit(1) paths = None if args.input: state = json.load(args.input) paths = get_random_paths(network=state, count=args.pings) paths = filter_paths(state, paths, min_hops=args.min_hops, max_hops=args.max_hops) else: if args.min_hops is not None or args.max_hops is not None: eprint( 'No min/max hops available without topology information (--input)' ) stop_all_terminals() exit(1) rmap = get_remote_mapping(args.remotes) all = list(rmap.keys()) paths = _get_random_paths(nodes=all, count=args.pings) address_type = None if getattr(args, '4'): address_type = '4' if getattr(args, '6'): address_type = '6' ping(paths=paths, remotes=args.remotes, duration_ms=args.duration, interface=args.interface, verbosity='verbose', address_type=address_type) stop_all_terminals()
def main(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description= 'Create a virtual network based on linux network names and virtual network interfaces:\n ./network.py change none test.json' ) parser.add_argument('--verbosity', choices=['verbose', 'normal', 'quiet'], default='normal', help='Set verbosity.') parser.add_argument( '--link-command', help= 'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {tq}"' ) parser.add_argument( '--node-command', help= 'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {id}"' ) parser.add_argument('--block-arp', action='store_true', help='Block ARP packets.') parser.add_argument('--block-multicast', action='store_true', help='Block multicast packets.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) subparsers = parser.add_subparsers(dest='action', required=True) parser_change = subparsers.add_parser( 'apply', help='Create or change a virtual network.') parser_change.add_argument( 'new_state', help= 'JSON file that describes the target topology. Use "none" to remove all network namespaces.' ) subparsers.add_parser( 'show', help= 'List all Linux network namespaces. Namespace "switch" is the special cable cabinet namespace.' ) subparsers.add_parser( 'clear', help= 'Remove all Linux network namespaces. Processes still might need to be killed.' ) args = parser.parse_args() block_arp = args.block_arp block_multicast = args.block_multicast verbosity = args.verbosity if args.remotes: with open(args.remotes) as file: args.remotes = json.load(file) else: args.remotes = default_remotes if args.action == 'clear': clear(args.remotes) elif args.action == 'show': show(args.remotes) elif args.action == 'apply': apply(args.new_state, args.node_command, args.link_command, args.remotes) else: eprint(f'Invalid command: {args.action}') exit(1) stop_all_terminals()
def main(): parser = argparse.ArgumentParser(description="Ping various nodes.") parser.add_argument( "--remotes", help= "Distribute nodes and links on remotes described in the JSON file.", ) parser.add_argument("--input", help="JSON state of the network.") parser.add_argument("--interface", help="Interface to send data over (autodetected).") parser.add_argument("--min-hops", type=int, help="Minimum hops to ping. Needs --input.") parser.add_argument("--max-hops", type=int, help="Maximum hops to ping. Needs --input.") parser.add_argument( "--pings", type=int, default=10, help="Number of pings (unique, no self, no reverse paths).", ) parser.add_argument("--duration", type=int, default=1000, help="Spread pings over duration in ms.") parser.add_argument( "--deadline", type=int, default=1, help= "Specify a timeout, in seconds, before ping exits regardless of how many packets have been sent or received. In this case ping does not stop after count packet are sent, it waits either for deadline expire or until count probes are answered or for some error notification from network.", ) parser.add_argument( "--timeout", type=int, default=None, help= "Time to wait for a response, in seconds. The option affects only timeout in absence of any responses, otherwise ping waits for two RTTs.", ) parser.add_argument("--path", nargs=2, help="Send pings from a node to another.") parser.add_argument("-4", action="store_true", help="Force use of IPv4 addresses.") parser.add_argument("-6", action="store_true", help="Force use of IPv6 addresses.") args = parser.parse_args() if args.remotes: if not os.path.isfile(args.remotes): eprint(f"File not found: {args.remotes}") stop_all_terminals() exit(1) with open(args.remotes) as file: args.remotes = [Remote.from_json(obj) for obj in json.load(file)] else: args.remotes = default_remotes # need root for local setup for remote in args.remotes: if remote.address is None: if os.geteuid() != 0: eprint("Need to run as root.") exit(1) paths = None if args.path: for ns in args.path: if not namespace_exists(args.remotes, ns): eprint(f"Namespace ns-{ns} does not exist") stop_all_terminals() exit(1) paths = [args.path] elif args.input: state = json.load(args.input) paths = get_random_paths(network=state, count=args.pings) paths = filter_paths(state, paths, min_hops=args.min_hops, max_hops=args.max_hops) else: if args.min_hops is not None or args.max_hops is not None: eprint( "No min/max hops available without topology information (--input)" ) stop_all_terminals() exit(1) rmap = get_remote_mapping(args.remotes) all = list(rmap.keys()) paths = _get_random_paths(nodes=all, count=args.pings) address_type = None if getattr(args, "4"): address_type = "4" if getattr(args, "6"): address_type = "6" ping( paths=paths, remotes=args.remotes, duration_ms=args.duration, interface=args.interface, verbosity="verbose", address_type=address_type, ping_deadline=args.deadline, ping_timeout=args.timeout, ) stop_all_terminals()
def ping( paths, duration_ms=1000, remotes=default_remotes, interface=None, verbosity="normal", address_type=None, ping_deadline=1, ping_timeout=None, ): ping_count = 1 rmap = get_remote_mapping(remotes) path_count = len(paths) # prepare ping tasks tasks = [] for (source, target) in paths: source_remote = rmap[source] target_remote = rmap[target] if interface is None: interface = _get_interface(source_remote, source) target_addr = _get_ip_address(target_remote, target, interface, address_type) if target_addr is None: eprint(f"Cannot get address of {interface} in ns-{target}") else: debug = f"ping {source:>4} => {target:>4} ({target_addr:<18} / {interface})" command = ( f"ip netns exec ns-{source} ping -c {ping_count} " + (f"-w {ping_deadline} " if ping_deadline is not None else "") + (f"-W {ping_timeout} " if ping_timeout is not None else "") + f"-D -I {interface} {target_addr}") tasks.append((source_remote, command, debug)) processes = [] started = 0 def process_results(): for (process, started_ms, debug, result) in processes: if not result.processed and process.poll() is not None: process.wait() (output, err) = process.communicate() _parse_ping(result, output.decode()) result.processed = True # keep track of status ouput lines to delete them for updates lines_printed = 0 def print_processes(): nonlocal lines_printed # delete previous printed lines for _ in range(lines_printed): sys.stdout.write("\x1b[1A\x1b[2K") lines_printed = 0 process_counter = 0 for (process, started_ms, debug, result) in processes: process_counter += 1 status = "???" if result.processed: if result.packet_loss == 0.0: status = "success" elif result.packet_loss == 100.0: status = "failed" else: status = f"mixed ({result.packet_loss:0.2f}% loss)" else: status = "running" print( f"[{process_counter:03}:{started_ms:06}] {debug} => {status}") lines_printed += 1 # start tasks in the given time frame start_ms = millis() last_processed = millis() tasks_count = len(tasks) while started < tasks_count: started_expected = math.ceil(tasks_count * ((millis() - start_ms) / duration_ms)) if started_expected > started: for _ in range(0, started_expected - started): if len(tasks) == 0: break (remote, command, debug) = tasks.pop() process = create_process(remote, command) started_ms = millis() - start_ms processes.append( (process, started_ms, debug, _PingResult(ping_count))) # process results and print updates once per second if (last_processed + 1000) < millis(): last_processed = millis() process_results() if verbosity != "quiet": print_processes() started += 1 else: # sleep a small amount time.sleep(duration_ms / tasks_count / 1000.0 / 10.0) stop1_ms = millis() # wait until rest fraction of duration_ms is over if (stop1_ms - start_ms) < duration_ms: time.sleep((duration_ms - (stop1_ms - start_ms)) / 1000.0) stop2_ms = millis() process_results() if verbosity != "quiet": print_processes() # collect results rtt_avg_ms_count = 0 ret = _PingStats() for (process, started_ms, debug, result) in processes: ret.send += result.send if result.processed: ret.received += int(result.send * (1.0 - (result.packet_loss / 100.0))) # failing ping outputs do not have rtt values if not math.isnan(result.rtt_avg): ret.rtt_avg_ms += result.rtt_avg rtt_avg_ms_count += 1 if rtt_avg_ms_count > 0: ret.rtt_avg_ms /= float(rtt_avg_ms_count) result_duration_ms = stop1_ms - start_ms result_filler_ms = stop2_ms - stop1_ms if verbosity != "quiet": print( "pings send: {}, received: {} ({}), measurement span: {}ms".format( ret.send, ret.received, "-" if (ret.send == 0) else f"{100.0 * (ret.received / ret.send):0.2f}%", result_duration_ms + result_filler_ms, )) return ret
def root(): if os.geteuid() != 0: eprint('Need to run as root.') exit(1)
def apply(state={}, node_command=None, link_command=None, remotes=default_remotes): check_access(remotes) new_state = state (cur_state, cur_state_rmap) = get_current_state(remotes) # handle different new_state types if isinstance(new_state, str): if new_state == 'none': new_state = {} else: if not os.path.isfile(new_state): eprint(f'File not found: {new_state}') stop_all_terminals() exit(1) with open(new_state) as file: new_state = json.load(file) # map each node to a remote or local computer # distribute evenly with minimized interconnects rmap = _get_remote_mapping(cur_state, new_state, remotes, cur_state_rmap) data = _get_task(cur_state, new_state) beg_ms = millis() # add "switch" namespace if state_empty(cur_state): for remote in remotes: # add switch if it does not exist yet exec(remote, 'ip netns add "switch" || true') # disable IPv6 in switch namespace (no need, less overhead) exec( remote, 'ip netns exec "switch" sysctl -q -w net.ipv6.conf.all.disable_ipv6=1' ) for node in data.nodes_update: update_node(node, node_command, rmap) for link in data.links_update: update_link(link, link_command, rmap) for node in data.nodes_create: create_node(node, node_command, rmap) for link in data.links_create: create_link(link, link_command, rmap) for link in data.links_remove: remove_link(link, rmap) for node in data.nodes_remove: remove_node(node, rmap) # remove "switch" namespace if state_empty(new_state): for remote in remotes: exec(remote, 'ip netns del "switch" || true') # wait for tasks to complete wait_for_completion() end_ms = millis() if verbosity != 'quiet': print('Network setup in {}:'.format(format_duration(end_ms - beg_ms))) print( f' nodes: {len(data.nodes_create)} created, {len(data.nodes_remove)} removed, {len(data.nodes_update)} updated' ) print( f' links: {len(data.links_create)} created, {len(data.links_remove)} removed, {len(data.links_update)} updated' ) return new_state
def ping_paths(paths, duration_ms=1000, remotes=default_remotes, interface=None, verbosity='normal'): ping_deadline = 1 ping_count = 1 processes = [] start_ms = millis() started = 0 rmap = get_remote_mapping(remotes) path_count = len(paths) while started < path_count: # number of expected tests to have been run started_expected = math.ceil(path_count * ((millis() - start_ms) / duration_ms)) if started_expected > started: for _ in range(0, started_expected - started): if len(paths) == 0: break (source, target) = paths.pop() source_remote = rmap[source] target_remote = rmap[target] if interface is None: interface = _get_interface(source_remote, source) target_addr = _get_ip_address(target_remote, target, interface) if target_addr is None: eprint(f'Cannot get address of {interface} in ns-{target}') # count as started started += 1 else: debug = '[{:06}] Ping {} => {} ({} / {})'.format( millis() - start_ms, source, target, target_addr, interface) process = create_process( source_remote, f'ip netns exec ns-{source} ping -c {ping_count} -w {ping_deadline} -D {target_addr}' ) processes.append((process, debug)) started += 1 else: # sleep a small amount time.sleep(duration_ms / path_count / 1000.0 / 10.0) stop1_ms = millis() # wait until duration_ms is over if (stop1_ms - start_ms) < duration_ms: time.sleep((duration_ms - (stop1_ms - start_ms)) / 1000.0) stop2_ms = millis() ret = _PingResult() # wait/collect for results from pings (prolongs testing up to 1 second!) for (process, debug) in processes: process.wait() (output, err) = process.communicate() result = _parse_ping(output.decode()) result.send = ping_count # TODO: nicer ret.send += result.send ret.transmitted += result.transmitted ret.received += result.received ret.rtt_avg += result.rtt_avg if verbosity != 'quiet': if result.send != result.received: print(f'{debug} => failed') else: # success print(f'{debug}') ret.rtt_avg = 0 if ret.received == 0 else int(ret.rtt_avg / ret.received) result_duration_ms = stop1_ms - start_ms result_filler_ms = stop2_ms - stop1_ms if verbosity != 'quiet': print('send: {}, received: {}, arrived: {}%, measurement span: {}ms'. format( ret.send, ret.received, '-' if (ret.send == 0) else '{:0.2f}'.format(100.0 * (ret.received / ret.send)), result_duration_ms + result_filler_ms)) return ret
def main(): parser = argparse.ArgumentParser() parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) subparsers = parser.add_subparsers(dest='action', required=True) parser_traffic = subparsers.add_parser('traffic', help='Measure mean traffic.') parser_traffic.add_argument('--interface', help='Interface to measure traffic on.') parser_traffic.add_argument('--duration', type=int, help='Measurement duration in seconds.') parser_ping = subparsers.add_parser('ping', help='Ping various nodes.') parser_ping.add_argument('--input', help='JSON state of the network.') parser_ping.add_argument( '--interface', help='Interface to send data over (autodetected).') parser_ping.add_argument('--min-hops', type=int, help='Minimum hops to ping. Needs --input.') parser_ping.add_argument('--max-hops', type=int, help='Maximum hops to ping. Needs --input.') parser_ping.add_argument( '--pings', type=int, default=10, help='Number of pings (unique, no self, no reverse paths).') parser_ping.add_argument('--duration', type=int, default=1000, help='Spread pings over duration in ms.') args = parser.parse_args() if args.remotes: with open(args.remotes) as file: args.remotes = json.load(file) else: args.remotes = default_remotes # need root for local setup for remote in args.remotes: if remote.get('address') is None: if os.geteuid() != 0: eprint('Need to run as root.') exit(1) if args.action == 'traffic': rmap = get_remote_mapping(args.remotes) if args.duration: ds = args.duration ts_beg = traffic(args.remotes, interface=args.interface, rmap=rmap) time.sleep(ds) ts_end = traffic(args.remotes, interface=args.interface, rmap=rmap) ts = ts_end - ts_beg n = ds * len(rmap) print( f'rx: {format_size(ts.rx_bytes / n)}/s, {ts.rx_packets / n:.2f} packets/s, {ts.rx_dropped / n:.2f} dropped/s (avg. per node)' ) print( f'tx: {format_size(ts.tx_bytes / n)}/s, {ts.tx_packets / n:.2f} packets/s, {ts.tx_dropped / n:.2f} dropped/s (avg. per node)' ) else: ts = traffic(args.remotes, interface=args.interface, rmap=rmap) print( f'rx: {format_size(ts.rx_bytes)} / {ts.rx_packets} packets / {ts.rx_dropped} dropped' ) print( f'tx: {format_size(ts.tx_bytes)} / {ts.tx_packets} packets / {ts.tx_dropped} dropped' ) elif args.action == 'ping': paths = None if args.input: state = json.load(args.input) paths = get_random_paths(network=state, count=args.pings) paths = filter_paths(state, paths, min_hops=args.min_hops, max_hops=args.max_hops) else: if args.min_hops is not None or args.max_hops is not None: eprint( 'No min/max hops available without topology information (--input)' ) stop_all_terminals() exit(1) rmap = get_remote_mapping(args.remotes) all = list(rmap.keys()) paths = _get_random_paths(nodes=all, count=args.pings) ping_paths(paths=paths, remotes=args.remotes, duration_ms=args.duration, interface=args.interface, verbosity='verbose') else: eprint(f'Unknown action: {args.action}') exit(1) stop_all_terminals()
def main(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description= 'Create a virtual network based on linux network names and virtual network interfaces:\n ./network.py change none test.json' ) parser.add_argument('--verbosity', choices=['verbose', 'normal', 'quiet'], default='normal', help='Set verbosity.') parser.add_argument( '--link-command', help= 'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {tq}"' ) parser.add_argument( '--node-command', help= 'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {id}"' ) parser.add_argument('--disable-arp', action='store_true', help='Disable ARP support on each interface.') parser.add_argument('--disable-multicast', action='store_true', help='Disable Multicast support each interface.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) parser.add_argument( '--mtu', type=int, default=1500, help='Set Maximum Transfer Unit (MTU) on each interface.') subparsers = parser.add_subparsers(dest='action', required=True) parser_change = subparsers.add_parser( 'apply', help='Create or change a virtual network.') parser_change.add_argument( 'new_state', help= 'JSON file that describes the target topology. Use "none" to remove all network namespaces.' ) subparsers.add_parser( 'show', help= 'List all Linux network namespaces. Namespace "switch" is the special cable cabinet namespace.' ) subparsers.add_parser( 'clear', help= 'Remove all Linux network namespaces. Processes still might need to be killed.' ) args = parser.parse_args() global disable_arp global disable_multicast global verbosity global mtu disable_arp = args.disable_arp disable_multicast = args.disable_multicast verbosity = args.verbosity mtu = args.mtu if args.remotes: if not os.path.isfile(args.remotes): eprint(f'File not found: {args.remotes}') stop_all_terminals() exit(1) with open(args.remotes) as file: args.remotes = [Remote.from_json(obj) for obj in json.load(file)] else: args.remotes = default_remotes if args.action == 'clear': clear(args.remotes) elif args.action == 'show': show(args.remotes) elif args.action == 'apply': apply(args.new_state, args.node_command, args.link_command, args.remotes) else: eprint(f'Invalid command: {args.action}') exit(1) stop_all_terminals()
def main(): parser = argparse.ArgumentParser() parser.add_argument('--verbosity', choices=['verbose', 'normal', 'quiet'], default='normal', help='Set verbosity.') parser.add_argument( '--remotes', help='Distribute nodes and links on remotes described in the JSON file.' ) parser.set_defaults(to_state=None) subparsers = parser.add_subparsers(dest='action', required=True, help='Action') parser_start = subparsers.add_parser( 'start', help='Run start script in every namespace.') parser_start.add_argument('protocol', help='Routing protocol script prefix.') parser_start.add_argument('to_state', nargs='?', default=None, help='To state') parser_stop = subparsers.add_parser( 'stop', help='Run stop script in every namespace.') parser_stop.add_argument('protocol', help='Routing protocol script prefix.') parser_stop.add_argument('to_state', nargs='?', default=None, help='To state') parser_change = subparsers.add_parser( 'apply', help='Run stop/start scripts in every namespace.') parser_change.add_argument('protocol', help='Routing protocol script prefix.') parser_change.add_argument('to_state', nargs='?', default=None, help='To state') parser_run = subparsers.add_parser( 'run', help='Execute any command in every namespace.') parser_run.add_argument( 'command', nargs=argparse.REMAINDER, help= 'Shell command that is run. Remote address and namespace id is added to call arguments.' ) parser_run.add_argument('to_state', nargs='?', default=None, help='To state') parser_copy = subparsers.add_parser('copy', help='Copy to all remotes.') parser_copy.add_argument('source', nargs='+') parser_copy.add_argument('destination') parser_clear = subparsers.add_parser( 'clear', help='Run all stop scripts in every namespaces.') args = parser.parse_args() if args.remotes: if not os.path.isfile(args.remotes): eprint(f'File not found: {args.remotes}') stop_all_terminals() exit(1) with open(args.remotes) as file: args.remotes = [Remote.from_json(obj) for obj in json.load(file)] else: args.remotes = default_remotes check_access(args.remotes) global verbosity verbosity = args.verbosity # get nodes that have been added or will be removed (old_ids, new_ids, rmap) = _get_update(args.to_state, args.remotes) if args.action == 'start': ids = new_ids if args.to_state else list(rmap.keys()) beg_ms = millis() _start_protocol(args.protocol, rmap, ids) end_ms = millis() if verbosity != 'quiet': print('started {} in {} namespaces in {}'.format( args.protocol, len(ids), format_duration(end_ms - beg_ms))) elif args.action == 'stop': ids = old_ids if args.to_state else list(rmap.keys()) beg_ms = millis() _stop_protocol(args.protocol, rmap, ids) end_ms = millis() if verbosity != 'quiet': print('stopped {} in {} namespaces in {}'.format( args.protocol, len(ids), format_duration(end_ms - beg_ms))) elif args.action == 'apply': beg_ms = millis() _stop_protocol(args.protocol, rmap, old_ids) _start_protocol(args.protocol, rmap, new_ids) end_ms = millis() if verbosity != 'quiet': print('applied {} in {} namespaces in {}'.format( args.protocol, len(rmap.keys()), format_duration(end_ms - beg_ms))) elif args.action == 'clear': beg_ms = millis() clear(args.remotes) end_ms = millis() if verbosity != 'quiet': print('cleared on {} remotes in {}'.format( len(args.remotes), format_duration(end_ms - beg_ms))) elif args.action == 'copy': beg_ms = millis() copy(args.remotes, args.source, args.destination) end_ms = millis() if verbosity != 'quiet': print('copied on {} remotes in {}'.format( len(args.remotes), format_duration(end_ms - beg_ms))) elif args.action == 'run': ids = new_ids if args.to_state else list(rmap.keys()) for id in ids: remote = rmap[id] label = remote.address or 'local' cmd = f'ip netns exec ns-{id} {" ".join(args.command)} {label} {id}' _exec_verbose(remote, cmd) else: eprint('Unknown action: {}'.format(args.action)) stop_all_terminals() exit(1) stop_all_terminals()