# Network topology mapping tool for discovering and visualizing network segments. # Saves settings in `/home/bjorn/.settings_bjorn/yggdrasil_mapper_settings.json`. # Automatically loads saved settings if arguments are not provided. # -r, --range Network range to scan (CIDR format). # -i, --interface Network interface to use (default: active interface). # -d, --depth Maximum trace depth for routing (default: 5). # -o, --output Output directory (default: /home/bjorn/Bjorn/data/output/topology). # -t, --timeout Timeout for probes in seconds (default: 2). import os import json import argparse from datetime import datetime import logging import subprocess import networkx as nx import matplotlib.pyplot as plt import nmap import scapy.all as scapy from scapy.layers.inet import IP, ICMP, TCP import threading import queue b_class = "YggdrasilMapper" b_module = "yggdrasil_mapper" b_enabled = 0 # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # Default settings DEFAULT_OUTPUT_DIR = "/home/bjorn/Bjorn/data/output/topology" DEFAULT_SETTINGS_DIR = "/home/bjorn/.settings_bjorn" SETTINGS_FILE = os.path.join(DEFAULT_SETTINGS_DIR, "yggdrasil_mapper_settings.json") class YggdrasilMapper: def __init__(self, network_range, interface=None, max_depth=5, output_dir=DEFAULT_OUTPUT_DIR, timeout=2): self.network_range = network_range self.interface = interface or scapy.conf.iface self.max_depth = max_depth self.output_dir = output_dir self.timeout = timeout self.graph = nx.Graph() self.hosts = {} self.routes = {} self.lock = threading.Lock() # For parallel processing self.queue = queue.Queue() self.results = queue.Queue() def discover_hosts(self): """Discover live hosts in the network range.""" try: logging.info(f"Discovering hosts in {self.network_range}") # ARP scan for local network arp_request = scapy.ARP(pdst=self.network_range) broadcast = scapy.Ether(dst="ff:ff:ff:ff:ff:ff") packets = broadcast/arp_request answered, _ = scapy.srp(packets, timeout=self.timeout, iface=self.interface, verbose=False) for sent, received in answered: ip = received.psrc mac = received.hwsrc self.hosts[ip] = {'mac': mac, 'status': 'up'} logging.info(f"Discovered host: {ip} ({mac})") # Additional Nmap scan for service discovery nm = nmap.PortScanner() nm.scan(hosts=self.network_range, arguments=f'-sn -T4') for host in nm.all_hosts(): if host not in self.hosts: self.hosts[host] = {'status': 'up'} logging.info(f"Discovered host: {host}") except Exception as e: logging.error(f"Error discovering hosts: {e}") def trace_route(self, target): """Perform traceroute to a target.""" try: hops = [] for ttl in range(1, self.max_depth + 1): pkt = IP(dst=target, ttl=ttl)/ICMP() reply = scapy.sr1(pkt, timeout=self.timeout, verbose=False) if reply is None: continue if reply.src == target: hops.append(reply.src) break hops.append(reply.src) return hops except Exception as e: logging.error(f"Error tracing route to {target}: {e}") return [] def scan_ports(self, ip): """Scan common ports on a host.""" try: common_ports = [21, 22, 23, 25, 53, 80, 443, 445, 3389] open_ports = [] for port in common_ports: tcp_connect = IP(dst=ip)/TCP(dport=port, flags="S") response = scapy.sr1(tcp_connect, timeout=self.timeout, verbose=False) if response and response.haslayer(TCP): if response[TCP].flags == 0x12: # SYN-ACK open_ports.append(port) # Send RST to close connection rst = IP(dst=ip)/TCP(dport=port, flags="R") scapy.send(rst, verbose=False) return open_ports except Exception as e: logging.error(f"Error scanning ports for {ip}: {e}") return [] def worker(self): """Worker function for parallel processing.""" while True: try: task = self.queue.get() if task is None: break ip = task hops = self.trace_route(ip) ports = self.scan_ports(ip) self.results.queue.put({ 'ip': ip, 'hops': hops, 'ports': ports }) self.queue.task_done() except Exception as e: logging.error(f"Worker error: {e}") self.queue.task_done() def build_topology(self): """Build network topology by tracing routes and scanning hosts.""" try: # Start worker threads workers = [] for _ in range(5): # Number of parallel workers t = threading.Thread(target=self.worker) t.start() workers.append(t) # Add tasks to queue for ip in self.hosts.keys(): self.queue.put(ip) # Add None to queue to stop workers for _ in workers: self.queue.put(None) # Wait for all workers to complete for t in workers: t.join() # Process results while not self.results.empty(): result = self.results.get() ip = result['ip'] hops = result['hops'] ports = result['ports'] self.hosts[ip]['ports'] = ports if len(hops) > 1: self.routes[ip] = hops # Add nodes and edges to graph self.graph.add_node(ip, **self.hosts[ip]) for i in range(len(hops) - 1): self.graph.add_edge(hops[i], hops[i + 1]) except Exception as e: logging.error(f"Error building topology: {e}") def generate_visualization(self): """Generate network topology visualization.""" try: plt.figure(figsize=(12, 8)) # Position nodes using spring layout pos = nx.spring_layout(self.graph) # Draw nodes nx.draw_networkx_nodes(self.graph, pos, node_size=500) # Draw edges nx.draw_networkx_edges(self.graph, pos) # Add labels labels = {} for node in self.graph.nodes(): label = f"{node}\n" if 'ports' in self.hosts[node]: label += f"Ports: {', '.join(map(str, self.hosts[node]['ports']))}" labels[node] = label nx.draw_networkx_labels(self.graph, pos, labels, font_size=8) # Save visualization timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") viz_path = os.path.join(self.output_dir, f"topology_{timestamp}.png") plt.savefig(viz_path) plt.close() logging.info(f"Visualization saved to {viz_path}") except Exception as e: logging.error(f"Error generating visualization: {e}") def save_results(self): """Save topology data to JSON file.""" try: os.makedirs(self.output_dir, exist_ok=True) timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") results = { 'timestamp': datetime.now().isoformat(), 'network_range': self.network_range, 'hosts': self.hosts, 'routes': self.routes, 'topology': { 'nodes': list(self.graph.nodes()), 'edges': list(self.graph.edges()) } } output_file = os.path.join(self.output_dir, f"topology_{timestamp}.json") with open(output_file, 'w') as f: json.dump(results, f, indent=4) logging.info(f"Results saved to {output_file}") except Exception as e: logging.error(f"Failed to save results: {e}") def execute(self): """Execute the network mapping process.""" try: logging.info(f"Starting network mapping of {self.network_range}") # Discovery phase self.discover_hosts() if not self.hosts: logging.error("No hosts discovered") return # Topology building phase self.build_topology() # Generate outputs self.generate_visualization() self.save_results() logging.info("Network mapping completed") except Exception as e: logging.error(f"Error during execution: {e}") def save_settings(network_range, interface, max_depth, output_dir, timeout): """Save settings to JSON file.""" try: os.makedirs(DEFAULT_SETTINGS_DIR, exist_ok=True) settings = { "network_range": network_range, "interface": interface, "max_depth": max_depth, "output_dir": output_dir, "timeout": timeout } with open(SETTINGS_FILE, 'w') as f: json.dump(settings, f) logging.info(f"Settings saved to {SETTINGS_FILE}") except Exception as e: logging.error(f"Failed to save settings: {e}") def load_settings(): """Load settings from JSON file.""" if os.path.exists(SETTINGS_FILE): try: with open(SETTINGS_FILE, 'r') as f: return json.load(f) except Exception as e: logging.error(f"Failed to load settings: {e}") return {} def main(): parser = argparse.ArgumentParser(description="Network topology mapping tool") parser.add_argument("-r", "--range", help="Network range to scan (CIDR)") parser.add_argument("-i", "--interface", help="Network interface to use") parser.add_argument("-d", "--depth", type=int, default=5, help="Maximum trace depth") parser.add_argument("-o", "--output", default=DEFAULT_OUTPUT_DIR, help="Output directory") parser.add_argument("-t", "--timeout", type=int, default=2, help="Timeout for probes") args = parser.parse_args() settings = load_settings() network_range = args.range or settings.get("network_range") interface = args.interface or settings.get("interface") max_depth = args.depth or settings.get("max_depth") output_dir = args.output or settings.get("output_dir") timeout = args.timeout or settings.get("timeout") if not network_range: logging.error("Network range is required. Use -r or save it in settings") return save_settings(network_range, interface, max_depth, output_dir, timeout) mapper = YggdrasilMapper( network_range=network_range, interface=interface, max_depth=max_depth, output_dir=output_dir, timeout=timeout ) mapper.execute() if __name__ == "__main__": main()