def __init__(self, config: NetworkConfig, loop: AbstractEventLoop): self.config = config self.sm = NetRomStateMachine(self, AsyncioTimer) self.router = NetRomRoutingTable(config.node_alias()) self.l3_apps: Dict[AX25Call, str] = {} # Mapping of destination addresses to protocol factory. When a new connection is made to the destination # we will create a new instance of the protocol as well as a NetRom transport and add it to l3_connections self.l3_servers: Dict[AX25Call, Callable[[], Protocol]] = {} # This is a mapping of local circuit IDs to (transport, protocol). When incoming data is handled for a # circuit, this is how we pass it on to the instance of the protocol self.l3_connections: Dict[int, Tuple[Transport, Protocol]] = {} # This is a mapping of local circuit ID to a protocol factory. These protocols do not yet have a transport and # are in a half-opened state. Once a connect ack is received, a transport will be created and these will be # migrated to l3_connections self.l3_half_open: Dict[int, Callable[[], Protocol]] = {} self.data_links: Dict[int, DataLinkManager] = {} self.route_lock = Lock() self.loop = loop def extra(): return f"[L4 Call={str(config.node_call())} Alias={config.node_alias()}]" LoggingMixin.__init__(self, logging.getLogger("main"), extra)
def __init__(self, config: NetworkConfig, l3_protocol: L3Protocol, scheduler: Scheduler): self.config = config self.sm = NetRomStateMachine(self, scheduler.timer) # Mapping of destination addresses to protocol factory. When a new connection is made to the destination # we will create a new instance of the protocol as well as a NetRom transport and add it to l3_connections self.l3_servers: Dict[AX25Call, Callable[[], Protocol]] = {} # This is a mapping of local circuit IDs to (transport, protocol). When incoming data is handled for a # circuit, this is how we pass it on to the instance of the protocol self.l3_connections: Dict[int, Tuple[Transport, Protocol]] = {} # This is a mapping of local circuit ID to a protocol factory. These protocols do not yet have a transport and # are in a half-opened state. Once a connect ack is received, a transport will be created and these will be # migrated to l3_connections self.l3_half_open: Dict[int, Protocol] = {} self.l3_destinations: Set[AX25Call] = set() self.l3_protocol = l3_protocol self.l3_protocol.register_transport_protocol(self) def extra(): return f"[L4]" LoggingMixin.__init__(self, logging.getLogger("main"), extra)
class NetworkManager(NetRom, L3Handler, LoggingMixin): def __init__(self, config: NetworkConfig, loop: AbstractEventLoop): self.config = config self.sm = NetRomStateMachine(self, AsyncioTimer) self.router = NetRomRoutingTable(config.node_alias()) self.l3_apps: Dict[AX25Call, str] = {} # Mapping of destination addresses to protocol factory. When a new connection is made to the destination # we will create a new instance of the protocol as well as a NetRom transport and add it to l3_connections self.l3_servers: Dict[AX25Call, Callable[[], Protocol]] = {} # This is a mapping of local circuit IDs to (transport, protocol). When incoming data is handled for a # circuit, this is how we pass it on to the instance of the protocol self.l3_connections: Dict[int, Tuple[Transport, Protocol]] = {} # This is a mapping of local circuit ID to a protocol factory. These protocols do not yet have a transport and # are in a half-opened state. Once a connect ack is received, a transport will be created and these will be # migrated to l3_connections self.l3_half_open: Dict[int, Callable[[], Protocol]] = {} self.data_links: Dict[int, DataLinkManager] = {} self.route_lock = Lock() self.loop = loop def extra(): return f"[L4 Call={str(config.node_call())} Alias={config.node_alias()}]" LoggingMixin.__init__(self, logging.getLogger("main"), extra) async def start(self): self.info("Starting NetworkManager") asyncio.create_task(self._broadcast_nodes_loop()) await self.sm.start() def can_handle(self, protocol: L3Protocol) -> bool: """L3Handler.can_handle""" return protocol == L3Protocol.NetRom def maybe_handle_special(self, port: int, packet: AX25Packet) -> bool: """L3Handler.maybe_handle_special""" if type(packet) == UIFrame: ui = cast(UIFrame, packet) if ui.protocol == L3Protocol.NetRom and ui.dest == AX25Call( "NODES"): # Parse this NODES packet and mark it as handled nodes = parse_netrom_nodes(ui.info) EventBus.emit("netrom.nodes", [nodes]) asyncio.get_event_loop().create_task( self._update_nodes(packet.source, port, nodes)) # Stop further processing return False return True def handle(self, port: int, remote_call: AX25Call, data: bytes): """L3Handler.handle""" try: netrom_packet = parse_netrom_packet(data) asyncio.create_task(self._handle_packet_async(netrom_packet)) return True except: return False def local_call(self) -> AX25Call: return AX25Call.parse(self.config.node_call()) async def _handle_packet_async(self, netrom_packet: NetRomPacket): """If packet is for us, handle it, otherwise forward it using our L3 routing table""" EventBus.emit("netrom.incoming", [netrom_packet]) self.debug(f"RX: {netrom_packet}") packet_logger.info(f"RX: {netrom_packet}") if netrom_packet.dest == AX25Call("KEEPLI-0"): # What are these?? Just ignore them pass elif netrom_packet.dest == AX25Call.parse(self.config.node_call()): # Destination is this node self.sm.handle_packet(netrom_packet) elif netrom_packet.dest in self.l3_apps: # Destination is an app served by this node self.sm.handle_packet(netrom_packet) elif netrom_packet.dest in self.l3_servers: # Destination is an app served by this node self.sm.handle_packet(netrom_packet) else: # Destination is somewhere else self.write_packet(netrom_packet, forward=True) def get_circuit_ids(self) -> List[int]: return self.sm.get_circuits() def get_circuit(self, circuit_id: int): return self.sm._get_circuit(circuit_id, circuit_id) # TODO fix this def nl_data_request(self, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, data: bytes): event = NetRomStateEvent.nl_data(my_circuit_id, remote_call, data) event.future = self.loop.create_future() self.sm.handle_internal_event(event) return event.future def nl_data_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, data: bytes): # Called from the state machine to indicate data to higher layers EventBus.emit(f"netrom.{local_call}.inbound", my_circuit_idx, remote_call, data) if my_circuit_idx in self.l3_connections: self.l3_connections[my_circuit_idx][1].data_received(data) else: self.warning( f"Data indication for unknown circuit {my_circuit_idx}") def nl_connect_request(self, remote_call: AX25Call, local_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call): if remote_call == local_call: raise RuntimeError( f"Cannot connect to node's own callsign {local_call}") # circuit_id of -1 means pick an unused circuit to use nl_connect = NetRomStateEvent.nl_connect(-1, remote_call, local_call, origin_node, origin_user) self.sm.handle_internal_event(nl_connect) async def poll(): while True: if self.sm.get_state( nl_connect.circuit_id) == NetRomStateType.Connected: return nl_connect.circuit_id else: await asyncio.sleep(0.001) return asyncio.create_task(poll()) def nl_connect_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call): # Send a connect event EventBus.emit(f"netrom.{local_call}.connect", my_circuit_idx, remote_call) if my_circuit_idx in self.l3_half_open: # Complete a half-opened connection protocol_factory = self.l3_half_open[my_circuit_idx] protocol = protocol_factory() transport = NetworkTransport(self, local_call, remote_call, my_circuit_idx, origin_node, origin_user) protocol.connection_made(transport) self.l3_connections[my_circuit_idx] = (transport, protocol) del self.l3_half_open[my_circuit_idx] elif local_call in self.l3_servers: if my_circuit_idx in self.l3_connections: # An existing connection, re-connect it self.l3_connections[my_circuit_idx][1].connection_lost( RuntimeError("Remote end reconnected")) self.l3_connections[my_circuit_idx][1].connection_made( self.l3_connections[my_circuit_idx][0]) else: # This a new connection, create the transport and protocol transport = NetworkTransport(self, local_call, remote_call, my_circuit_idx, origin_node, origin_user) protocol = self.l3_servers[local_call]() protocol.connection_made(transport) self.l3_connections[my_circuit_idx] = (transport, protocol) else: self.warning( f"Got unexpected connection from {remote_call} at {local_call}:{my_circuit_idx}" ) def nl_disconnect_request(self, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call): nl_disconnect = NetRomStateEvent.nl_disconnect(my_circuit_id, remote_call, local_call) self.sm.handle_internal_event(nl_disconnect) async def poll(): while True: if self.sm.get_state(nl_disconnect.circuit_id ) == NetRomStateType.Disconnected: return nl_disconnect.circuit_id else: await asyncio.sleep(0.001) return asyncio.create_task(poll()) def nl_disconnect_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call): EventBus.emit(f"netrom.{local_call}.disconnect", my_circuit_idx, remote_call) if local_call in self.l3_servers: if my_circuit_idx in self.l3_connections: self.l3_connections[my_circuit_idx][1].connection_lost(None) del self.l3_connections[my_circuit_idx] else: self.warning( f"Disconnect indication received for unknown circuit {my_circuit_idx}" ) # TODO error indication def write_packet(self, packet: NetRomPacket, forward: bool = False) -> bool: possible_routes = self.router.route(packet) routed = False for route in possible_routes: neighbor = self.router.neighbors.get(str(route)) if neighbor is None: self.logger.warning(f"No neighbor for route {route}") continue #print(f"Trying route {route} to neighbor {neighbor}") data_link = self.data_links.get(neighbor.port) if data_link.link_state(neighbor.call) == AX25StateType.Connected: data_link.dl_data_request(neighbor.call, L3Protocol.NetRom, packet.buffer) routed = True EventBus.emit("netrom.outbound", [packet]) if forward: # Log this transmission differently if it's being forwarded self.logger.info( f"[L3 Route={route} Neighbor={neighbor.call}] TX: {packet}" ) else: self.debug(f"TX: {packet}") packet_logger.info(f"TX: {packet}") break if not routed: self.warning( f"Could not route packet to {packet.dest}. Possible routes were {possible_routes}" ) pass return routed def attach_data_link(self, data_link: DataLinkManager): self.data_links[data_link.link_port] = data_link self.router.our_calls.add(data_link.link_call) data_link.add_l3_handler(self) def bind(self, l4_call: AX25Call, l4_alias: str): self.router.listen_for_address(l4_call, l4_alias) self.l3_apps[l4_call] = l4_alias # Need to bind this here so the application can start sending packets right away EventBus.bind( EventListener( f"netrom.{l4_call}.outbound", f"netrom_{l4_call}_outbound", lambda remote_call, data: self.nl_data_request( remote_call, l4_call, data)), True) def bind_server(self, l3_call: AX25Call, l3_alias: str, protocol_factory: Callable[[], Protocol]): """ Bind a protocol factory to a NetRom destination address :param l3_call: The callsign for the destination :param l3_alias: An alias for this destination (used in NODES broadcast) :param protocol_factory: The protocol factory :return: """ self.router.listen_for_address(l3_call, l3_alias) self.l3_servers[l3_call] = protocol_factory def open(self, protocol_factory: Callable[[], Protocol], local_call: AX25Call, remote_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call): # This a new connection, create the transport and protocol nl_connect = NetRomStateEvent.nl_connect(-1, remote_call, local_call, origin_node, origin_user) self.sm.handle_internal_event(nl_connect) self.l3_half_open[nl_connect.circuit_id] = protocol_factory async def _maybe_open_data_link(self, port: int, remote_call: AX25Call): data_link = self.data_links.get(port) if data_link.link_state(remote_call) in ( AX25StateType.Disconnected, AX25StateType.AwaitingRelease): # print(f"Opening data link to {remote_call}") done, pending = await asyncio.wait( {data_link.dl_connect_request(remote_call)}, timeout=5.000) if len(pending) != 0: self.warning( f"Timed out waiting on data link with {remote_call}") async def _update_nodes(self, heard_from: AX25Call, heard_on: int, nodes: NetRomNodes): async with self.route_lock: #print(f"Got Nodes\n{nodes}") self.router.update_routes(heard_from, heard_on, nodes) #self.info(f"New Routing Table: {self.router}") asyncio.create_task( self._maybe_open_data_link(heard_on, heard_from)) #print(f"New routing table\n{self.router}") await asyncio.sleep(10) async def _broadcast_nodes(self): nodes = self.router.get_nodes() nodes.save("nodes.json") for dl in self.data_links.values(): for nodes_packet in nodes.to_packets( AX25Call.parse(self.config.node_call())): dl.write_packet(nodes_packet) async def _broadcast_nodes_loop(self): await asyncio.sleep(10) # initial delay while True: async with self.route_lock: self.router.prune_routes() await self._broadcast_nodes() await asyncio.sleep(self.config.nodes_interval())
class NetRomTransportProtocol(NetRom, L4Protocol, LoggingMixin): def __init__(self, config: NetworkConfig, l3_protocol: L3Protocol, scheduler: Scheduler): self.config = config self.sm = NetRomStateMachine(self, scheduler.timer) # Mapping of destination addresses to protocol factory. When a new connection is made to the destination # we will create a new instance of the protocol as well as a NetRom transport and add it to l3_connections self.l3_servers: Dict[AX25Call, Callable[[], Protocol]] = {} # This is a mapping of local circuit IDs to (transport, protocol). When incoming data is handled for a # circuit, this is how we pass it on to the instance of the protocol self.l3_connections: Dict[int, Tuple[Transport, Protocol]] = {} # This is a mapping of local circuit ID to a protocol factory. These protocols do not yet have a transport and # are in a half-opened state. Once a connect ack is received, a transport will be created and these will be # migrated to l3_connections self.l3_half_open: Dict[int, Protocol] = {} self.l3_destinations: Set[AX25Call] = set() self.l3_protocol = l3_protocol self.l3_protocol.register_transport_protocol(self) def extra(): return f"[L4]" LoggingMixin.__init__(self, logging.getLogger("main"), extra) def handle_packet(self, netrom_packet: NetRomPacket) -> bool: """ If this packet is addressed to one of the servers or connections, dispatch it to the state machine and then eventually to the transport. Otherwise, drop it. """ if netrom_packet.dest in self.l3_destinations: self.sm.handle_packet_sync(netrom_packet) return True else: return False def local_call(self) -> AX25Call: return AX25Call.parse(self.config.node_call()) def get_circuit_ids(self) -> List[int]: return super().get_circuit_ids() def get_circuit(self, circuit_id: int): return super().get_circuit(circuit_id) def nl_data_request(self, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, data: bytes) -> None: dest = NetRomAddress(remote_call.callsign, remote_call.ssid) routed, mtu = self.l3_protocol.route_packet(dest) if routed: event = NetRomStateEvent.nl_data(my_circuit_id, remote_call, data, mtu) self.sm.handle_internal_event_sync(event) else: self.debug(f"No route to {dest}, dropping") def nl_data_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, data: bytes) -> None: self.debug( f"NET/ROM data from {remote_call} on circuit {my_circuit_id}: {data}" ) if my_circuit_idx in self.l3_connections: self.l3_connections[my_circuit_idx][1].data_received(data) else: self.warning( f"Data indication for unknown circuit {my_circuit_idx}") def nl_connect_request(self, remote_call: AX25Call, local_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call) -> None: nl_connect = NetRomStateEvent.nl_connect(-1, remote_call, local_call, origin_node, origin_user) self.sm.handle_internal_event_sync(nl_connect) def nl_connect_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call) -> None: if my_circuit_idx in self.l3_half_open: # Complete a half-opened connection self.info( f"Completing half-opened connection to {remote_call} for {origin_node}" ) protocol = self.l3_half_open[my_circuit_idx] transport = NetRomTransport(self, local_call, remote_call, my_circuit_idx, origin_node, origin_user) protocol.connection_made(transport) self.l3_connections[my_circuit_idx] = (transport, protocol) del self.l3_half_open[my_circuit_idx] elif my_circuit_idx in self.l3_connections: # An existing connection, re-connect it self.warning(f"Reconnecting to {remote_call} for {origin_node}") (transport, protocol) = self.l3_connections[my_circuit_idx] protocol.connection_made(transport) else: # This a new connection, create the transport and protocol self.info( f"Creating new connection for server {local_call} to {remote_call}" ) transport = NetRomTransport(self, local_call, remote_call, my_circuit_idx, origin_node, origin_user) protocol = self.l3_servers[local_call]() protocol.connection_made(transport) self.l3_connections[my_circuit_idx] = (transport, protocol) def nl_disconnect_request(self, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call) -> None: nl_disconnect = NetRomStateEvent.nl_disconnect(my_circuit_id, remote_call, local_call) self.sm.handle_internal_event_sync(nl_disconnect) def nl_disconnect_indication(self, my_circuit_idx: int, my_circuit_id: int, remote_call: AX25Call, local_call: AX25Call) -> None: self.info( f"NET/ROM disconnected from {remote_call} on circuit {my_circuit_id}" ) if my_circuit_idx in self.l3_connections: self.l3_connections[my_circuit_idx][1].connection_lost(None) del self.l3_connections[my_circuit_idx] else: self.warning( f"Disconnect indication received for unknown circuit {my_circuit_idx}" ) def write_packet(self, packet: NetRomPacket) -> bool: source = NetRomAddress(packet.source.callsign, packet.source.ssid, "SOURCE") dest = NetRomAddress(packet.dest.callsign, packet.dest.ssid, "DEST") payload = L3Payload(source, dest, 0xCF, packet.buffer, -1, QoS.Default, True) if self.l3_protocol is not None: return self.l3_protocol.send_packet(payload) else: self.warning("Cannot send, L3 not attached") return False def open(self, protocol_factory: Callable[[], Protocol], local_call: AX25Call, remote_call: AX25Call, origin_node: AX25Call, origin_user: AX25Call) -> Protocol: # This a new connection, create the transport and protocol nl_connect = NetRomStateEvent.nl_connect(-1, remote_call, local_call, origin_node, origin_user) self.sm.handle_internal_event_sync(nl_connect) protocol = protocol_factory() self.l3_half_open[nl_connect.circuit_id] = protocol self.l3_protocol.listen( NetRomAddress(local_call.callsign, local_call.ssid, "")) self.l3_destinations.add(local_call) return protocol def bind_server(self, l3_call: AX25Call, l3_alias: str, protocol_factory: Callable[[], Protocol]): """ Bind a protocol factory to a NetRom destination address :param l3_call: The callsign for the destination :param l3_alias: An alias for this destination (used in NODES broadcast) :param protocol_factory: The protocol factory :return: """ self.l3_servers[l3_call] = protocol_factory self.l3_protocol.listen( NetRomAddress(l3_call.callsign, l3_call.ssid, l3_alias)) self.l3_destinations.add(l3_call)