#!/usr/bin/env python3 import time import threading import os import logging import logging.handlers import socket import sys import ax25 import ax25.ports import ax25.socket import subprocess # Settings CLIENT_CALLSIGN = "N0CALL-7" BEACON_CALLSIGN = "KI5QKX-10" # We expect the server to be beaconing from here AX_IFACE = "ax0" # Missing from 'socket' ETH_P_AX25 = 2 ETH_P_ALL = 3 # Setup logging os.makedirs("logs", exist_ok=True) logger = logging.getLogger("craprniac_client") logger.setLevel(logging.DEBUG) log_handler = logging.handlers.TimedRotatingFileHandler( "logs/craprniac_client.log", when="midnight", interval=1, backupCount=7 ) log_handler.setFormatter(logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s" )) logger.addHandler(log_handler) console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s" )) logger.addHandler(console_handler) # Globals for network state configured = False lease_expiration = None # Packet builders def build_request(network_name): return f"0.1|CRAP_REQUEST|{CLIENT_CALLSIGN}|{network_name}".encode('utf-8') # Configure network def interface_needs_update(ip_address, netmask, iface=AX_IFACE): try: result = subprocess.run( ["ip", "-4", "addr", "show", "dev", iface], capture_output=True, text=True, check=True ) output = result.stdout logger.debug(f"Interface status:\n{output}") cidr_ip = f"{ip_address}/{netmask}" return cidr_ip not in output except subprocess.CalledProcessError as e: logger.error(f"Error checking interface: {e}") return True # Assume needs update if we can't tell def configure_network_interface(ip_address, netmask, gateway, dns_server, iface=AX_IFACE): try: if interface_needs_update(ip_address, netmask, iface): logger.info("Configuring network interface...") # Flush existing addresses subprocess.run(["ip", "addr", "flush", "dev", iface], check=True) # Add new IP cidr_ip = f"{ip_address}/{netmask}" subprocess.run(["ip", "addr", "add", cidr_ip, "dev", iface], check=True) # Set up route # subprocess.run(["ip", "route", "add", "default", "via", gateway, "dev", iface], check=True) # Update DNS # with open("/etc/resolv.conf", "w") as resolv: # resolv.write(f"nameserver {dns_server}\n") logger.info(f"Network interface {iface} configured successfully.") else: logger.info(f"Interface {iface} already configured with {ip_address}/{netmask}. No changes needed.") except subprocess.CalledProcessError as e: logger.error(f"Failed to configure network: {e}") except Exception as e: logger.error(f"Unexpected error during network config: {e}") # Apply network configuration (mock) def apply_network_config(assigned_ip, gateway, dns, lease_time): global configured, lease_expiration logger.info(f"Applying network config:") logger.info(f" Assigned IP: {assigned_ip}") # logger.info(f" Gateway: {gateway}") # logger.info(f" DNS Server: {dns}") logger.info(f" Lease Time: {lease_time} seconds") configure_network_interface( ip_address=assigned_ip.split('/')[0], netmask=assigned_ip.split('/')[1] if '/' in assigned_ip else "24", gateway=gateway, dns_server=dns ) configured = True lease_expiration = time.time() + int(lease_time) # Reset network configuration (mock) def reset_network_config(): global configured, lease_expiration logger.info("Resetting network configuration (lease expired)") configured = False lease_expiration = None # Main Client Logic def main(): # Setup DGRAM socket to listen for beacons try: beacon_sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_AX25)) logger.info(f"Client {CLIENT_CALLSIGN} started and waiting for beacons...") except: print("Unable to open listener socket. Are you root?") sys.exit(1) while True: try: if configured and lease_expiration and time.time() >= lease_expiration: reset_network_config() data, src_callsign = beacon_sock.recvfrom(1024) # recvfrom, not recv logger.debug(f"Received: {data}") frame = ax25.Frame.unpack(data[1:]) logger.debug(f"Unpacked: {frame}") decoded = frame.data.decode('utf-8') logger.debug(f"Data: {decoded}") parts = decoded.split('|') if len(parts) < 2: continue if parts[1] == "CRAP_BEACON" and not configured: base_callsign = parts[2] network_name = parts[3] logger.info(f"Received beacon from {base_callsign} (network {network_name})") # Now create a connection socket for CRAP_REQUEST logger.debug(f"Connecting to {base_callsign}") session_sock = ax25.socket.Socket() # SOCK_STREAM by default session_sock.bind(CLIENT_CALLSIGN) session_sock.connect(base_callsign) session_sock.send(build_request(network_name)) logger.info(f"Sent CRAP_REQUEST to {base_callsign}") response = session_sock.recv(1024) parts = response.decode('utf-8').split('|') if parts[1] == "CRAP_ACCEPT": assigned_ip = parts[4] gateway = parts[5] dns = parts[6] lease_time = parts[7] logger.info(f"Received CRAP_ACCEPT from {base_callsign}") apply_network_config(assigned_ip, gateway, dns, lease_time) session_sock.close() except Exception as e: logger.error(f"Client error: {e}") time.sleep(5) if __name__ == "__main__": main()