#!/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 # Settings CLIENT_CALLSIGN = "N0CALL-7" BEACON_CALLSIGN = "KI5QKX-10" # We expect the server to be beaconing from here # 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') # 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") 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()