core-extra/daemon/core/nodes/network.py

1126 lines
36 KiB
Python

"""
Defines network nodes used within core.
"""
import logging
import threading
import time
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type
import netaddr
from core import utils
from core.constants import EBTABLES_BIN, TC_BIN
from core.emulator.data import LinkData, NodeData
from core.emulator.emudata import InterfaceData, LinkOptions
from core.emulator.enumerations import (
LinkTypes,
MessageFlags,
NetworkPolicy,
NodeTypes,
RegisterTlvs,
)
from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNetworkBase
from core.nodes.interface import CoreInterface, GreTap, Veth
from core.nodes.netclient import get_net_client
if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer
from core.emulator.session import Session
from core.location.mobility import WirelessModel, WayPointMobility
WirelessModelType = Type[WirelessModel]
ebtables_lock = threading.Lock()
class EbtablesQueue:
"""
Helper class for queuing up ebtables commands into rate-limited
atomic commits. This improves performance and reliability when there are
many WLAN link updates.
"""
# update rate is every 300ms
rate: float = 0.3
# ebtables
atomic_file: str = "/tmp/pycore.ebtables.atomic"
def __init__(self) -> None:
"""
Initialize the helper class, but don't start the update thread
until a WLAN is instantiated.
"""
self.doupdateloop: bool = False
self.updatethread: Optional[threading.Thread] = None
# this lock protects cmds and updates lists
self.updatelock: threading.Lock = threading.Lock()
# list of pending ebtables commands
self.cmds: List[str] = []
# list of WLANs requiring update
self.updates: List["CoreNetwork"] = []
# timestamps of last WLAN update; this keeps track of WLANs that are
# using this queue
self.last_update_time: Dict["CoreNetwork", float] = {}
def startupdateloop(self, wlan: "CoreNetwork") -> None:
"""
Kick off the update loop; only needs to be invoked once.
:return: nothing
"""
with self.updatelock:
self.last_update_time[wlan] = time.monotonic()
if self.doupdateloop:
return
self.doupdateloop = True
self.updatethread = threading.Thread(target=self.updateloop, daemon=True)
self.updatethread.start()
def stopupdateloop(self, wlan: "CoreNetwork") -> None:
"""
Kill the update loop thread if there are no more WLANs using it.
:return: nothing
"""
with self.updatelock:
try:
del self.last_update_time[wlan]
except KeyError:
logging.exception(
"error deleting last update time for wlan, ignored before: %s", wlan
)
if len(self.last_update_time) > 0:
return
self.doupdateloop = False
if self.updatethread:
self.updatethread.join()
self.updatethread = None
def ebatomiccmd(self, cmd: str) -> str:
"""
Helper for building ebtables atomic file command list.
:param cmd: ebtable command
:return: ebtable atomic command
"""
return f"{EBTABLES_BIN} --atomic-file {self.atomic_file} {cmd}"
def lastupdate(self, wlan: "CoreNetwork") -> float:
"""
Return the time elapsed since this WLAN was last updated.
:param wlan: wlan entity
:return: elpased time
"""
try:
elapsed = time.monotonic() - self.last_update_time[wlan]
except KeyError:
self.last_update_time[wlan] = time.monotonic()
elapsed = 0.0
return elapsed
def updated(self, wlan: "CoreNetwork") -> None:
"""
Keep track of when this WLAN was last updated.
:param wlan: wlan entity
:return: nothing
"""
self.last_update_time[wlan] = time.monotonic()
self.updates.remove(wlan)
def updateloop(self) -> None:
"""
Thread target that looks for WLANs needing update, and
rate limits the amount of ebtables activity. Only one userspace program
should use ebtables at any given time, or results can be unpredictable.
:return: nothing
"""
while self.doupdateloop:
with self.updatelock:
for wlan in self.updates:
# Check if wlan is from a previously closed session. Because of the
# rate limiting scheme employed here, this may happen if a new session
# is started soon after closing a previous session.
# TODO: if these are WlanNodes, this will never throw an exception
try:
wlan.session
except Exception:
# Just mark as updated to remove from self.updates.
self.updated(wlan)
continue
if self.lastupdate(wlan) > self.rate:
self.buildcmds(wlan)
self.ebcommit(wlan)
self.updated(wlan)
time.sleep(self.rate)
def ebcommit(self, wlan: "CoreNetwork") -> None:
"""
Perform ebtables atomic commit using commands built in the self.cmds list.
:return: nothing
"""
# save kernel ebtables snapshot to a file
args = self.ebatomiccmd("--atomic-save")
wlan.host_cmd(args)
# modify the table file using queued ebtables commands
for c in self.cmds:
args = self.ebatomiccmd(c)
wlan.host_cmd(args)
self.cmds = []
# commit the table file to the kernel
args = self.ebatomiccmd("--atomic-commit")
wlan.host_cmd(args)
try:
wlan.host_cmd(f"rm -f {self.atomic_file}")
except CoreCommandError:
logging.exception("error removing atomic file: %s", self.atomic_file)
def ebchange(self, wlan: "CoreNetwork") -> None:
"""
Flag a change to the given WLAN's _linked dict, so the ebtables
chain will be rebuilt at the next interval.
:return: nothing
"""
with self.updatelock:
if wlan not in self.updates:
self.updates.append(wlan)
def buildcmds(self, wlan: "CoreNetwork") -> None:
"""
Inspect a _linked dict from a wlan, and rebuild the ebtables chain for that WLAN.
:return: nothing
"""
with wlan._linked_lock:
if wlan.has_ebtables_chain:
# flush the chain
self.cmds.append(f"-F {wlan.brname}")
else:
wlan.has_ebtables_chain = True
self.cmds.extend(
[
f"-N {wlan.brname} -P {wlan.policy.value}",
f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}",
]
)
# rebuild the chain
for netif1, v in wlan._linked.items():
for netif2, linked in v.items():
if wlan.policy == NetworkPolicy.DROP and linked:
self.cmds.extend(
[
f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT",
f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT",
]
)
elif wlan.policy == NetworkPolicy.ACCEPT and not linked:
self.cmds.extend(
[
f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP",
f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j DROP",
]
)
# a global object because all WLANs share the same queue
# cannot have multiple threads invoking the ebtables commnd
ebq: EbtablesQueue = EbtablesQueue()
def ebtablescmds(call: Callable[..., str], cmds: List[str]) -> None:
"""
Run ebtable commands.
:param call: function to call commands
:param cmds: commands to call
:return: nothing
"""
with ebtables_lock:
for args in cmds:
call(args)
class CoreNetwork(CoreNetworkBase):
"""
Provides linux bridge network functionality for core nodes.
"""
policy: NetworkPolicy = NetworkPolicy.DROP
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
start: bool = True,
server: "DistributedServer" = None,
policy: NetworkPolicy = None,
) -> None:
"""
Creates a LxBrNet instance.
:param session: core session instance
:param _id: object id
:param name: object name
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
:param policy: network policy
"""
super().__init__(session, _id, name, start, server)
if name is None:
name = str(self.id)
if policy is not None:
self.policy = policy
self.name: Optional[str] = name
sessionid = self.session.short_session_id()
self.brname: str = f"b.{self.id}.{sessionid}"
self.has_ebtables_chain: bool = False
if start:
self.startup()
ebq.startupdateloop(self)
def host_cmd(
self,
args: str,
env: Dict[str, str] = None,
cwd: str = None,
wait: bool = True,
shell: bool = False,
) -> str:
"""
Runs a command that is used to configure and setup the network on the host
system and all configured distributed servers.
:param args: command to run
:param env: environment to run command with
:param cwd: directory to run command in
:param wait: True to wait for status, False otherwise
:param shell: True to use shell, False otherwise
:return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs
"""
logging.debug("network node(%s) cmd", self.name)
output = utils.cmd(args, env, cwd, wait, shell)
self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait))
return output
def startup(self) -> None:
"""
Linux bridge starup logic.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.net_client.create_bridge(self.brname)
self.has_ebtables_chain = False
self.up = True
def shutdown(self) -> None:
"""
Linux bridge shutdown logic.
:return: nothing
"""
if not self.up:
return
ebq.stopupdateloop(self)
try:
self.net_client.delete_bridge(self.brname)
if self.has_ebtables_chain:
cmds = [
f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}",
f"{EBTABLES_BIN} -X {self.brname}",
]
ebtablescmds(self.host_cmd, cmds)
except CoreCommandError:
logging.exception("error during shutdown")
# removes veth pairs used for bridge-to-bridge connections
for netif in self.netifs():
netif.shutdown()
self._netif.clear()
self._linked.clear()
del self.session
self.up = False
def attach(self, netif: CoreInterface) -> None:
"""
Attach a network interface.
:param netif: network interface to attach
:return: nothing
"""
if self.up:
netif.net_client.set_interface_master(self.brname, netif.localname)
super().attach(netif)
def detach(self, netif: CoreInterface) -> None:
"""
Detach a network interface.
:param netif: network interface to detach
:return: nothing
"""
if self.up:
netif.net_client.delete_interface(self.brname, netif.localname)
super().detach(netif)
def linked(self, netif1: CoreInterface, netif2: CoreInterface) -> bool:
"""
Determine if the provided network interfaces are linked.
:param netif1: interface one
:param netif2: interface two
:return: True if interfaces are linked, False otherwise
"""
# check if the network interfaces are attached to this network
if self._netif[netif1.netifi] != netif1:
raise ValueError(f"inconsistency for netif {netif1.name}")
if self._netif[netif2.netifi] != netif2:
raise ValueError(f"inconsistency for netif {netif2.name}")
try:
linked = self._linked[netif1][netif2]
except KeyError:
if self.policy == NetworkPolicy.ACCEPT:
linked = True
elif self.policy == NetworkPolicy.DROP:
linked = False
else:
raise Exception(f"unknown policy: {self.policy.value}")
self._linked[netif1][netif2] = linked
return linked
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
"""
Unlink two interfaces, resulting in adding or removing ebtables
filtering rules.
:param netif1: interface one
:param netif2: interface two
:return: nothing
"""
with self._linked_lock:
if not self.linked(netif1, netif2):
return
self._linked[netif1][netif2] = False
ebq.ebchange(self)
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
"""
Link two interfaces together, resulting in adding or removing
ebtables filtering rules.
:param netif1: interface one
:param netif2: interface two
:return: nothing
"""
with self._linked_lock:
if self.linked(netif1, netif2):
return
self._linked[netif1][netif2] = True
ebq.ebchange(self)
def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
) -> None:
"""
Configure link parameters by applying tc queuing disciplines on the interface.
:param netif: interface one
:param options: options for configuring link
:param netif2: interface two
:return: nothing
"""
devname = netif.localname
tc = f"{TC_BIN} qdisc replace dev {devname}"
parent = "root"
changed = False
bw = options.bandwidth
if netif.setparam("bw", bw):
# from tc-tbf(8): minimum value for burst is rate / kernel_hz
burst = max(2 * netif.mtu, int(bw / 1000))
# max IP payload
limit = 0xFFFF
tbf = f"tbf rate {bw} burst {burst} limit {limit}"
if bw > 0:
if self.up:
cmd = f"{tc} {parent} handle 1: {tbf}"
netif.host_cmd(cmd)
netif.setparam("has_tbf", True)
changed = True
elif netif.getparam("has_tbf") and bw <= 0:
if self.up:
cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}"
netif.host_cmd(cmd)
netif.setparam("has_tbf", False)
# removing the parent removes the child
netif.setparam("has_netem", False)
changed = True
if netif.getparam("has_tbf"):
parent = "parent 1:1"
netem = "netem"
delay = options.delay
changed = max(changed, netif.setparam("delay", delay))
loss = options.per
if loss is not None:
loss = float(loss)
changed = max(changed, netif.setparam("loss", loss))
duplicate = options.dup
if duplicate is not None:
duplicate = int(duplicate)
changed = max(changed, netif.setparam("duplicate", duplicate))
jitter = options.jitter
changed = max(changed, netif.setparam("jitter", jitter))
if not changed:
return
# jitter and delay use the same delay statement
if delay is not None:
netem += f" delay {delay}us"
if jitter is not None:
if delay is None:
netem += f" delay 0us {jitter}us 25%"
else:
netem += f" {jitter}us 25%"
if loss is not None and loss > 0:
netem += f" loss {min(loss, 100)}%"
if duplicate is not None and duplicate > 0:
netem += f" duplicate {min(duplicate, 100)}%"
delay_check = delay is None or delay <= 0
jitter_check = jitter is None or jitter <= 0
loss_check = loss is None or loss <= 0
duplicate_check = duplicate is None or duplicate <= 0
if all([delay_check, jitter_check, loss_check, duplicate_check]):
# possibly remove netem if it exists and parent queue wasn't removed
if not netif.getparam("has_netem"):
return
if self.up:
cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:"
netif.host_cmd(cmd)
netif.setparam("has_netem", False)
elif len(netem) > 1:
if self.up:
cmd = (
f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}"
)
netif.host_cmd(cmd)
netif.setparam("has_netem", True)
def linknet(self, net: CoreNetworkBase) -> CoreInterface:
"""
Link this bridge with another by creating a veth pair and installing
each device into each bridge.
:param net: network to link with
:return: created interface
"""
sessionid = self.session.short_session_id()
try:
_id = f"{self.id:x}"
except TypeError:
_id = str(self.id)
try:
net_id = f"{net.id:x}"
except TypeError:
net_id = str(net.id)
localname = f"veth{_id}.{net_id}.{sessionid}"
if len(localname) >= 16:
raise ValueError(f"interface local name {localname} too long")
name = f"veth{net_id}.{_id}.{sessionid}"
if len(name) >= 16:
raise ValueError(f"interface name {name} too long")
netif = Veth(self.session, None, name, localname, start=self.up)
self.attach(netif)
if net.up and net.brname:
netif.net_client.set_interface_master(net.brname, netif.name)
i = net.newifindex()
net._netif[i] = netif
with net._linked_lock:
net._linked[netif] = {}
netif.net = self
netif.othernet = net
return netif
def getlinknetif(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
"""
Return the interface of that links this net with another net
(that were linked using linknet()).
:param net: interface to get link for
:return: interface the provided network is linked to
"""
for netif in self.netifs():
if netif.othernet == net:
return netif
return None
def addrconfig(self, addrlist: List[str]) -> None:
"""
Set addresses on the bridge.
:param addrlist: address list
:return: nothing
"""
if not self.up:
return
for addr in addrlist:
self.net_client.create_address(self.brname, str(addr))
class GreTapBridge(CoreNetwork):
"""
A network consisting of a bridge with a gretap device for tunneling to
another system.
"""
def __init__(
self,
session: "Session",
remoteip: str = None,
_id: int = None,
name: str = None,
policy: NetworkPolicy = NetworkPolicy.ACCEPT,
localip: str = None,
ttl: int = 255,
key: int = None,
start: bool = True,
server: "DistributedServer" = None,
) -> None:
"""
Create a GreTapBridge instance.
:param session: core session instance
:param remoteip: remote address
:param _id: object id
:param name: object name
:param policy: network policy
:param localip: local address
:param ttl: ttl value
:param key: gre tap key
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
"""
CoreNetwork.__init__(self, session, _id, name, False, server, policy)
if key is None:
key = self.session.id ^ self.id
self.grekey: int = key
self.localnum: Optional[int] = None
self.remotenum: Optional[int] = None
self.remoteip: Optional[str] = remoteip
self.localip: Optional[str] = localip
self.ttl: int = ttl
self.gretap: Optional[GreTap] = None
if remoteip is not None:
self.gretap = GreTap(
node=self,
session=session,
remoteip=remoteip,
localip=localip,
ttl=ttl,
key=self.grekey,
)
if start:
self.startup()
def startup(self) -> None:
"""
Creates a bridge and adds the gretap device to it.
:return: nothing
"""
super().startup()
if self.gretap:
self.attach(self.gretap)
def shutdown(self) -> None:
"""
Detach the gretap device and remove the bridge.
:return: nothing
"""
if self.gretap:
self.detach(self.gretap)
self.gretap.shutdown()
self.gretap = None
super().shutdown()
def addrconfig(self, addrlist: List[str]) -> None:
"""
Set the remote tunnel endpoint. This is a one-time method for
creating the GreTap device, which requires the remoteip at startup.
The 1st address in the provided list is remoteip, 2nd optionally
specifies localip.
:param addrlist: address list
:return: nothing
"""
if self.gretap:
raise ValueError(f"gretap already exists for {self.name}")
remoteip = addrlist[0].split("/")[0]
localip = None
if len(addrlist) > 1:
localip = addrlist[1].split("/")[0]
self.gretap = GreTap(
session=self.session,
remoteip=remoteip,
localip=localip,
ttl=self.ttl,
key=self.grekey,
)
self.attach(self.gretap)
def setkey(self, key: int, interface_data: InterfaceData) -> None:
"""
Set the GRE key used for the GreTap device. This needs to be set
prior to instantiating the GreTap device (before addrconfig).
:param key: gre key
:param interface_data: interface data for setting up tunnel key
:return: nothing
"""
self.grekey = key
addresses = interface_data.get_addresses()
if addresses:
self.addrconfig(addresses)
class CtrlNet(CoreNetwork):
"""
Control network functionality.
"""
policy: NetworkPolicy = NetworkPolicy.ACCEPT
# base control interface index
CTRLIF_IDX_BASE: int = 99
DEFAULT_PREFIX_LIST: List[str] = [
"172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24",
"172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24",
"172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24",
"172.19.0.0/24 172.19.1.0/24 172.19.2.0/24 172.19.3.0/24 172.19.4.0/24",
]
def __init__(
self,
session: "Session",
prefix: str,
_id: int = None,
name: str = None,
hostid: int = None,
start: bool = True,
server: "DistributedServer" = None,
assign_address: bool = True,
updown_script: str = None,
serverintf: str = None,
) -> None:
"""
Creates a CtrlNet instance.
:param session: core session instance
:param _id: node id
:param name: node namee
:param prefix: control network ipv4 prefix
:param hostid: host id
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
:param assign_address: assigned address
:param updown_script: updown script
:param serverintf: server interface
:return:
"""
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(prefix).cidr
self.hostid: Optional[int] = hostid
self.assign_address: bool = assign_address
self.updown_script: Optional[str] = updown_script
self.serverintf: Optional[str] = serverintf
super().__init__(session, _id, name, start, server)
def add_addresses(self, index: int) -> None:
"""
Add addresses used for created control networks,
:param index: starting address index
:return: nothing
"""
use_ovs = self.session.options.get_config("ovs") == "True"
address = self.prefix[index]
current = f"{address}/{self.prefix.prefixlen}"
net_client = get_net_client(use_ovs, utils.cmd)
net_client.create_address(self.brname, current)
servers = self.session.distributed.servers
for name in servers:
server = servers[name]
index -= 1
address = self.prefix[index]
current = f"{address}/{self.prefix.prefixlen}"
net_client = get_net_client(use_ovs, server.remote_cmd)
net_client.create_address(self.brname, current)
def startup(self) -> None:
"""
Startup functionality for the control network.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
if self.net_client.existing_bridges(self.id):
raise CoreError(f"old bridges exist for node: {self.id}")
super().startup()
logging.info("added control network bridge: %s %s", self.brname, self.prefix)
if self.hostid and self.assign_address:
self.add_addresses(self.hostid)
elif self.assign_address:
self.add_addresses(-2)
if self.updown_script:
logging.info(
"interface %s updown script (%s startup) called",
self.brname,
self.updown_script,
)
self.host_cmd(f"{self.updown_script} {self.brname} startup")
if self.serverintf:
self.net_client.set_interface_master(self.brname, self.serverintf)
def shutdown(self) -> None:
"""
Control network shutdown.
:return: nothing
"""
if self.serverintf is not None:
try:
self.net_client.delete_interface(self.brname, self.serverintf)
except CoreCommandError:
logging.exception(
"error deleting server interface %s from bridge %s",
self.serverintf,
self.brname,
)
if self.updown_script is not None:
try:
logging.info(
"interface %s updown script (%s shutdown) called",
self.brname,
self.updown_script,
)
self.host_cmd(f"{self.updown_script} {self.brname} shutdown")
except CoreCommandError:
logging.exception("error issuing shutdown script shutdown")
super().shutdown()
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Do not include CtrlNet in link messages describing this session.
:param flags: message flags
:return: list of link data
"""
return []
class PtpNet(CoreNetwork):
"""
Peer to peer network node.
"""
policy: NetworkPolicy = NetworkPolicy.ACCEPT
def attach(self, netif: CoreInterface) -> None:
"""
Attach a network interface, but limit attachment to two interfaces.
:param netif: network interface
:return: nothing
"""
if len(self._netif) >= 2:
raise ValueError(
"Point-to-point links support at most 2 network interfaces"
)
super().attach(netif)
def data(
self, message_type: MessageFlags = MessageFlags.NONE, source: str = None
) -> Optional[NodeData]:
"""
Do not generate a Node Message for point-to-point links. They are
built using a link message instead.
:param message_type: purpose for the data object we are creating
:param source: source of node data
:return: node data object
"""
return None
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Build CORE API TLVs for a point-to-point link. One Link message
describes this network.
:param flags: message flags
:return: list of link data
"""
all_links = []
if len(self._netif) != 2:
return all_links
if1, if2 = self._netif.values()
unidirectional = 0
if if1.getparams() != if2.getparams():
unidirectional = 1
interface1_ip4 = None
interface1_ip4_mask = None
interface1_ip6 = None
interface1_ip6_mask = None
for address in if1.addrlist:
ip, _sep, mask = address.partition("/")
mask = int(mask)
if netaddr.valid_ipv4(ip):
interface1_ip4 = ip
interface1_ip4_mask = mask
else:
interface1_ip6 = ip
interface1_ip6_mask = mask
interface2_ip4 = None
interface2_ip4_mask = None
interface2_ip6 = None
interface2_ip6_mask = None
for address in if2.addrlist:
ip, _sep, mask = address.partition("/")
mask = int(mask)
if netaddr.valid_ipv4(ip):
interface2_ip4 = ip
interface2_ip4_mask = mask
else:
interface2_ip6 = ip
interface2_ip6_mask = mask
link_data = LinkData(
message_type=flags,
node1_id=if1.node.id,
node2_id=if2.node.id,
link_type=self.linktype,
unidirectional=unidirectional,
delay=if1.getparam("delay"),
bandwidth=if1.getparam("bw"),
per=if1.getparam("loss"),
dup=if1.getparam("duplicate"),
jitter=if1.getparam("jitter"),
interface1_id=if1.node.getifindex(if1),
interface1_name=if1.name,
interface1_mac=if1.hwaddr,
interface1_ip4=interface1_ip4,
interface1_ip4_mask=interface1_ip4_mask,
interface1_ip6=interface1_ip6,
interface1_ip6_mask=interface1_ip6_mask,
interface2_id=if2.node.getifindex(if2),
interface2_name=if2.name,
interface2_mac=if2.hwaddr,
interface2_ip4=interface2_ip4,
interface2_ip4_mask=interface2_ip4_mask,
interface2_ip6=interface2_ip6,
interface2_ip6_mask=interface2_ip6_mask,
)
all_links.append(link_data)
# build a 2nd link message for the upstream link parameters
# (swap if1 and if2)
if unidirectional:
link_data = LinkData(
message_type=MessageFlags.NONE,
link_type=self.linktype,
node1_id=if2.node.id,
node2_id=if1.node.id,
delay=if2.getparam("delay"),
bandwidth=if2.getparam("bw"),
per=if2.getparam("loss"),
dup=if2.getparam("duplicate"),
jitter=if2.getparam("jitter"),
unidirectional=1,
interface1_id=if2.node.getifindex(if2),
interface2_id=if1.node.getifindex(if1),
)
all_links.append(link_data)
return all_links
class SwitchNode(CoreNetwork):
"""
Provides switch functionality within a core node.
"""
apitype: NodeTypes = NodeTypes.SWITCH
policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "lanswitch"
class HubNode(CoreNetwork):
"""
Provides hub functionality within a core node, forwards packets to all bridge
ports by turning off MAC address learning.
"""
apitype: NodeTypes = NodeTypes.HUB
policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "hub"
def startup(self) -> None:
"""
Startup for a hub node, that disables mac learning after normal startup.
:return: nothing
"""
super().startup()
self.net_client.disable_mac_learning(self.brname)
class WlanNode(CoreNetwork):
"""
Provides wireless lan functionality within a core node.
"""
apitype: NodeTypes = NodeTypes.WIRELESS_LAN
linktype: LinkTypes = LinkTypes.WIRED
policy: NetworkPolicy = NetworkPolicy.DROP
type: str = "wlan"
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
start: bool = True,
server: "DistributedServer" = None,
policy: NetworkPolicy = None,
) -> None:
"""
Create a WlanNode instance.
:param session: core session instance
:param _id: node id
:param name: node name
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
:param policy: wlan policy
"""
super().__init__(session, _id, name, start, server, policy)
# wireless and mobility models (BasicRangeModel, Ns2WaypointMobility)
self.model: Optional[WirelessModel] = None
self.mobility: Optional[WayPointMobility] = None
def startup(self) -> None:
"""
Startup for a wlan node, that disables mac learning after normal startup.
:return: nothing
"""
super().startup()
self.net_client.disable_mac_learning(self.brname)
ebq.ebchange(self)
def attach(self, netif: CoreInterface) -> None:
"""
Attach a network interface.
:param netif: network interface
:return: nothing
"""
super().attach(netif)
if self.model:
netif.poshook = self.model.position_callback
netif.setposition()
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]):
"""
Sets the mobility and wireless model.
:param model: wireless model to set to
:param config: configuration for model being set
:return: nothing
"""
logging.debug("node(%s) setting model: %s", self.name, model.name)
if model.config_type == RegisterTlvs.WIRELESS:
self.model = model(session=self.session, _id=self.id)
for netif in self.netifs():
netif.poshook = self.model.position_callback
netif.setposition()
self.updatemodel(config)
elif model.config_type == RegisterTlvs.MOBILITY:
self.mobility = model(session=self.session, _id=self.id)
self.mobility.update_config(config)
def update_mobility(self, config: Dict[str, str]) -> None:
if not self.mobility:
raise CoreError(f"no mobility set to update for node({self.name})")
self.mobility.update_config(config)
def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model:
raise CoreError(f"no model set to update for node({self.name})")
logging.debug(
"node(%s) updating model(%s): %s", self.id, self.model.name, config
)
self.model.update_config(config)
for netif in self.netifs():
netif.setposition()
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Retrieve all link data.
:param flags: message flags
:return: list of link data
"""
all_links = super().all_link_data(flags)
if self.model:
all_links.extend(self.model.all_link_data(flags))
return all_links
class TunnelNode(GreTapBridge):
"""
Provides tunnel functionality in a core node.
"""
apitype: NodeTypes = NodeTypes.TUNNEL
policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "tunnel"