Files
HyperionOS/wip/SNIP/server/main.py

168 lines
4.9 KiB
Python

import asyncio
import struct
import ipaddress
import websockets
HOST = "0.0.0.0"
PORT = 8765
ENABLE_PACKET_LOGGING = True
ENABLE_IP_INFO_LOGGING = True
NETWORK = ipaddress.IPv4Network("0.0.0.0/0")
STATIC_IPS: set[int] = set()
clients: dict[int, websockets.WebSocketServerProtocol] = {}
leases: dict[websockets.WebSocketServerProtocol, int] = {}
clients_lock = asyncio.Lock()
leases_lock = asyncio.Lock()
DATA_HEADER = "!4s4sI"
DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER)
NET_START = int(NETWORK.network_address) + 1
NET_END = int(NETWORK.broadcast_address) - 1
def int_to_bytes(ip: int) -> bytes:
return ip.to_bytes(4, "big")
def bytes_to_int(b: bytes) -> int:
return int.from_bytes(b, "big")
def allocate_dhcp_ip() -> int | None:
"""Lazy allocation of first free IP."""
for ip in range(NET_START, NET_END + 1):
if ip not in clients and ip not in STATIC_IPS:
return ip
return None
async def safe_send(ws: websockets.WebSocketServerProtocol, msg: bytes):
"""Fire-and-forget safe send with cleanup."""
try:
await ws.send(msg)
except websockets.ConnectionClosed:
async with leases_lock:
ip = leases.pop(ws, None)
if ip is not None:
async with clients_lock:
clients.pop(ip, None)
if ENABLE_IP_INFO_LOGGING:
print(f"Released {ipaddress.IPv4Address(ip)} (peer closed)")
async def debug():
while True:
print(len(clients))
await asyncio.sleep(1)
async def handle_websocket(ws: websockets.WebSocketServerProtocol):
try:
async for msg in ws:
if not msg:
continue
if not isinstance(msg, bytes):
msg = msg.encode()
opcode = msg[0]
# --------------------
# IP REQUEST
# --------------------
if opcode == 0x01:
async with leases_lock:
if ws in leases:
continue
flags = msg[1]
static = False
if flags == 0x00: # DHCP
ip = allocate_dhcp_ip()
if ip is None:
await ws.send(b"\x02\x02\x00\x00\x00\x00")
continue
elif flags == 0x01 and len(msg) == 6:
requested = bytes_to_int(msg[2:6])
if requested not in STATIC_IPS:
await ws.send(b"\x02\x01\x00\x00\x00\x00")
continue
async with clients_lock:
if requested in clients:
await ws.send(b"\x02\x01\x00\x00\x00\x00")
continue
ip = requested
static = True
else:
continue
async with leases_lock:
leases[ws] = ip
async with clients_lock:
clients[ip] = ws
await ws.send(b"\x02\x00" + int_to_bytes(ip))
label = "static" if static else "dynamic"
if ENABLE_IP_INFO_LOGGING:
print(f"Assigned {label} {ipaddress.IPv4Address(ip)}")
# --------------------
# DATA PLANE
# --------------------
elif opcode == 0x10:
async with leases_lock:
if ws not in leases:
continue
if len(msg) < 1 + DATA_HEADER_SIZE:
continue
try:
src_b, dst_b, length = struct.unpack(
DATA_HEADER, msg[1:1 + DATA_HEADER_SIZE]
)
except struct.error:
continue
payload = msg[1 + DATA_HEADER_SIZE:]
if len(payload) != length:
continue
src = bytes_to_int(src_b)
dst = bytes_to_int(dst_b)
if ENABLE_PACKET_LOGGING:
print(f"{ipaddress.IPv4Address(src)} > {ipaddress.IPv4Address(dst)}: {payload}")
async with clients_lock:
receiver = clients.get(dst)
if receiver:
# Fire-and-forget send
asyncio.create_task(safe_send(receiver, msg))
except websockets.ConnectionClosed:
pass
finally:
async with leases_lock:
ip = leases.pop(ws, None)
if ip is not None:
async with clients_lock:
clients.pop(ip, None)
if ENABLE_IP_INFO_LOGGING:
print(f"Released {ipaddress.IPv4Address(ip)}")
async def main():
async with websockets.serve(handle_websocket, HOST, PORT, max_size=2**20, max_queue=65535):
print(f"IPoW router listening on {PORT}")
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())