#!/usr/bin/env python3 """ Routing Manager for Personal Internet Cell Handles VPN gateway, NAT, iptables, and advanced routing """ import os import json import subprocess import logging import ipaddress from datetime import datetime from typing import Dict, List, Optional, Tuple, Any import re from base_service_manager import BaseServiceManager logger = logging.getLogger(__name__) class RoutingManager(BaseServiceManager): """Manages VPN gateway, NAT, and routing functionality""" def __init__(self, data_dir: str = '/app/data', config_dir: str = '/app/config'): super().__init__('routing', data_dir, config_dir) self.routing_dir = os.path.join(config_dir, 'routing') self.rules_file = os.path.join(data_dir, 'routing', 'rules.json') # Ensure directories exist os.makedirs(self.routing_dir, exist_ok=True) os.makedirs(os.path.dirname(self.rules_file), exist_ok=True) # Initialize routing configuration self._ensure_config_exists() def _ensure_config_exists(self): """Ensure routing configuration exists""" if not os.path.exists(self.rules_file): self._initialize_rules() def _initialize_rules(self): """Initialize routing rules""" default_rules = { 'nat_rules': [], 'forwarding_rules': [], 'peer_routes': {}, 'exit_nodes': [], 'bridge_routes': [], 'split_routes': [], 'firewall_rules': [] } with open(self.rules_file, 'w') as f: json.dump(default_rules, f, indent=2) logger.info("Routing rules initialized") def _validate_cidr(self, cidr): import ipaddress try: ipaddress.ip_network(cidr) return True except Exception: return False def add_nat_rule(self, source_network: str, target_interface: str, masquerade: bool = True, nat_type: str = 'MASQUERADE', protocol: str = 'ALL', external_port: str = None, internal_ip: str = None, internal_port: str = None) -> bool: """Add NAT rule for network translation, port forwarding, or 1:1 NAT.""" # Validation if not source_network or not self._validate_cidr(source_network): logger.error(f"Invalid source_network: {source_network}") return False if not target_interface or not isinstance(target_interface, str): logger.error(f"Invalid target_interface: {target_interface}") return False if nat_type not in ['MASQUERADE', 'SNAT', 'DNAT']: logger.error(f"Invalid nat_type: {nat_type}") return False if protocol not in ['TCP', 'UDP', 'ALL']: logger.error(f"Invalid protocol: {protocol}") return False try: rules = self._load_rules() nat_rule = { 'id': f"nat_{len(rules['nat_rules']) + 1}", 'source_network': source_network, 'target_interface': target_interface, 'masquerade': masquerade, 'nat_type': nat_type, 'protocol': protocol, 'external_port': external_port, 'internal_ip': internal_ip, 'internal_port': internal_port, 'enabled': True, 'created_at': datetime.now().isoformat() } rules['nat_rules'].append(nat_rule) self._save_rules(rules) self._apply_nat_rule(nat_rule) logger.info(f"Added NAT rule for {source_network} -> {target_interface} type={nat_type}") return True except Exception as e: logger.error(f"Failed to add NAT rule: {e}") return False def remove_nat_rule(self, rule_id: str) -> bool: """Remove NAT rule""" try: rules = self._load_rules() # Find and remove rule rules['nat_rules'] = [rule for rule in rules['nat_rules'] if rule['id'] != rule_id] self._save_rules(rules) # Remove from iptables self._remove_nat_rule(rule_id) logger.info(f"Removed NAT rule {rule_id}") return True except Exception as e: logger.error(f"Failed to remove NAT rule: {e}") return False def add_peer_route(self, peer_name: str, peer_ip: str, allowed_networks: list, route_type: str = 'lan') -> bool: """Add routing rule for a peer""" # Validation if not peer_name or not isinstance(peer_name, str): logger.error(f"Invalid peer_name: {peer_name}") return False if not peer_ip or not isinstance(peer_ip, str): logger.error(f"Invalid peer_ip: {peer_ip}") return False if not allowed_networks or not isinstance(allowed_networks, list) or not all(self._validate_cidr(n) for n in allowed_networks): logger.error(f"Invalid allowed_networks: {allowed_networks}") return False if route_type not in ['lan', 'exit', 'bridge', 'split']: logger.error(f"Invalid route_type: {route_type}") return False try: rules = self._load_rules() peer_route = { 'peer_name': peer_name, 'peer_ip': peer_ip, 'allowed_networks': allowed_networks, 'route_type': route_type, 'enabled': True, 'created_at': datetime.now().isoformat() } rules['peer_routes'][peer_name] = peer_route self._save_rules(rules) self._apply_peer_route(peer_route) logger.info(f"Added peer route for {peer_name}") return True except Exception as e: logger.error(f"Failed to add peer route: {e}") return False def remove_peer_route(self, peer_name: str) -> bool: """Remove routing rule for a peer""" try: rules = self._load_rules() if peer_name in rules['peer_routes']: del rules['peer_routes'][peer_name] self._save_rules(rules) # Remove from routing table self._remove_peer_route(peer_name) logger.info(f"Removed peer route for {peer_name}") return True return False except Exception as e: logger.error(f"Failed to remove peer route: {e}") return False def add_exit_node(self, peer_name: str, peer_ip: str, allowed_domains: List[str] = None) -> bool: """Add exit node configuration""" try: rules = self._load_rules() exit_node = { 'peer_name': peer_name, 'peer_ip': peer_ip, 'allowed_domains': allowed_domains or [], 'enabled': True, 'created_at': datetime.now().isoformat() } rules['exit_nodes'].append(exit_node) self._save_rules(rules) # Apply exit node rules self._apply_exit_node(exit_node) logger.info(f"Added exit node {peer_name}") return True except Exception as e: logger.error(f"Failed to add exit node: {e}") return False def add_bridge_route(self, source_peer: str, target_peer: str, allowed_networks: List[str]) -> bool: """Add bridge route between peers""" try: rules = self._load_rules() bridge_route = { 'id': f"bridge_{len(rules['bridge_routes']) + 1}", 'source_peer': source_peer, 'target_peer': target_peer, 'allowed_networks': allowed_networks, 'enabled': True, 'created_at': datetime.now().isoformat() } rules['bridge_routes'].append(bridge_route) self._save_rules(rules) # Apply bridge route self._apply_bridge_route(bridge_route) logger.info(f"Added bridge route {source_peer} -> {target_peer}") return True except Exception as e: logger.error(f"Failed to add bridge route: {e}") return False def add_split_route(self, network: str, exit_peer: str, fallback_peer: str = None) -> bool: """Add split routing rule""" try: rules = self._load_rules() split_route = { 'id': f"split_{len(rules['split_routes']) + 1}", 'network': network, 'exit_peer': exit_peer, 'fallback_peer': fallback_peer, 'enabled': True, 'created_at': datetime.now().isoformat() } rules['split_routes'].append(split_route) self._save_rules(rules) # Apply split route self._apply_split_route(split_route) logger.info(f"Added split route for {network}") return True except Exception as e: logger.error(f"Failed to add split route: {e}") return False def add_firewall_rule(self, rule_type: str, source: str, destination: str, action: str = 'ACCEPT', port: str = None, protocol: str = 'ALL', port_range: str = None) -> bool: """Add firewall rule with protocol and port range support.""" # Validation if rule_type not in ['INPUT', 'OUTPUT', 'FORWARD']: logger.error(f"Invalid rule_type: {rule_type}") return False if not source or not self._validate_cidr(source): logger.error(f"Invalid source: {source}") return False if not destination or not self._validate_cidr(destination): logger.error(f"Invalid destination: {destination}") return False if action not in ['ACCEPT', 'DROP', 'REJECT']: logger.error(f"Invalid action: {action}") return False if protocol not in ['TCP', 'UDP', 'ICMP', 'ALL']: logger.error(f"Invalid protocol: {protocol}") return False if port is not None and port != '': try: port_num = int(port) if not (0 < port_num < 65536): logger.error(f"Invalid port: {port}") return False except Exception: logger.error(f"Invalid port: {port}") return False if port_range is not None and port_range != '': # Validate port range format (e.g., 1000-2000) if not re.match(r'^\d{1,5}-\d{1,5}$', port_range): logger.error(f"Invalid port_range: {port_range}") return False try: rules = self._load_rules() firewall_rule = { 'id': f"fw_{len(rules['firewall_rules']) + 1}", 'rule_type': rule_type, 'source': source, 'destination': destination, 'action': action, 'port': port, 'protocol': protocol, 'port_range': port_range, 'enabled': True, 'created_at': datetime.now().isoformat() } rules['firewall_rules'].append(firewall_rule) self._save_rules(rules) self._apply_firewall_rule(firewall_rule) logger.info(f"Added firewall rule {rule_type} {source} -> {destination} proto={protocol}") return True except Exception as e: logger.error(f"Failed to add firewall rule: {e}") return False def get_routing_status(self) -> Dict: """Get routing and gateway status""" try: rules = self._load_rules() # Get iptables status nat_rules_count = len([r for r in rules['nat_rules'] if r['enabled']]) firewall_rules_count = len([r for r in rules['firewall_rules'] if r['enabled']]) peer_routes_count = len([r for r in rules['peer_routes'].values() if r['enabled']]) exit_nodes_count = len([r for r in rules['exit_nodes'] if r['enabled']]) # Get routing table info routing_table = self._get_routing_table() return { 'nat_rules_count': nat_rules_count, 'firewall_rules_count': firewall_rules_count, 'peer_routes_count': peer_routes_count, 'exit_nodes_count': exit_nodes_count, 'bridge_routes_count': len(rules['bridge_routes']), 'split_routes_count': len(rules['split_routes']), 'routing_table': routing_table, 'active_rules': rules } except Exception as e: logger.error(f"Failed to get routing status: {e}") return { 'nat_rules_count': 0, 'firewall_rules_count': 0, 'peer_routes_count': 0, 'exit_nodes_count': 0, 'bridge_routes_count': 0, 'split_routes_count': 0, 'routing_table': [], 'active_rules': {} } def test_routing_connectivity(self, target_ip: str, via_peer: str = None) -> Dict: """Test routing connectivity""" try: results = {} # Test basic connectivity try: result = subprocess.run(['ping', '-c', '3', '-W', '5', target_ip], capture_output=True, text=True, timeout=30) results['ping'] = { 'success': result.returncode == 0, 'output': result.stdout, 'error': result.stderr } except Exception as e: results['ping'] = { 'success': False, 'output': '', 'error': str(e) } # Test traceroute try: result = subprocess.run(['traceroute', '-m', '10', target_ip], capture_output=True, text=True, timeout=30) results['traceroute'] = { 'success': result.returncode == 0, 'output': result.stdout, 'error': result.stderr } except Exception as e: results['traceroute'] = { 'success': False, 'output': '', 'error': str(e) } # Test specific route if via_peer is specified if via_peer: try: # Test route through specific peer result = subprocess.run(['ping', '-c', '3', '-W', '5', '-I', via_peer, target_ip], capture_output=True, text=True, timeout=30) results['peer_route'] = { 'success': result.returncode == 0, 'output': result.stdout, 'error': result.stderr } except Exception as e: results['peer_route'] = { 'success': False, 'output': '', 'error': str(e) } return results except Exception as e: return { 'ping': {'success': False, 'output': '', 'error': str(e)}, 'traceroute': {'success': False, 'output': '', 'error': str(e)} } def get_routing_logs(self, lines: int = 50) -> Dict: """Get routing and firewall logs""" try: logs = {} # Get iptables logs try: result = subprocess.run(['dmesg', '|', 'grep', 'iptables'], capture_output=True, text=True, timeout=10) logs['iptables'] = result.stdout except Exception as e: logs['iptables'] = f"Error getting iptables logs: {e}" # Get routing logs try: result = subprocess.run(['dmesg', '|', 'grep', 'routing'], capture_output=True, text=True, timeout=10) logs['routing'] = result.stdout except Exception as e: logs['routing'] = f"Error getting routing logs: {e}" # Get network interface logs try: result = subprocess.run(['ip', 'route', 'show'], capture_output=True, text=True, timeout=10) logs['routes'] = result.stdout except Exception as e: logs['routes'] = f"Error getting route table: {e}" return logs except Exception as e: logger.error(f"Failed to get routing logs: {e}") return {'error': str(e)} def get_nat_rules(self): """Return all NAT rules.""" rules = self._load_rules() return rules.get('nat_rules', []) def get_peer_routes(self): """Return all peer routes as a list.""" rules = self._load_rules() # peer_routes is a dict keyed by peer_name return list(rules.get('peer_routes', {}).values()) def get_firewall_rules(self): """Return all firewall rules.""" rules = self._load_rules() return rules.get('firewall_rules', []) def update_peer_ip(self, peer_name: str, new_ip: str) -> bool: """Update peer IP in all routes and re-apply them.""" try: rules = self._load_rules() updated = False if 'peer_routes' in rules and peer_name in rules['peer_routes']: rules['peer_routes'][peer_name]['peer_ip'] = new_ip self._save_rules(rules) self._apply_peer_route(rules['peer_routes'][peer_name]) updated = True # Optionally update exit_nodes, bridge_routes, split_routes if needed return updated except Exception as e: logger.error(f"Failed to update peer IP in routing: {e}") return False def get_status(self) -> Dict[str, Any]: """Get routing service status""" try: routing_status = self.get_routing_status() rules = self._load_rules() status = { 'running': routing_status.get('running', False), 'status': 'online' if routing_status.get('running', False) else 'offline', 'routing_status': routing_status, 'nat_rules_count': len(rules.get('nat_rules', [])), 'peer_routes_count': len(rules.get('peer_routes', {})), 'exit_nodes_count': len(rules.get('exit_nodes', [])), 'firewall_rules_count': len(rules.get('firewall_rules', [])), 'timestamp': datetime.utcnow().isoformat() } return status except Exception as e: return self.handle_error(e, "get_status") def test_connectivity(self) -> Dict[str, Any]: """Test routing service connectivity""" try: # Test basic routing functionality routing_test = self._test_routing_functionality() # Test iptables access iptables_test = self._test_iptables_access() # Test network interfaces interfaces_test = self._test_network_interfaces() # Test routing table access routing_table_test = self._test_routing_table_access() results = { 'routing_functionality': routing_test, 'iptables_access': iptables_test, 'network_interfaces': interfaces_test, 'routing_table_access': routing_table_test, 'success': routing_test.get('success', False) and iptables_test.get('success', False), 'timestamp': datetime.utcnow().isoformat() } return results except Exception as e: return self.handle_error(e, "test_connectivity") def _test_routing_functionality(self) -> Dict[str, Any]: """Test basic routing functionality""" try: # Test if we can read routing rules rules = self._load_rules() # Test if we can access routing status routing_status = self.get_routing_status() return { 'success': True, 'message': 'Routing functionality working', 'rules_loaded': bool(rules), 'status_accessible': bool(routing_status) } except Exception as e: return { 'success': False, 'message': f'Routing functionality test failed: {str(e)}', 'error': str(e) } def _test_iptables_access(self) -> Dict[str, Any]: """Test iptables access""" try: # Test if we can list iptables rules result = subprocess.run(['iptables', '-L', '-n'], capture_output=True, text=True, timeout=10) if result.returncode == 0: return { 'success': True, 'message': 'iptables access working', 'rules_count': len([line for line in result.stdout.split('\n') if line.strip()]) } else: return { 'success': False, 'message': f'iptables access failed: {result.stderr}', 'error': result.stderr } except Exception as e: return { 'success': False, 'message': f'iptables access test failed: {str(e)}', 'error': str(e) } def _test_network_interfaces(self) -> Dict[str, Any]: """Test network interfaces access""" try: # Test if we can list network interfaces result = subprocess.run(['ip', 'link', 'show'], capture_output=True, text=True, timeout=10) if result.returncode == 0: interfaces = [line.strip() for line in result.stdout.split('\n') if line.strip()] return { 'success': True, 'message': 'Network interfaces accessible', 'interfaces_count': len(interfaces) } else: return { 'success': False, 'message': f'Network interfaces access failed: {result.stderr}', 'error': result.stderr } except Exception as e: return { 'success': False, 'message': f'Network interfaces test failed: {str(e)}', 'error': str(e) } def _test_routing_table_access(self) -> Dict[str, Any]: """Test routing table access""" try: # Test if we can read routing table result = subprocess.run(['ip', 'route', 'show'], capture_output=True, text=True, timeout=10) if result.returncode == 0: routes = [line.strip() for line in result.stdout.split('\n') if line.strip()] return { 'success': True, 'message': 'Routing table accessible', 'routes_count': len(routes) } else: return { 'success': False, 'message': f'Routing table access failed: {result.stderr}', 'error': result.stderr } except Exception as e: return { 'success': False, 'message': f'Routing table test failed: {str(e)}', 'error': str(e) } def _load_rules(self) -> Dict: """Load routing rules from file""" try: with open(self.rules_file, 'r') as f: return json.load(f) except Exception as e: logger.error(f"Failed to load routing rules: {e}") return {} def _save_rules(self, rules: Dict): """Save routing rules to file""" try: with open(self.rules_file, 'w') as f: json.dump(rules, f, indent=2) except Exception as e: logger.error(f"Failed to save routing rules: {e}") def _apply_nat_rule(self, rule: Dict): """Apply NAT rule to iptables, supporting MASQUERADE, SNAT, DNAT, and port forwarding.""" try: if rule.get('nat_type', 'MASQUERADE') == 'MASQUERADE' and rule['masquerade']: cmd = [ 'iptables', '-t', 'nat', '-A', 'POSTROUTING', '-s', rule['source_network'], '-o', rule['target_interface'], '-j', 'MASQUERADE' ] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied MASQUERADE NAT rule: {rule['source_network']} -> {rule['target_interface']}") elif rule.get('nat_type') == 'DNAT' and rule['internal_ip']: # Port forwarding (DNAT) cmd = [ 'iptables', '-t', 'nat', '-A', 'PREROUTING', '-d', rule['source_network'], ] if rule.get('protocol') and rule['protocol'] != 'ALL': cmd += ['-p', rule['protocol'].lower()] if rule.get('external_port'): cmd += ['--dport', str(rule['external_port'])] cmd += ['-j', 'DNAT', '--to-destination', f"{rule['internal_ip']}{':' + str(rule['internal_port']) if rule.get('internal_port') else ''}"] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied DNAT rule: {rule['source_network']}:{rule.get('external_port')} -> {rule['internal_ip']}:{rule.get('internal_port')}") elif rule.get('nat_type') == 'SNAT' and rule['internal_ip']: # 1:1 NAT (SNAT) cmd = [ 'iptables', '-t', 'nat', '-A', 'POSTROUTING', '-s', rule['internal_ip'], ] if rule.get('protocol') and rule['protocol'] != 'ALL': cmd += ['-p', rule['protocol'].lower()] if rule.get('internal_port'): cmd += ['--sport', str(rule['internal_port'])] cmd += ['-j', 'SNAT', '--to-source', rule['source_network']] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied SNAT rule: {rule['internal_ip']} -> {rule['source_network']}") except Exception as e: logger.error(f"Failed to apply NAT rule: {e}") def _remove_nat_rule(self, rule_id: str): """Remove NAT rule from iptables""" try: # This is a simplified removal - in practice you'd need to track the exact rule cmd = ['iptables', '-t', 'nat', '-F', 'POSTROUTING'] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Removed NAT rule: {rule_id}") except Exception as e: logger.error(f"Failed to remove NAT rule: {e}") def _apply_peer_route(self, route: Dict): """Apply peer routing rule""" try: # Add route for peer networks for network in route['allowed_networks']: cmd = [ 'ip', 'route', 'add', network, 'via', route['peer_ip'], 'dev', 'wg0' ] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied peer route for {route['peer_name']}") except Exception as e: logger.error(f"Failed to apply peer route: {e}") def _remove_peer_route(self, peer_name: str): """Remove peer routing rule""" try: # Remove routes for this peer cmd = ['ip', 'route', 'del', 'via', peer_name, 'dev', 'wg0'] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Removed peer route for {peer_name}") except Exception as e: logger.error(f"Failed to remove peer route: {e}") def _apply_exit_node(self, exit_node: Dict): """Apply exit node configuration""" try: # Add default route through exit node cmd = [ 'ip', 'route', 'add', 'default', 'via', exit_node['peer_ip'], 'dev', 'wg0' ] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied exit node {exit_node['peer_name']}") except Exception as e: logger.error(f"Failed to apply exit node: {e}") def _apply_bridge_route(self, route: Dict): """Apply bridge routing rule""" try: # Add forwarding rules for bridge for network in route['allowed_networks']: cmd = [ 'iptables', '-A', 'FORWARD', '-s', network, '-d', route['target_peer'], '-j', 'ACCEPT' ] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied bridge route {route['source_peer']} -> {route['target_peer']}") except Exception as e: logger.error(f"Failed to apply bridge route: {e}") def _apply_split_route(self, route: Dict): """Apply split routing rule""" try: # Add specific route for network cmd = [ 'ip', 'route', 'add', route['network'], 'via', route['exit_peer'], 'dev', 'wg0' ] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied split route for {route['network']}") except Exception as e: logger.error(f"Failed to apply split route: {e}") def _apply_firewall_rule(self, rule: Dict): """Apply firewall rule with protocol and port range support.""" try: cmd = [ 'iptables', '-A', rule['rule_type'], '-s', rule['source'], '-d', rule['destination'] ] if rule.get('protocol') and rule['protocol'] != 'ALL': cmd += ['-p', rule['protocol'].lower()] if rule.get('port'): cmd += ['--dport', str(rule['port'])] if rule.get('port_range'): cmd += ['--dport', rule['port_range'].replace('-', ':')] cmd += ['-j', rule['action']] subprocess.run(cmd, check=True, timeout=10) logger.info(f"Applied firewall rule {rule['rule_type']} proto={rule.get('protocol')} port={rule.get('port') or rule.get('port_range')}") except Exception as e: logger.error(f"Failed to apply firewall rule: {e}") def _get_routing_table(self) -> List[Dict]: """Get current routing table""" try: result = subprocess.run(['ip', 'route', 'show'], capture_output=True, text=True, timeout=10) routes = [] for line in result.stdout.strip().split('\n'): if line.strip(): routes.append({ 'route': line.strip(), 'parsed': self._parse_route(line.strip()) }) return routes except Exception as e: logger.error(f"Failed to get routing table: {e}") return [] def _parse_route(self, route_line: str) -> Dict: """Parse route line into components""" try: # Simple route parsing - can be enhanced parts = route_line.split() parsed = { 'destination': parts[0] if parts else '', 'via': '', 'dev': '', 'metric': '' } for i, part in enumerate(parts): if part == 'via' and i + 1 < len(parts): parsed['via'] = parts[i + 1] elif part == 'dev' and i + 1 < len(parts): parsed['dev'] = parts[i + 1] elif part == 'metric' and i + 1 < len(parts): parsed['metric'] = parts[i + 1] return parsed except Exception as e: logger.error(f"Failed to parse route: {e}") return {'destination': route_line, 'via': '', 'dev': '', 'metric': ''}