188 lines
6.0 KiB
Python
188 lines
6.0 KiB
Python
#!/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()
|