diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 8e7b37c3..45af5728 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -16,7 +16,13 @@ from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsRequest, GetNodeConfigServiceRequest, ) -from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest, LinkedRequest +from core.api.grpc.core_pb2 import ( + ExecuteScriptRequest, + GetConfigRequest, + LinkedRequest, + WirelessConfigRequest, + WirelessLinkedRequest, +) from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, GetEmaneEventChannelRequest, @@ -43,6 +49,7 @@ from core.api.grpc.wlan_pb2 import ( WlanConfig, WlanLinkRequest, ) +from core.api.grpc.wrappers import LinkOptions from core.emulator.data import IpPrefixes from core.errors import CoreError @@ -1079,6 +1086,42 @@ class CoreGrpcClient: ) self.stub.Linked(request) + def wireless_linked( + self, + session_id: int, + wireless_id: int, + node1_id: int, + node2_id: int, + linked: bool, + ) -> None: + request = WirelessLinkedRequest( + session_id=session_id, + wireless_id=wireless_id, + node1_id=node1_id, + node2_id=node2_id, + linked=linked, + ) + self.stub.WirelessLinked(request) + + def wireless_config( + self, + session_id: int, + wireless_id: int, + node1_id: int, + node2_id: int, + options1: LinkOptions, + options2: LinkOptions = None, + ) -> None: + request = WirelessConfigRequest( + session_id=session_id, + wireless_id=wireless_id, + node1_id=node1_id, + node2_id=node2_id, + options1=options1.to_proto(), + options2=options2.to_proto(), + ) + self.stub.WirelessConfig(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 9605d1d4..18a97992 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -26,7 +26,15 @@ from core.api.grpc.configservices_pb2 import ( GetNodeConfigServiceRequest, GetNodeConfigServiceResponse, ) -from core.api.grpc.core_pb2 import ExecuteScriptResponse, LinkedRequest, LinkedResponse +from core.api.grpc.core_pb2 import ( + ExecuteScriptResponse, + LinkedRequest, + LinkedResponse, + WirelessConfigRequest, + WirelessConfigResponse, + WirelessLinkedRequest, + WirelessLinkedResponse, +) from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, EmaneLinkResponse, @@ -82,6 +90,7 @@ from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, NodeBase from core.nodes.network import CoreNetwork, WlanNode +from core.nodes.wireless import WirelessNode from core.services.coreservices import ServiceManager logger = logging.getLogger(__name__) @@ -1329,3 +1338,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): request.linked, ) return LinkedResponse() + + def WirelessLinked( + self, request: WirelessLinkedRequest, context: ServicerContext + ) -> WirelessLinkedResponse: + session = self.get_session(request.session_id, context) + wireless = self.get_node(session, request.wireless_id, context, WirelessNode) + wireless.link_control(request.node1_id, request.node2_id, request.linked) + return WirelessLinkedResponse() + + def WirelessConfig( + self, request: WirelessConfigRequest, context: ServicerContext + ) -> WirelessConfigResponse: + session = self.get_session(request.session_id, context) + wireless = self.get_node(session, request.wireless_id, context, WirelessNode) + options1 = request.options1 + options2 = options1 + if request.HasField("options2"): + options2 = request.options2 + wireless.link_config(request.node1_id, request.node2_id, options1, options2) + return WirelessConfigResponse() diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index c4e081fa..7bbfdd7b 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -67,6 +67,7 @@ class NodeType(Enum): CONTROL_NET = 13 DOCKER = 15 LXC = 16 + WIRELESS = 17 class LinkType(Enum): diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index f09428ca..e08481c2 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -4,14 +4,25 @@ from typing import Any, Dict, List from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet -from core.nodes.base import CoreNodeBase +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import WlanNode +from core.nodes.wireless import WirelessNode GROUP: str = "FRR" FRR_STATE_DIR: str = "/var/run/frr" +def is_wireless(node: NodeBase) -> bool: + """ + Check if the node is a wireless type node. + + :param node: node to check type for + :return: True if wireless type, False otherwise + """ + return isinstance(node, (WlanNode, EmaneNet, WirelessNode)) + + def has_mtu_mismatch(iface: CoreInterface) -> bool: """ Helper to detect MTU mismatch and add the appropriate FRR @@ -324,7 +335,7 @@ class FRRBabel(FrrService, ConfigService): return self.render_text(text, data) def frr_iface_config(self, iface: CoreInterface) -> str: - if isinstance(iface.net, (WlanNode, EmaneNet)): + if is_wireless(iface.net): text = """ babel wireless no babel split-horizon diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index a4ee157d..2bafeff6 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -5,16 +5,27 @@ from typing import Any, Dict, List from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet -from core.nodes.base import CoreNodeBase +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node +from core.nodes.wireless import WirelessNode logger = logging.getLogger(__name__) GROUP: str = "Quagga" QUAGGA_STATE_DIR: str = "/var/run/quagga" +def is_wireless(node: NodeBase) -> bool: + """ + Check if the node is a wireless type node. + + :param node: node to check type for + :return: True if wireless type, False otherwise + """ + return isinstance(node, (WlanNode, EmaneNet, WirelessNode)) + + def has_mtu_mismatch(iface: CoreInterface) -> bool: """ Helper to detect MTU mismatch and add the appropriate OSPF @@ -265,7 +276,7 @@ class Ospfv3mdr(Ospfv3): def quagga_iface_config(self, iface: CoreInterface) -> str: config = super().quagga_iface_config(iface) - if isinstance(iface.net, (WlanNode, EmaneNet)): + if is_wireless(iface.net): config = self.clean_text( f""" {config} @@ -390,7 +401,7 @@ class Babel(QuaggaService, ConfigService): return self.render_text(text, data) def quagga_iface_config(self, iface: CoreInterface) -> str: - if isinstance(iface.net, (WlanNode, EmaneNet)): + if is_wireless(iface.net): text = """ babel wireless no babel split-horizon diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 0039d8f4..e04d382b 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -49,6 +49,7 @@ class NodeTypes(Enum): CONTROL_NET = 13 DOCKER = 15 LXC = 16 + WIRELESS = 17 class LinkTypes(Enum): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index c6bd0fed..4cae8a77 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -58,6 +58,7 @@ from core.nodes.network import ( WlanNode, ) from core.nodes.physical import PhysicalNode, Rj45Node +from core.nodes.wireless import WirelessNode from core.plugins.sdt import Sdt from core.services.coreservices import CoreServices from core.xml import corexml, corexmldeployment @@ -80,13 +81,18 @@ NODES: Dict[NodeTypes, Type[NodeBase]] = { NodeTypes.CONTROL_NET: CtrlNet, NodeTypes.DOCKER: DockerNode, NodeTypes.LXC: LxcNode, + NodeTypes.WIRELESS: WirelessNode, } NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode} CTRL_NET_ID: int = 9001 LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] NT: TypeVar = TypeVar("NT", bound=NodeBase) -WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet]] = (WlanNode, EmaneNet) +WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet], Type[WirelessNode]] = ( + WlanNode, + EmaneNet, + WirelessNode, +) class Session: @@ -274,9 +280,9 @@ class Session: # custom links iface1 = None iface2 = None - if isinstance(node1, WlanNode): + if isinstance(node1, (WlanNode, WirelessNode)): iface2 = self._add_wlan_link(node2, iface2_data, node1) - elif isinstance(node2, WlanNode): + elif isinstance(node2, (WlanNode, WirelessNode)): iface1 = self._add_wlan_link(node1, iface1_data, node2) elif isinstance(node1, EmaneNet) and isinstance(node2, CoreNode): iface2 = self._add_emane_link(node2, iface2_data, node1) @@ -298,7 +304,10 @@ class Session: return iface1, iface2 def _add_wlan_link( - self, node: NodeBase, iface_data: InterfaceData, net: WlanNode + self, + node: NodeBase, + iface_data: InterfaceData, + net: Union[WlanNode, WirelessNode], ) -> CoreInterface: """ Create a wlan link. @@ -392,10 +401,10 @@ class Session: ) iface1 = None iface2 = None - if isinstance(node1, WlanNode): + if isinstance(node1, (WlanNode, WirelessNode)): iface2 = node2.delete_iface(iface2_id) node1.detach(iface2) - elif isinstance(node2, WlanNode): + elif isinstance(node2, (WlanNode, WirelessNode)): iface1 = node1.delete_iface(iface1_id) node2.detach(iface1) elif isinstance(node1, EmaneNet): @@ -1111,6 +1120,10 @@ class Session: # boot node services and then start mobility exceptions = self.boot_nodes() if not exceptions: + # complete wireless node + for node in self.nodes.values(): + if isinstance(node, WirelessNode): + node.post_startup() self.mobility.startup() # notify listeners that instantiation is complete event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE) diff --git a/daemon/core/gui/data/icons/wireless.png b/daemon/core/gui/data/icons/wireless.png new file mode 100644 index 00000000..2b42b8dd Binary files /dev/null and b/daemon/core/gui/data/icons/wireless.png differ diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 7c5897e5..53620e72 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -53,6 +53,7 @@ class ImageEnum(Enum): LINK = "link" HUB = "hub" WLAN = "wlan" + WIRELESS = "wireless" EMANE = "emane" RJ45 = "rj45" TUNNEL = "tunnel" diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index ebd96ab0..0175797d 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -18,7 +18,11 @@ NETWORK_NODES: List["NodeDraw"] = [] NODE_ICONS = {} CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} -WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} +WIRELESS_NODES: Set[NodeType] = { + NodeType.WIRELESS_LAN, + NodeType.EMANE, + NodeType.WIRELESS, +} RJ45_NODES: Set[NodeType] = {NodeType.RJ45} BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH} IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} @@ -46,6 +50,7 @@ def setup() -> None: (ImageEnum.HUB, NodeType.HUB, "Hub"), (ImageEnum.SWITCH, NodeType.SWITCH, "Switch"), (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"), + (ImageEnum.WIRELESS, NodeType.WIRELESS, "Wireless"), (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), (ImageEnum.RJ45, NodeType.RJ45, "RJ45"), (ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"), diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 818c700c..41d6efb3 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -766,8 +766,9 @@ class CoreNode(CoreNodeBase): raise CoreError(f"adopting unknown iface({iface.name})") # add iface to container namespace self.net_client.device_ns(iface.name, str(self.pid)) - # update iface name to container name - name = name if name else f"eth{iface_id}" + # use default iface name for container, if a unique name was not provided + if iface.name == name: + name = f"eth{iface_id}" self.node_net_client.device_name(iface.name, name) iface.name = name # turn checksums off diff --git a/daemon/core/nodes/wireless.py b/daemon/core/nodes/wireless.py new file mode 100644 index 00000000..69e64639 --- /dev/null +++ b/daemon/core/nodes/wireless.py @@ -0,0 +1,240 @@ +""" +Defines a wireless node that allows programmatic link connectivity and +configuration between pairs of nodes. +""" + +import logging +import math +from dataclasses import dataclass +from typing import TYPE_CHECKING, Dict, Tuple + +from core.emulator.data import LinkData, LinkOptions +from core.emulator.enumerations import LinkTypes, MessageFlags +from core.errors import CoreError +from core.executables import NFTABLES +from core.nodes.base import CoreNetworkBase +from core.nodes.interface import CoreInterface + +if TYPE_CHECKING: + from core.emulator.session import Session + from core.emulator.distributed import DistributedServer + +logger = logging.getLogger(__name__) + + +def calc_distance( + point1: Tuple[float, float, float], point2: Tuple[float, float, float] +) -> float: + a = point1[0] - point2[0] + b = point1[1] - point2[1] + c = 0 + if point1[2] is not None and point2[2] is not None: + c = point1[2] - point2[2] + return math.hypot(math.hypot(a, b), c) + + +@dataclass +class WirelessLink: + bridge1: str + bridge2: str + iface: CoreInterface + + +class WirelessNode(CoreNetworkBase): + def __init__( + self, + session: "Session", + _id: int, + name: str, + server: "DistributedServer" = None, + ): + super().__init__(session, _id, name, server) + self.bridges: Dict[int, Tuple[CoreInterface, str]] = {} + self.links: Dict[Tuple[int, int], WirelessLink] = {} + self.labels: Dict[Tuple[int, int], str] = {} + + def startup(self) -> None: + if self.up: + return + self.up = True + + def shutdown(self) -> None: + while self.bridges: + _, (_, bridge_name) = self.bridges.popitem() + self.net_client.delete_bridge(bridge_name) + self.host_cmd(f"{NFTABLES} delete table bridge {bridge_name}") + while self.links: + _, link = self.links.popitem() + link.iface.shutdown() + self.up = False + + def attach(self, iface: CoreInterface) -> None: + super().attach(iface) + logging.info("attaching node(%s) iface(%s)", iface.node.name, iface.name) + if self.up: + # create node unique bridge + bridge_name = f"wb{iface.node.id}.{self.id}.{self.session.id}" + self.net_client.create_bridge(bridge_name) + # setup initial bridge rules + self.host_cmd(f'{NFTABLES} "add table bridge {bridge_name}"') + self.host_cmd( + f"{NFTABLES} " + f"'add chain bridge {bridge_name} forward {{type filter hook " + f"forward priority -1; policy drop;}}'" + ) + self.host_cmd( + f"{NFTABLES} " + f"'add rule bridge {bridge_name} forward " + f"ibriport != {bridge_name} accept'" + ) + # associate node iface with bridge + iface.net_client.set_iface_master(bridge_name, iface.localname) + # assign position callback + iface.poshook = self.position_callback + # save created bridge + self.bridges[iface.node.id] = (iface, bridge_name) + + def post_startup(self) -> None: + routes = {} + for node_id, (iface, bridge_name) in self.bridges.items(): + for onode_id, (oiface, obridge_name) in self.bridges.items(): + if node_id == onode_id: + continue + if node_id < onode_id: + node1, node2 = iface.node, oiface.node + bridge1, bridge2 = bridge_name, obridge_name + else: + node1, node2 = oiface.node, iface.node + bridge1, bridge2 = obridge_name, bridge_name + key = (node1.id, node2.id) + if key in self.links: + continue + # create node to node link + session_id = self.session.short_session_id() + name1 = f"we{self.id}.{node1.id}.{node2.id}.{session_id}" + name2 = f"we{self.id}.{node2.id}.{node1.id}.{session_id}" + link_iface = CoreInterface(0, name1, name2, self.session.use_ovs()) + link_iface.startup() + link = WirelessLink(bridge1, bridge2, link_iface) + self.links[key] = link + # assign ifaces to respective bridges + self.net_client.set_iface_master(bridge1, link_iface.name) + self.net_client.set_iface_master(bridge2, link_iface.localname) + # track bridge routes + node1_routes = routes.setdefault(node1.id, set()) + node1_routes.add(name1) + node2_routes = routes.setdefault(node2.id, set()) + node2_routes.add(name2) + # send link + self.send_link(node1.id, node2.id, MessageFlags.ADD) + for node_id, ifaces in routes.items(): + iface, bridge_name = self.bridges[node_id] + ifaces = ",".join(ifaces) + # out routes + self.host_cmd( + f"{NFTABLES} " + f'"add rule bridge {bridge_name} forward ' + f"iif {iface.localname} oif {{{ifaces}}} " + f'accept"' + ) + # in routes + self.host_cmd( + f"{NFTABLES} " + f'"add rule bridge {bridge_name} forward ' + f"iif {{{ifaces}}} oif {iface.localname} " + f'accept"' + ) + + def link_control(self, node1_id: int, node2_id: int, linked: bool) -> None: + key = (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id) + link = self.links.get(key) + if not link: + raise CoreError(f"invalid node links node1({node1_id}) node2({node2_id})") + bridge1, bridge2 = link.bridge1, link.bridge2 + iface = link.iface + if linked: + self.net_client.set_iface_master(bridge1, iface.name) + self.net_client.set_iface_master(bridge2, iface.localname) + message_type = MessageFlags.ADD + else: + self.net_client.delete_iface(bridge1, iface.name) + self.net_client.delete_iface(bridge2, iface.localname) + message_type = MessageFlags.DELETE + label = self.labels.get(key) + self.send_link(key[0], key[1], message_type, label) + + def link_config( + self, node1_id: int, node2_id: int, options1: LinkOptions, options2: LinkOptions + ) -> None: + key = (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id) + link = self.links.get(key) + if not link: + raise CoreError(f"invalid node links node1({node1_id}) node2({node2_id})") + iface = link.iface + iface.options.update(options1) + iface.set_config() + name, localname = iface.name, iface.localname + iface.name, iface.localname = localname, name + iface.options.update(options2) + iface.set_config() + iface.name, iface.localname = name, localname + if options1 == options2: + if options1.is_clear(): + label = "" + else: + label = f"{options1.loss:.2f}%/{options1.delay}us" + else: + label = ( + f"({options1.loss:.2f}%/{options1.delay}us) " + f"({options2.loss:.2f}%/{options2.delay}us)" + ) + self.labels[key] = label + self.send_link(key[0], key[1], MessageFlags.NONE, label) + + def send_link( + self, + node1_id: int, + node2_id: int, + message_type: MessageFlags, + label: str = None, + ) -> None: + """ + Broadcasts out a wireless link/unlink message. + + :param node1_id: first node in link + :param node2_id: second node in link + :param message_type: type of link message to send + :param label: label to display for link + :return: nothing + """ + color = self.session.get_link_color(self.id) + link_data = LinkData( + message_type=message_type, + type=LinkTypes.WIRELESS, + node1_id=node1_id, + node2_id=node2_id, + network_id=self.id, + color=color, + label=label, + ) + self.session.broadcast_link(link_data) + + def position_callback(self, iface: CoreInterface) -> None: + for oiface, bridge_name in self.bridges.values(): + if iface == oiface: + continue + point1 = iface.node.position.get() + point2 = oiface.node.position.get() + distance = calc_distance(point1, point2) - 250 + distance = max(distance, 0.0) + logger.info( + "n1(%s) n2(%s) d(%s)", iface.node.name, oiface.node.name, distance + ) + loss = min((distance / 500) * 100.0, 100.0) + node1_id = iface.node.id + node2_id = oiface.node.id + options = LinkOptions(loss=loss, delay=0) + self.link_config(node1_id, node2_id, options, options) + + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + raise CoreError(f"{type(self)} does not support adopt interface") diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 575f9257..0300a3b0 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,7 +4,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket -from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from urllib.parse import urlparse from core.constants import CORE_CONF_DIR, CORE_DATA_DIR @@ -12,22 +12,15 @@ from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import EventTypes, MessageFlags from core.errors import CoreError -from core.nodes.base import CoreNetworkBase, NodeBase +from core.nodes.base import NodeBase from core.nodes.network import WlanNode +from core.nodes.wireless import WirelessNode logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.emulator.session import Session - -def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str: - link_id = f"{node1_id}-{node2_id}" - if network_id is not None: - link_id = f"{link_id}-{network_id}" - return link_id - - CORE_LAYER: str = "CORE" NODE_LAYER: str = "CORE::Nodes" LINK_LAYER: str = "CORE::Links" @@ -36,6 +29,17 @@ CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER] DEFAULT_LINK_COLOR: str = "red" +def is_wireless(node: NodeBase) -> bool: + return isinstance(node, (WlanNode, EmaneNet, WirelessNode)) + + +def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str: + link_id = f"{node1_id}-{node2_id}" + if network_id is not None: + link_id = f"{link_id}-{network_id}" + return link_id + + class Sdt: """ Helper class for exporting session objects to NRL"s SDT3D. @@ -67,7 +71,7 @@ class Sdt: :param session: session this manager is tied to """ self.session: "Session" = session - self.sock: Optional[IO] = None + self.sock: Optional[socket.socket] = None self.connected: bool = False self.url: str = self.DEFAULT_SDT_URL self.address: Optional[Tuple[Optional[str], Optional[int]]] = None @@ -210,26 +214,23 @@ class Sdt: :return: nothing """ - nets = [] - # create layers for layer in CORE_LAYERS: self.cmd(f"layer {layer}") - with self.session.nodes_lock: - for node_id in self.session.nodes: - node = self.session.nodes[node_id] - if isinstance(node, CoreNetworkBase): + nets = [] + for node in self.session.nodes.values(): + if isinstance(node, (EmaneNet, WlanNode)): nets.append(node) if not isinstance(node, NodeBase): continue self.add_node(node) - + for link in self.session.link_manager.links(): + if is_wireless(link.node1) or is_wireless(link.node2): + continue + link_data = link.get_data(MessageFlags.ADD) + self.handle_link_update(link_data) for net in nets: - all_links = net.links(flags=MessageFlags.ADD) - for link_data in all_links: - is_wireless = isinstance(net, (WlanNode, EmaneNet)) - if is_wireless and link_data.node1_id == net.id: - continue + for link_data in net.links(MessageFlags.ADD): self.handle_link_update(link_data) def get_node_position(self, node: NodeBase) -> Optional[str]: @@ -341,7 +342,7 @@ class Sdt: result = False try: node = self.session.get_node(node_id, NodeBase) - result = isinstance(node, (WlanNode, EmaneNet)) + result = isinstance(node, (WlanNode, EmaneNet, WirelessNode)) except CoreError: pass return result diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 9029daec..07b1cfa5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -7,15 +7,26 @@ from typing import Optional, Tuple import netaddr from core.emane.nodes import EmaneNet -from core.nodes.base import CoreNode +from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node +from core.nodes.wireless import WirelessNode from core.services.coreservices import CoreService FRR_STATE_DIR: str = "/var/run/frr" +def is_wireless(node: NodeBase) -> bool: + """ + Check if the node is a wireless type node. + + :param node: node to check type for + :return: True if wireless type, False otherwise + """ + return isinstance(node, (WlanNode, EmaneNet, WirelessNode)) + + class FRRZebra(CoreService): name: str = "FRRzebra" group: str = "FRR" @@ -593,7 +604,7 @@ class FRRBabel(FrrService): @classmethod def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: - if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)): + if is_wireless(iface.net): return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index fa71feee..51668bc9 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -7,15 +7,26 @@ import netaddr from core.emane.nodes import EmaneNet from core.emulator.enumerations import LinkTypes -from core.nodes.base import CoreNode +from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node +from core.nodes.wireless import WirelessNode from core.services.coreservices import CoreService QUAGGA_STATE_DIR: str = "/var/run/quagga" +def is_wireless(node: NodeBase) -> bool: + """ + Check if the node is a wireless type node. + + :param node: node to check type for + :return: True if wireless type, False otherwise + """ + return isinstance(node, (WlanNode, EmaneNet, WirelessNode)) + + class Zebra(CoreService): name: str = "zebra" group: str = "Quagga" @@ -431,7 +442,7 @@ class Ospfv3mdr(Ospfv3): @classmethod def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: cfg = cls.mtu_check(iface) - if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)): + if is_wireless(iface.net): return ( cfg + """\ diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 7edc6511..57d42187 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -16,6 +16,7 @@ from core.nodes.docker import DockerNode from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet, GreTapBridge, WlanNode +from core.nodes.wireless import WirelessNode from core.services.coreservices import CoreService logger = logging.getLogger(__name__) @@ -542,8 +543,8 @@ class CoreXmlWriter: iface2 = self.create_iface_element("iface2", iface2) link_element.append(iface2) # check for options, don't write for emane/wlan links - is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) - is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) + is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet, WirelessNode)) + is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet, WirelessNode)) if not (is_node1_wireless or is_node2_wireless): unidirectional = 1 if unidirectional else 0 options_element = etree.Element("options") diff --git a/daemon/examples/grpc/wireless.py b/daemon/examples/grpc/wireless.py new file mode 100644 index 00000000..b196d748 --- /dev/null +++ b/daemon/examples/grpc/wireless.py @@ -0,0 +1,29 @@ +from core.api.grpc import client +from core.api.grpc.wrappers import NodeType, Position + +# interface helper +iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") + +# create grpc client and connect +core = client.CoreGrpcClient() +core.connect() + +# add session +session = core.create_session() + +# create nodes +position = Position(x=200, y=200) +wlan = session.add_node(1, _type=NodeType.WIRELESS, position=position) +position = Position(x=100, y=100) +node1 = session.add_node(2, model="mdr", position=position) +position = Position(x=300, y=100) +node2 = session.add_node(3, model="mdr", position=position) + +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +session.add_link(node1=node1, node2=wlan, iface1=iface1) +iface1 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node2, node2=wlan, iface1=iface1) + +# start session +core.start_session(session) diff --git a/daemon/examples/python/wireless.py b/daemon/examples/python/wireless.py new file mode 100644 index 00000000..559cf2f8 --- /dev/null +++ b/daemon/examples/python/wireless.py @@ -0,0 +1,46 @@ +# required imports +import logging + +from core.emulator.coreemu import CoreEmu +from core.emulator.data import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.network import WlanNode + +# enable info logging +logging.basicConfig(level=logging.INFO) + +# ip nerator for example +ip_prefixes = IpPrefixes(ip4_prefix="10.0.0.0/24") + +# create emulator instance for creating sessions and utility methods +coreemu = CoreEmu() +session = coreemu.create_session() + +# must be in configuration state for nodes to start +session.set_state(EventTypes.CONFIGURATION_STATE) + +# create wireless +options = NodeOptions(x=200, y=200) +wireless = session.add_node(WlanNode, options=options) + +# create nodes +options = NodeOptions(model="mdr", x=100, y=100) +n1 = session.add_node(CoreNode, options=options) +options = NodeOptions(model="mdr", x=300, y=100) +n2 = session.add_node(CoreNode, options=options) + +# link nodes to wireless +iface1 = ip_prefixes.create_iface(n1) +session.add_link(n1.id, wireless.id, iface1) +iface1 = ip_prefixes.create_iface(n2) +session.add_link(n2.id, wireless.id, iface1) + +# start session +session.instantiate() + +# do whatever you like here +input("press enter to shutdown") + +# stop session +session.shutdown() diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index c364ce24..f17b61fe 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -100,6 +100,12 @@ service CoreApi { rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) { } + // wireless rpc + rpc WirelessLinked (WirelessLinkedRequest) returns (WirelessLinkedResponse) { + } + rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) { + } + // emane rpc rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { } @@ -697,3 +703,26 @@ message LinkedRequest { message LinkedResponse { } + +message WirelessLinkedRequest { + int32 session_id = 1; + int32 wireless_id = 2; + int32 node1_id = 3; + int32 node2_id = 4; + bool linked = 5; +} + +message WirelessLinkedResponse { +} + +message WirelessConfigRequest { + int32 session_id = 1; + int32 wireless_id = 2; + int32 node1_id = 3; + int32 node2_id = 4; + LinkOptions options1 = 5; + LinkOptions options2 = 6; +} + +message WirelessConfigResponse { +}