diff --git a/examples/scion/S01-scion/README.md b/examples/scion/S01-scion/README.md index d04389fbe..68a8ae8ec 100644 --- a/examples/scion/S01-scion/README.md +++ b/examples/scion/S01-scion/README.md @@ -108,6 +108,8 @@ Inter-AS routing in SCION is based on fixed "links" which are combined during th The `Scion` layer exposes two methods `addIxLink()` and `addXcLink()` for setting up SCION links over an IX and via direct cross-connect links, respectively. In both methods, we must specify the two endpoints of the link as pairs of ISD and ASN. The order of endpoints matters as SCION transit links are directional (for beaconing purposes, packets are still forwarded in both directions) from `a` to `b`. The reason we must name ASes by ASN and ISD is that only the pair of ISD and ASN uniquely identifies a SCION AS, i.e., a link from (1, 150) to (1, 151) is completely different from a hypothetical link between (2, 150) and (2, 151). +Additionally, there are two optional arguments `a_router` and `b_router`. If there is only one link between a pair of ASN's specifying these is not necessary. But if there are several cross-connects or several routers on the internet exchange, a_router has to be set to the name of the border-router. If you want to see an example of this check out S06-scion-link-properties + Besides the endpoints every SCION link has a type. Currently there are three types: - `Core` links connect core ASes of the same or different ISDs. The core ASes of every ISD must all be reachable by one another via core links. The BGP analogy to core links is peering between Tier 1 ASes. - `Transit` links connect core ASes to non-core ASes in the same ISD. They model Internet transit as sold from a provider AS to a customer AS. diff --git a/examples/scion/S06-scion-link-properties/README.md b/examples/scion/S06-scion-link-properties/README.md new file mode 100644 index 000000000..eeff56f58 --- /dev/null +++ b/examples/scion/S06-scion-link-properties/README.md @@ -0,0 +1,39 @@ +# Scion with link properties + +In this example we show how one can specify link properties. + +We have three ASes 110,111 and 112. 110 is a core AS and is connected to the other two ASes through a cross connects. + +## Setting Cross connect link properties + +Setting cross connect link properties works as shown in this exampls: + +`as_110_br2.crossConnect(112,'br1','10.3.0.10/29',latency=30,bandwidth=500,packetDrop=0.1,MTU=1100)` + + +`as110.createNetwork('net0').setDefaultLinkProperties(latency=10, bandwidth=1000, packetDrop=0.1).setMtu(1400)` + +Latency, and Bandwidth will be included in the Scion beacons if specified here. If no properties are specified, they will be omitted in the beacons + +## Specify border routers for scion routing + +If there is more than one link between a pair of ASes one can specify how to set the routes as follows: + +`scion.addXcLink((1,110),(1,111),ScLinkType.Transit,a_router='br1',b_router='br1')` +`scion.addXcLink((1,110),(1,111),ScLinkType.Transit,a_router='br2',b_router='br1')` + +## Set additional Border Router / AS properties + +One can also specify additional information for the Scion-ASes and the border routers. Geolocation, and AS note will also be included in the beacons during the beaconing process + +`as110.setNote('This is a core AS')` + +`as_110_br1 = as110.createRouter('br1').joinNetwork('net0').setGeo(Lat=37.7749, Long=-122.4194,Address="San Francisco, CA, USA").setNote("This is a border router")` + +## Including link properties in beacons + +By default, available link properties, Geolocation, Hops and AS-Notes will be included in the `staticInfoConfig.json` file on the control Service nodes. To turn this of one can set the `generateStaticInfoConfig` flag to false as follows: + +```python +as110.setGenerateStaticInfoConfig(False) +``` \ No newline at end of file diff --git a/examples/scion/S06-scion-link-properties/scion-link-properties.py b/examples/scion/S06-scion-link-properties/scion-link-properties.py new file mode 100644 index 000000000..e9ccba7f5 --- /dev/null +++ b/examples/scion/S06-scion-link-properties/scion-link-properties.py @@ -0,0 +1,67 @@ + +from seedemu.compiler import Docker +from seedemu.core import Emulator +from seedemu.layers import ScionBase, ScionRouting, ScionIsd, Scion +from seedemu.layers.Scion import LinkType as ScLinkType + +# Initialize +emu = Emulator() +base = ScionBase() +routing = ScionRouting() +scion_isd = ScionIsd() +scion = Scion() + +# Create ISDs +base.createIsolationDomain(1) + + +# Ases + +# AS-110 +as110 = base.createAutonomousSystem(110) +as110.setNote('This is a core AS') +as110.setGenerateStaticInfoConfig(True) # Generate static info config (This is not neccessary as the default behaviour is to generate static info config) +scion_isd.addIsdAs(1,110,is_core=True) +as110.createNetwork('net0').setDefaultLinkProperties(latency=10, bandwidth=1000, packetDrop=0.1).setMtu(1400) +as110.createControlService('cs_1').joinNetwork('net0') +as_110_br1 = as110.createRouter('br1').joinNetwork('net0').setGeo(Lat=37.7749, Long=-122.4194,Address="San Francisco, CA, USA").setNote("This is a border router") +as_110_br1.crossConnect(111,'br1','10.3.0.2/29',latency=0,bandwidth=0,packetDrop=0,MTU=1280) +as_110_br2 = as110.createRouter('br2').joinNetwork('net0') +as_110_br2.crossConnect(112,'br1','10.3.0.10/29',latency=30,bandwidth=500,packetDrop=0.1,MTU=1100) +as_110_br2.crossConnect(111,'br1','10.3.0.16/29') + +# AS-111 +as111 = base.createAutonomousSystem(111) +scion_isd.addIsdAs(1,111,is_core=False) +scion_isd.setCertIssuer((1,111),issuer=110) +as111.createNetwork('net0').setDefaultLinkProperties(latency=0, bandwidth=0, packetDrop=0) +as111.createControlService('cs_1').joinNetwork('net0') +as_111_br1 = as111.createRouter('br1').joinNetwork('net0') +as_111_br1.crossConnect(110,'br1','10.3.0.3/29',latency=0,bandwidth=0,packetDrop=0,MTU=1280) +as_111_br1.crossConnect(110,'br2','10.3.0.17/29') + +# AS-112 +as112 = base.createAutonomousSystem(112) +scion_isd.addIsdAs(1,112,is_core=False) +scion_isd.setCertIssuer((1,112),issuer=110) +as112.createNetwork('net0').setDefaultLinkProperties(latency=0, bandwidth=0, packetDrop=0) +as112.createControlService('cs_1').joinNetwork('net0') +as_112_br1 = as112.createRouter('br1').joinNetwork('net0') +as_112_br1.crossConnect(110,'br2','10.3.0.11/29',latency=30,bandwidth=500,packetDrop=0.1,MTU=1100) + + +# Inter-AS routing +scion.addXcLink((1,110),(1,111),ScLinkType.Transit,a_router='br1',b_router='br1') +scion.addXcLink((1,110),(1,112),ScLinkType.Transit,a_router='br2',b_router='br1') +scion.addXcLink((1,110),(1,111),ScLinkType.Transit,a_router='br2',b_router='br1') + +# Rendering +emu.addLayer(base) +emu.addLayer(routing) +emu.addLayer(scion_isd) +emu.addLayer(scion) + +emu.render() + +# Compilation +emu.compile(Docker(), './output') diff --git a/examples/scion/S07-scion-bwtest-client/README.md b/examples/scion/S07-scion-bwtest-client/README.md new file mode 100644 index 000000000..189214bb8 --- /dev/null +++ b/examples/scion/S07-scion-bwtest-client/README.md @@ -0,0 +1,72 @@ +# SCION Bandwidth Test Client + +This example demonstrates how to set up a SCION network for conducting bandwidth tests, using scion-bwtestclient, between Autonomous Systems (ASes) using SeedEmu. It includes configuring ASes, routers, hosts, and bandwidth test services. + +## Initializing the bwtestclient Service + +First, we initialize the bwtestclient service. + +```python +from seedemu.services import ScionBwtestClientService +# other imports + +# Create bandwidth test services +bwtest = ScionBwtestService() +bwtestclient = ScionBwtestClientService() +``` + +## Setting up Bandwidth Test Services + +First we create scion-bwtestserver in every AS. + +```python +# Bandwidth test server in AS-150 +as150.createHost('bwtest').joinNetwork('net0', address='10.150.0.30') +bwtest.install('bwtest150').setPort(40002) +emu.addBinding(Binding('bwtest150', filter=Filter(nodeName='bwtest', asn=150))) + +# Bandwidth test server in AS-151 +as151.createHost('bwtest').joinNetwork('net0', address='10.151.0.30') +bwtest.install('bwtest151') +emu.addBinding(Binding('bwtest151', filter=Filter(nodeName='bwtest', asn=151))) + +# Bandwidth test server in AS-152 +as152.createHost('bwtest').joinNetwork('net0', address='10.152.0.30') +bwtest.install('bwtest152') +emu.addBinding(Binding('bwtest152', filter=Filter(nodeName='bwtest', asn=152))) + +# Bandwidth test server in AS-153 +as153.createHost('bwtestserver').joinNetwork('net0', address='10.153.0.30') +bwtest.install('bwtest153') +emu.addBinding(Binding('bwtest153', filter=Filter(nodeName='bwtestserver', asn=153))) +``` + +Now we create a scion-bwtestclient in one of the ASes + +```python +# Bandwidth test client in AS-153 +as153.createHost('bwtestclient').joinNetwork('net0', address='10.153.0.31').addSharedFolder("/var/log", "/absolute/path/to/logs/on/host") +bwtestclient.install('bwtestclient153').setServerAddr('1-151,10.151.0.30').setWaitTime(20) +emu.addBinding(Binding('bwtestclient153', filter=Filter(nodeName='bwtestclient', asn=153))) +``` + +**Notes:** +- the addSharedFolder("nodePath","hostPath") function makes sure that we can see the bwtestclient output without attaching to the container +- the setServerAddr() function specifies the server address of the bwtestserver +- the setWaitTime() function sets the number of seconds to wait after the container started before running the test. The default is 60 seconds. +- there are also other functions: + - setPort() -- setting the port the server listens on + - setPreference() -- set the preference for sorting paths (check bwtester documentation for more details) + - setCS() -- Client->Server test parameter (default "3,1000,30,80kbps") + - setSC() -- Server->Client test parameter (default "3,1000,30,80kbps") + +## Rendering and Compilation + +We add the configured layers to the emulator, render the network topology, and compile the configuration into Docker containers. + +```python +# Rendering +emu.addLayer(bwtest) +emu.addLayer(bwtestclient) +``` + diff --git a/examples/scion/S07-scion-bwtest-client/scion-bwtest-client.py b/examples/scion/S07-scion-bwtest-client/scion-bwtest-client.py new file mode 100644 index 000000000..94ddbe1cf --- /dev/null +++ b/examples/scion/S07-scion-bwtest-client/scion-bwtest-client.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 + +from seedemu.compiler import Docker +from seedemu.core import Emulator, Binding, Filter +from seedemu.layers import ScionBase, ScionRouting, ScionIsd, Scion +from seedemu.layers.Scion import LinkType as ScLinkType +from seedemu.services import ScionBwtestService, ScionBwtestClientService + +# Initialize +emu = Emulator() +base = ScionBase() +routing = ScionRouting() +scion_isd = ScionIsd() +scion = Scion() +bwtest = ScionBwtestService() +bwtestclient = ScionBwtestClientService() + +# SCION ISDs +base.createIsolationDomain(1) + +# AS-150 +as150 = base.createAutonomousSystem(150) +scion_isd.addIsdAs(1, 150, is_core=True) +as150.createNetwork('net0') +as150.createControlService('cs1').joinNetwork('net0') +as150_router = as150.createRouter('br0').joinNetwork('net0') +as150_router.crossConnect(151, 'br0', '10.50.0.10/29', latency=10, bandwidth=1000000, packetDrop=0.01) +as150_router.crossConnect(152, 'br0', '10.50.0.18/29') +as150_router.crossConnect(153, 'br0', '10.50.0.3/29') + +# Create a host running the bandwidth test server +as150.createHost('bwtest').joinNetwork('net0', address='10.150.0.30') +bwtest.install('bwtest150').setPort(40002) # Setting the port is optional (40002 is the default) +emu.addBinding(Binding('bwtest150', filter=Filter(nodeName='bwtest', asn=150))) + +# AS-151 +as151 = base.createAutonomousSystem(151) +scion_isd.addIsdAs(1, 151, is_core=True) +as151.createNetwork('net0') +as151.createControlService('cs1').joinNetwork('net0') +as151_br0 = as151.createRouter('br0').joinNetwork('net0').addSoftware("iperf3") +as151_br0.crossConnect(150, 'br0', '10.50.0.11/29', latency=10, bandwidth=1000000, packetDrop=0.01) +as151_br0.crossConnect(152, 'br0', '10.50.0.26/29') + +as151.createHost('bwtest').joinNetwork('net0', address='10.151.0.30') +bwtest.install('bwtest151') +emu.addBinding(Binding('bwtest151', filter=Filter(nodeName='bwtest', asn=151))) + +# AS-152 +as152 = base.createAutonomousSystem(152) +scion_isd.addIsdAs(1, 152, is_core=True) +as152.createNetwork('net0') +as152.createControlService('cs1').joinNetwork('net0') +as152_br0 = as152.createRouter('br0').joinNetwork('net0') +as152_br0.crossConnect(150, 'br0', '10.50.0.19/29') +as152_br0.crossConnect(151, 'br0', '10.50.0.27/29') + +as152.createHost('bwtest').joinNetwork('net0', address='10.152.0.30') +bwtest.install('bwtest152') +emu.addBinding(Binding('bwtest152', filter=Filter(nodeName='bwtest', asn=152))) + +# AS-153 +as153 = base.createAutonomousSystem(153) +scion_isd.addIsdAs(1, 153, is_core=False) +scion_isd.setCertIssuer((1, 153), issuer=150) +as153.createNetwork('net0') +as153.createControlService('cs1').joinNetwork('net0') +as153_router = as153.createRouter('br0') +as153_router.joinNetwork('net0') +as153_router.crossConnect(150, 'br0', '10.50.0.4/29') + +as153.createHost('bwtestserver').joinNetwork('net0', address='10.153.0.30') +bwtest.install('bwtest153') +emu.addBinding(Binding('bwtest153', filter=Filter(nodeName='bwtestserver', asn=153))) + +# AS-153 bwtestclient +as153.createHost('bwtestclient').joinNetwork('net0', address='10.153.0.31').addSharedFolder("/var/log", "/absolute/path/to/logs/on/host") # make logs of bwtestclient available on host +bwtestclient.install('bwtestclient153').setServerAddr('1-151,10.151.0.30').setWaitTime(20) # set the server address and time to wait before starting the test +emu.addBinding(Binding('bwtestclient153', filter=Filter(nodeName='bwtestclient', asn=153))) + +# Inter-AS routing +scion.addXcLink((1, 150), (1, 151), ScLinkType.Core) +scion.addXcLink((1, 150), (1, 152), ScLinkType.Core) +scion.addXcLink((1, 151), (1, 152), ScLinkType.Core) +scion.addXcLink((1, 150), (1, 153), ScLinkType.Transit) + +# Rendering +emu.addLayer(base) +emu.addLayer(routing) +emu.addLayer(scion_isd) +emu.addLayer(scion) +emu.addLayer(bwtest) +emu.addLayer(bwtestclient) + +emu.render() + +# Compilation +emu.compile(Docker(internetMapEnabled=True), './output') diff --git a/seedemu/core/InternetExchange.py b/seedemu/core/InternetExchange.py index 53c9d6a67..c5274de87 100644 --- a/seedemu/core/InternetExchange.py +++ b/seedemu/core/InternetExchange.py @@ -70,6 +70,15 @@ def getRouteServerNode(self) -> Node: @returns RS node. """ return self.__rs + + def getNetwork(self) -> Network: + """! + @brief Get the network of Internet Exchange. + + @returns Network. + """ + + return self.__net def print(self, indent: int) -> str: out = ' ' * indent diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index a566d027f..723a59ead 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -147,7 +147,7 @@ def setType(self, newType: NetworkType) -> Network: return self - def getDefaultLinkProperties(self) -> Tuple[int, int, int]: + def getDefaultLinkProperties(self) -> Tuple[int, int, float]: """! @brief Get default link properties. diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 92a00ef25..c926d50a9 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -8,7 +8,7 @@ from .enums import NetworkType from .Visualization import Vertex from ipaddress import IPv4Address, IPv4Interface -from typing import List, Dict, Set, Tuple +from typing import List, Dict, Set, Tuple, Optional from string import ascii_letters from random import choice from .BaseSystem import BaseSystem @@ -222,13 +222,18 @@ class Node(Printable, Registrable, Configurable, Vertex): __configured: bool __pending_nets: List[Tuple[str, str]] - __xcs: Dict[Tuple[str, int], Tuple[IPv4Interface, str]] + + # Dict of (peername, peerasn) -> (localaddr, netname, netProperties) -- netProperties = (latency, bandwidth, packetDrop, MTU) + __xcs: Dict[Tuple[str, int], Tuple[IPv4Interface, str, Tuple[int,int,float,int]]] __shared_folders: Dict[str, str] __persistent_storages: List[str] __name_servers: List[str] + __geo: Tuple[float,float,str] # (Latitude,Longitude,Address) -- optional parameter that contains the geographical location of the Node + __note: str # optional parameter that contains a note about the Node + def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): """! @brief Node constructor. @@ -269,6 +274,9 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__name_servers = [] self.__host_names = [f"{self.__scope}-{self.__name}"] + self.__geo = None + self.__note = None + def configure(self, emulator: Emulator): """! @brief configure the node. This is called when rendering. @@ -309,19 +317,21 @@ def configure(self, emulator: Emulator): elif reg.has(str(peerasn), 'hnode', peername): peer = reg.get(str(peerasn), 'hnode', peername) else: assert False, 'as{}/{}: cannot xc to node as{}/{}: no such node'.format(self.getAsn(), self.getName(), peerasn, peername) - (peeraddr, netname) = peer.getCrossConnect(self.getAsn(), self.getName()) - (localaddr, _) = self.__xcs[(peername, peerasn)] + (peeraddr, netname, (peerLatency, peerBandwidth, peerPacketDrop, peerMTU)) = peer.getCrossConnect(self.getAsn(), self.getName()) + (localaddr, _, (latency, bandwidth, packetDrop, mtu)) = self.__xcs[(peername, peerasn)] assert localaddr.network == peeraddr.network, 'as{}/{}: cannot xc to node as{}/{}: {}.net != {}.net'.format(self.getAsn(), self.getName(), peerasn, peername, localaddr, peeraddr) - + assert (peerLatency == latency and peerBandwidth == bandwidth and peerPacketDrop == packetDrop and peerMTU == mtu), 'as{}/{}: cannot xc to node as{}/{}: because link properties (({},{},{},{}) -- ({},{},{},{})) dont match'.format(self.getAsn(), self.getName(), peerasn, peername, latency, bandwidth, packetDrop, mtu, peerLatency, peerBandwidth, peerPacketDrop, peerMTU) + if netname != None: self.__joinNetwork(reg.get('xc', 'net', netname), str(localaddr.ip)) - self.__xcs[(peername, peerasn)] = (localaddr, netname) + self.__xcs[(peername, peerasn)] = (localaddr, netname, (latency, bandwidth, packetDrop,mtu)) else: # netname = 'as{}.{}_as{}.{}'.format(self.getAsn(), self.getName(), peerasn, peername) netname = ''.join(choice(ascii_letters) for i in range(10)) net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False) # TODO: XC nets w/ direct flag? + net.setDefaultLinkProperties(latency, bandwidth, packetDrop).setMtu(mtu) # Set link properties self.__joinNetwork(reg.register('xc', 'net', netname, net), str(localaddr.ip)) - self.__xcs[(peername, peerasn)] = (localaddr, netname) + self.__xcs[(peername, peerasn)] = (localaddr, netname, (latency, bandwidth, packetDrop, mtu)) if len(self.__name_servers) == 0: return @@ -517,34 +527,37 @@ def updateNetwork(self, netname:str, address: str= "auto") -> Node: return self - def crossConnect(self, peerasn: int, peername: str, address: str) -> Node: + def crossConnect(self, peerasn: int, peername: str, address: str, latency: int=0, bandwidth: int=0, packetDrop: float=0, MTU: int=1500) -> Node: """! @brief create new p2p cross-connect connection to a remote node. @param peername node name of the peer node. @param peerasn asn of the peer node. @param address address to use on the interface in CIDR notation. Must be within the same subnet. + @param latency latency the cross connect network will have + @param bandwidth bandwidth the cross connect network will have + @param packetDrop packet drop the cross connect network will have @returns self, for chaining API calls. """ assert not self.__asn == 0, 'This API is only available on a real physical node.' assert peername != self.getName() or peerasn != self.getName(), 'cannot XC to self.' - self.__xcs[(peername, peerasn)] = (IPv4Interface(address), None) + self.__xcs[(peername, peerasn)] = (IPv4Interface(address), None, (latency,bandwidth,packetDrop,MTU)) - def getCrossConnect(self, peerasn: int, peername: str) -> Tuple[IPv4Interface, str]: + def getCrossConnect(self, peerasn: int, peername: str) -> Tuple[IPv4Interface, str, Tuple[int, int, float, int]]: """! @brief retrieve IP address for the given peer. @param peername node name of the peer node. @param peerasn asn of the peer node. @returns tuple of IP address and XC network name. XC network name will - be None if the network has not yet been created. + be None if the network has not yet been created. And Tuple with network link properties. """ assert not self.__asn == 0, 'This API is only available on a real physical node.' assert (peername, peerasn) in self.__xcs, 'as{}/{} is not in the XC list.'.format(peerasn, peername) return self.__xcs[(peername, peerasn)] - def getCrossConnects(self) -> Dict[Tuple[str, int], Tuple[IPv4Interface, str]]: + def getCrossConnects(self) -> Dict[Tuple[str, int], Tuple[IPv4Interface, str, Tuple[int, int, float]]]: """! @brief get all cross connects on this node. @@ -841,6 +854,46 @@ def getPersistentStorages(self) -> List[str]: @returns list of persistent storage folder. """ return self.__persistent_storages + + def setGeo(self, Lat: float, Long: float, Address: str="") -> Node: + """! + @brief Set geographical location of the Node + + @param Lat Latitude + @param Long Longitude + @param Address Address + + @returns self, for chaining API calls. + """ + self.__geo = (Lat, Long, Address) + return self + + def getGeo(self) -> Optional[Tuple[float,float,str]]: + """! + @brief Get geographical location of the Node + + @returns Tuple (Latitude, Longitude, Address) + """ + return self.__geo + + def setNote(self, note: str) -> Node: + """! + @brief Set a note about the Node + + @param note Note + + @returns self, for chaining API calls. + """ + self.__note = note + return self + + def getNote(self) -> Optional[str]: + """! + @brief Get a note about the Node + + @returns Note + """ + return self.__note def copySettings(self, node: Node): """! diff --git a/seedemu/core/ScionAutonomousSystem.py b/seedemu/core/ScionAutonomousSystem.py index 55b659362..769b1d5ed 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -36,6 +36,8 @@ class ScionAutonomousSystem(AutonomousSystem): __beaconing_intervals: Tuple[Optional[str], Optional[str], Optional[str]] __beaconing_policy: Dict[str, Dict] __next_ifid: int # Next IFID assigned to a link + __note: str # optional free form parameter that contains interesting information about AS. This will be included in beacons if it is set + __generateStaticInfoConfig: bool def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): """! @@ -49,6 +51,9 @@ def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): self.__beaconing_intervals = (None, None, None) self.__beaconing_policy = {} self.__next_ifid = 1 + self.__note = None + self.__generateStaticInfoConfig = False + def registerNodes(self, emulator: Emulator): """! @@ -227,6 +232,42 @@ def getControlService(self, name: str) -> Node: """ return self.__control_services[name] + def setNote(self, note: str) -> ScionAutonomousSystem: + """! + @brief Set a note for the AS. + + @param note Note. + @returns self + """ + self.__note = note + return self + + def getNote(self) -> Optional[str]: + """! + @brief Get the note for the AS. + + @returns Note or None if no note is set. + """ + return self.__note + + def setGenerateStaticInfoConfig(self, generateStaticInfoConfig: bool) -> ScionAutonomousSystem: + """! + @brief Set the generateStaticInfoConfig flag. + + @param generateStaticInfoConfig Flag. + @returns self + """ + self.__generateStaticInfoConfig = generateStaticInfoConfig + return self + + def getGenerateStaticInfoConfig(self) -> bool: + """! + @brief Get the generateStaticInfoConfig flag. + + @returns Flag. + """ + return self.__generateStaticInfoConfig + def _doCreateGraphs(self, emulator: Emulator): """! @copydoc AutonomousSystem._doCreateGraphs() diff --git a/seedemu/layers/Scion.py b/seedemu/layers/Scion.py index 59df33a06..e108b9346 100644 --- a/seedemu/layers/Scion.py +++ b/seedemu/layers/Scion.py @@ -56,8 +56,8 @@ class Scion(Layer, Graphable): alone do not uniquely identify a SCION AS (see ScionISD layer). """ - __links: Dict[Tuple[IA, IA, LinkType], int] - __ix_links: Dict[Tuple[int, IA, IA, LinkType], int] + __links: Dict[Tuple[IA, IA, str, str, LinkType], int] + __ix_links: Dict[Tuple[int, IA, IA, str, str, LinkType], int] def __init__(self): """! @@ -71,8 +71,9 @@ def __init__(self): def getName(self) -> str: return "Scion" + def addXcLink(self, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[int, int]], - linkType: LinkType, count: int=1) -> 'Scion': + linkType: LinkType, count: int=1, a_router: str="", b_router: str="",) -> 'Scion': """! @brief Create a direct cross-connect link between to ASes. @@ -80,6 +81,8 @@ def addXcLink(self, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[int, int]] @param b Second AS (ISD and ASN). @param linkType Link type from a to b. @param count Number of parallel links. + @param a_router router of AS a default is "" + @param b_router router of AS b default is "" @throws AssertionError if link already exists or is link to self. @@ -87,15 +90,15 @@ def addXcLink(self, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[int, int]] """ a, b = IA(*a), IA(*b) assert a.asn != b.asn, "Cannot link as{} to itself.".format(a.asn) - assert (a, b, linkType) not in self.__links, ( + assert (a, b, a_router, b_router, linkType) not in self.__links, ( "Link between as{} and as{} of type {} exists already.".format(a, b, linkType)) - self.__links[(a, b, linkType)] = count + self.__links[(a, b, a_router, b_router, linkType)] = count return self def addIxLink(self, ix: int, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[int, int]], - linkType: LinkType, count: int=1) -> 'Scion': + linkType: LinkType, count: int=1, a_router: str="", b_router: str="") -> 'Scion': """! @brief Create a private link between two ASes at an IX. @@ -104,6 +107,8 @@ def addIxLink(self, ix: int, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[i @param b Second AS (ISD and ASN). @param linkType Link type from a to b. @param count Number of parallel links. + @param a_router router of AS a default is "" + @param b_router router of AS b default is "" @throws AssertionError if link already exists or is link to self. @@ -111,10 +116,10 @@ def addIxLink(self, ix: int, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[i """ a, b = IA(*a), IA(*b) assert a.asn != b.asn, "Cannot link as{} to itself.".format(a) - assert (a, b, linkType) not in self.__links, ( + assert (a, b, a_router, b_router, linkType) not in self.__links, ( "Link between as{} and as{} of type {} at ix{} exists already.".format(a, b, linkType, ix)) - self.__ix_links[(ix, a, b, linkType)] = count + self.__ix_links[(ix, a, b, a_router, b_router, linkType)] = count return self @@ -141,7 +146,7 @@ def _doCreateGraphs(self, emulator: Emulator) -> None: reg = emulator.getRegistry() scionIsd_layer: ScionIsd = reg.get('seedemu', 'layer', 'ScionIsd') - for (a, b, rel), count in self.__links.items(): + for (a, b, a_router, b_router, rel), count in self.__links.items(): a_shape = 'doublecircle' if scionIsd_layer.isCoreAs(a.isd, a.asn) else 'circle' b_shape = 'doublecircle' if scionIsd_layer.isCoreAs(b.isd, b.asn) else 'circle' @@ -166,7 +171,7 @@ def _doCreateGraphs(self, emulator: Emulator) -> None: 'ISD{}'.format(a.isd), 'ISD{}'.format(b.isd), style= 'dashed') - for (ix, a, b, rel), count in self.__ix_links.items(): + for (ix, a, b, a_router, b_router, rel), count in self.__ix_links.items(): a_shape = 'doublecircle' if scionIsd_layer.isCoreAs(a.isd, a.asn) else 'circle' b_shape = 'doublecircle' if scionIsd_layer.isCoreAs(b.isd, b.asn) else 'circle' @@ -196,16 +201,30 @@ def print(self, indent: int = 0) -> str: out += 'ScionLayer:\n' indent += 4 - for (ix, a, b, rel), count in self.__ix_links.items(): + for (ix, a, b, a_router, b_router, rel), count in self.__ix_links.items(): out += ' ' * indent - out += f'IX{ix}: AS{a} -({rel})-> AS{b}' + if a_router == "": + out += f'IX{ix}: AS{a} -({rel})-> ' + else: + out += f'IX{ix}: AS{a}_{a_router} -({rel})-> ' + if b_router == "": + out += f'AS{b}' + else: + out += f'AS{b}_{b_router}' if count > 1: out += f' ({count} times)' out += '\n' - for (a, b, rel), count in self.__links.items(): + for (a, b, a_router, b_router, rel), count in self.__links.items(): out += ' ' * indent - out += f'XC: AS{a} -({rel})-> AS{b}' + if a_router == "": + out += f'XC: AS{a} -({rel})-> ' + else: + out += f'XC: AS{a}_{a_router} -({rel})-> ' + if b_router == "": + out += f'AS{b}' + else: + out += f'AS{b}_{b_router}' if count > 1: out += f' ({count} times)' out += '\n' @@ -215,19 +234,29 @@ def print(self, indent: int = 0) -> str: def _configure_links(self, reg: Registry, base_layer: ScionBase) -> None: """Configure SCION links with IFIDs, IPs, ports, etc.""" # cross-connect links - for (a, b, rel), count in self.__links.items(): + for (a, b, a_router, b_router, rel), count in self.__links.items(): a_reg = ScopedRegistry(str(a.asn), reg) b_reg = ScopedRegistry(str(b.asn), reg) a_as = base_layer.getAutonomousSystem(a.asn) b_as = base_layer.getAutonomousSystem(b.asn) - try: - a_router, b_router = self.__get_xc_routers(a.asn, a_reg, b.asn, b_reg) - except AssertionError: - assert False, f"cannot find XC to configure link as{a} --> as{b}" - - a_ifaddr, a_net = a_router.getCrossConnect(b.asn, b_router.getName()) - b_ifaddr, b_net = b_router.getCrossConnect(a.asn, a_router.getName()) + if a_router == "" or b_router == "": # if routers are not explicitly specified try to get them + try: + a_router, b_router = self.__get_xc_routers(a.asn, a_reg, b.asn, b_reg) + except AssertionError: + assert False, f"cannot find XC to configure link as{a} --> as{b}" + else: # if routers are explicitly specified, try to get them + try: + a_router = a_reg.get('rnode', a_router) + except AssertionError: + assert False, f"cannot find router {a_router} in as{a}" + try: + b_router = b_reg.get('rnode', b_router) + except AssertionError: + assert False, f"cannot find router {b_router} in as{b}" + + a_ifaddr, a_net, _ = a_router.getCrossConnect(b.asn, b_router.getName()) + b_ifaddr, b_net, _ = b_router.getCrossConnect(a.asn, a_router.getName()) assert a_net == b_net net = reg.get('xc', 'net', a_net) a_addr = str(a_ifaddr.ip) @@ -239,7 +268,7 @@ def _configure_links(self, reg: Registry, base_layer: ScionBase) -> None: a_addr, b_addr, net, rel) # IX links - for (ix, a, b, rel), count in self.__ix_links.items(): + for (ix, a, b, a_router, b_router, rel), count in self.__ix_links.items(): ix_reg = ScopedRegistry('ix', reg) a_reg = ScopedRegistry(str(a.asn), reg) b_reg = ScopedRegistry(str(b.asn), reg) @@ -247,9 +276,14 @@ def _configure_links(self, reg: Registry, base_layer: ScionBase) -> None: b_as = base_layer.getAutonomousSystem(b.asn) ix_net = ix_reg.get('net', f'ix{ix}') - a_routers = a_reg.getByType('rnode') - b_routers = b_reg.getByType('rnode') - + if a_router == "" or b_router == "": # if routers are not explicitly specified get all routers in AS + a_routers = a_reg.getByType('rnode') + b_routers = b_reg.getByType('rnode') + else: # else get the specified routers + a_routers = [a_reg.get('rnode', a_router)] + b_routers = [b_reg.get('rnode', b_router)] + + # get the routers connected to the IX try: a_ixrouter, a_ixif = self.__get_ix_port(a_routers, ix_net) except AssertionError: diff --git a/seedemu/layers/ScionRouting.py b/seedemu/layers/ScionRouting.py index debf83618..0408a17ed 100644 --- a/seedemu/layers/ScionRouting.py +++ b/seedemu/layers/ScionRouting.py @@ -1,11 +1,13 @@ from __future__ import annotations import json import os.path -from typing import Dict +from typing import Dict, Tuple +from ipaddress import IPv4Address import yaml -from seedemu.core import Emulator, Node, ScionAutonomousSystem, ScionRouter +from seedemu.core import Emulator, Node, ScionAutonomousSystem, ScionRouter, Network +from seedemu.core.enums import NetworkType from seedemu.layers import Routing, ScionBase, ScionIsd @@ -149,6 +151,8 @@ def render(self, emulator: Emulator): elif type == 'csnode': csnode: Node = obj self._provision_cs_config(csnode, as_) + if as_.getGenerateStaticInfoConfig(): + self._provision_staticInfo_config(csnode, as_) # provision staticInfoConfig.json @staticmethod def __provision_base_config(node: Node): @@ -170,6 +174,195 @@ def __provision_router_config(router: ScionRouter): name = router.getName() router.setFile(os.path.join("/etc/scion/", name + ".toml"), _Templates["general"].format(name=name)) + + @staticmethod + def _get_networks_from_router(router1 : str, router2 : str, as_ : ScionAutonomousSystem) -> list[Network]: + """ + gets all networks that both router1 and router2 are part of + + NOTE: assume that any two routers in an AS are connected through a network + """ + br1 = as_.getRouter(router1) + br2 = as_.getRouter(router2) + # create list of all networks router is in + br1_nets = [intf.getNet().getName() for intf in br1.getInterfaces()] + br2_nets = [intf.getNet().getName() for intf in br2.getInterfaces()] + # find common nets + joint_nets = [as_.getNetwork(net) for net in br1_nets if net in br2_nets] + # return first one + try: + return joint_nets[0] + except: + raise Exception(f"No common network between {router1} and {router2} but they are in the same AS") + + @staticmethod + def _get_BR_from_interface(interface : int, as_ : ScionAutonomousSystem) -> str: + """ + gets the name of the border router that the ScionInterface is connected to + """ + # find name of this br + for br in as_.getRouters(): + if interface in as_.getRouter(br).getScionInterfaces(): + return br + + @staticmethod + def _get_internal_link_properties(interface : int, as_ : ScionAutonomousSystem) -> Dict[str, Dict]: + """ + Gets the internal Link Properties to all other Scion interfaces from the given interface + """ + + this_br_name = ScionRouting._get_BR_from_interface(interface, as_) + + ifs = { + "Latency": {}, + "Bandwidth": {}, + "packetDrop": {}, + "MTU": {}, + "Hops": {}, + "Geo": {}, + } + + # get Geo information for this interface if it exists + if as_.getRouter(this_br_name).getGeo(): + (lat,long,address) = as_.getRouter(this_br_name).getGeo() + ifs["Geo"] = { + "Latitude": lat, + "Longitude": long, + "Address": address + } + + # iterate through all border routers to find latency to all interfaces + for br_str in as_.getRouters(): + br = as_.getRouter(br_str) + scion_ifs = br.getScionInterfaces() + # find latency to all interfaces except itself + for other_if in scion_ifs: + if other_if != interface: + # if interfaces are on same router latency is 0ms + if br_str == this_br_name: + ifs["Latency"][str(other_if)] = "0ms" + # NOTE: omit bandwidth as it is limited by cpu if the interfaces are on the same router + ifs["packetDrop"][str(other_if)] = "0.0" + # NOTE: omit MTU if interfaces are on same router as this depends on the router + ifs["Hops"][str(other_if)] = 0 # if interface is on same router, hops is 0 + else: + net = ScionRouting._get_networks_from_router(this_br_name, br_str, as_) # get network between the two routers (Assume any two routers in AS are connected through a network) + (latency, bandwidth, packetDrop) = net.getDefaultLinkProperties() + mtu = net.getMtu() + ifs["Latency"][str(other_if)] = f"{latency}ms" + if bandwidth != 0: # if bandwidth is not 0, add it + ifs["Bandwidth"][str(other_if)] = int(bandwidth/1000) # convert bps to kbps + ifs["packetDrop"][str(other_if)] = f"{packetDrop}" + ifs["MTU"][str(other_if)] = f"{mtu}" + ifs["Hops"][str(other_if)] = 1 # NOTE: if interface is on different router, hops is 1 since we assume all routers are connected through a network + + + return ifs + + @staticmethod + def _get_xc_link_properties(interface : int, as_ : ScionAutonomousSystem) -> Tuple[int, int, float, int]: + """ + get cross connect link properties from the given interface + """ + this_br_name = ScionRouting._get_BR_from_interface(interface, as_) + this_br = as_.getRouter(this_br_name) + + if_addr = this_br.getScionInterface(interface)['underlay']["public"].split(':')[0] + + xcs = this_br.getCrossConnects() + + for xc in xcs: + (xc_if,_,linkprops) = xcs[xc] + if if_addr == str(xc_if.ip): + return linkprops + + @staticmethod + def _get_ix_link_properties(interface : int, as_ : ScionAutonomousSystem) -> Tuple[int, int, float, int]: + """ + get internet exchange link properties from the given interface + """ + this_br_name = ScionRouting._get_BR_from_interface(interface, as_) + this_br = as_.getRouter(this_br_name) + + if_addr = IPv4Address(this_br.getScionInterface(interface)['underlay']["public"].split(':')[0]) + + # get a list of all ix networks this Border Router is attached to + ixs = [ifa.getNet() for ifa in this_br.getInterfaces() if ifa.getNet().getType() == NetworkType.InternetExchange] + + for ix in ixs: + ix.getPrefix() + if if_addr in ix.getPrefix(): + lat,bw,pd = ix.getDefaultLinkProperties() + mtu = ix.getMtu() + return lat,bw,pd,mtu + + @staticmethod + def _provision_staticInfo_config(node: Node, as_: ScionAutonomousSystem): + """ + Set staticInfo configuration. + + NOTE: Links also have PacketDrop and MTU, which could be added if it was supported by staticInfoConjg.json file + """ + + staticInfo = { + "Latency": {}, + "Bandwidth": {}, + "LinkType": {}, + "Geo": {}, + "Hops": {}, + "Note": "" + } + + # iterate through all ScionInterfaces in AS + for interface in range(1,as_._ScionAutonomousSystem__next_ifid): + + ifs = ScionRouting._get_internal_link_properties(interface, as_) + xc_linkprops = ScionRouting._get_xc_link_properties(interface, as_) + if xc_linkprops: + lat,bw,pd,mtu = xc_linkprops + else: # interface is not part of a cross connect thus it must be in an internet exchange + lat,bw,pd,mtu = ScionRouting._get_ix_link_properties(interface, as_) + + + # Add Latency + if lat != 0: # if latency is not 0, add it + if not staticInfo["Latency"]: # if no latencies have been added yet empty dict + staticInfo["Latency"][str(interface)] = {} + staticInfo["Latency"][str(interface)]["Inter"] = str(lat)+"ms" + for _if in ifs["Latency"]: # add intra latency + if ifs["Latency"][_if] != "0ms": # omit 0ms latency + if not staticInfo["Latency"][str(interface)]["Intra"]: # if no intra latencies have been added yet empty dict + staticInfo["Latency"][str(interface)]["Intra"] = {} + staticInfo["Latency"][str(interface)]["Intra"][str(_if)] = ifs["Latency"][_if] + + + + # Add Bandwidth + if bw != 0: # if bandwidth is not 0, add it + if not staticInfo["Bandwidth"]: # if no bandwidths have been added yet empty dict + staticInfo["Bandwidth"][str(interface)] = {} + staticInfo["Bandwidth"][str(interface)]["Inter"] = int(bw/1000) # convert bps to kbps + if ifs["Bandwidth"]: # add intra bandwidth + staticInfo["Bandwidth"][str(interface)]["Intra"] = ifs["Bandwidth"] + + # Add LinkType + staticInfo["LinkType"][str(interface)] = "direct" # NOTE: for now all ASes are connected through CrossConnects which are docker Nets under the hood and thus direct + + # Add Geo + if ifs["Geo"]: + staticInfo["Geo"][str(interface)] = ifs["Geo"] + + # Add Hops + staticInfo["Hops"][str(interface)] = { + "Intra": ifs["Hops"], + } + + # Add Note if exists + if as_.getNote(): + staticInfo["Note"] = as_.getNote() + + # Set file + node.setFile("/etc/scion/staticInfoConfig.json", json.dumps(staticInfo, indent=2)) @staticmethod def _provision_cs_config(node: Node, as_: ScionAutonomousSystem): diff --git a/seedemu/services/ScionBwtestClientService.py b/seedemu/services/ScionBwtestClientService.py new file mode 100644 index 000000000..472d62311 --- /dev/null +++ b/seedemu/services/ScionBwtestClientService.py @@ -0,0 +1,147 @@ +from __future__ import annotations +from typing import Dict + +from seedemu.core import Node, Server, Service + + +ScionBwtestClientTemplates: Dict[str, str] = {} + +ScionBwtestClientTemplates['command_with_preference'] = """\ +sleep {wait_time}; +nohup scion-bwtestclient -s {server_addr}:{port} -sc {SC} -cs {CS} -preference {preference} >> /var/log/bwtestclient.log 2>&1 & +echo "bwtestclient started" +""" + +ScionBwtestClientTemplates['command'] = """\ +sleep {wait_time}; +nohup scion-bwtestclient -s {server_addr}:{port} -sc {SC} -cs {CS} >> /var/log/bwtestclient.log 2>&1 & +echo "bwtestclient started" +""" + + +class ScionBwtestClient(Server): + """! + @brief SCION bandwidth test client. + + The output will be written to /var/log/bwtestclient.log + """ + + __port: int + __server_addr: str + __cs: str + __sc: str + __preference: str + __wait_time: int + + def __init__(self): + """! + @brief ScionBwtestServer constructor. + """ + super().__init__() + self.__port = 40002 + self.__server_addr = "" # Server address in format ISD-AS,IP-Addr (e.g. 1-151,10.151.0.30) + self.__cs = "3,1000,30,80kbps" # Client->Server test parameter default + self.__sc = "3,1000,30,80kbps" # Server->Client test parameter default + self.__preference = None + self.__wait_time = 60 # Default time to wait before starting the client + + + def setPort(self, port: int) -> ScionBwtestClient: + """! + @brief Set port the SCION bandwidth test server listens on. + + @param port + @returns self, for chaining API calls. + """ + self.__port = port + + return self + + def setServerAddr(self, server_addr: str) -> ScionBwtestClient: + """! + @brief Set the address of the SCION bandwidth test server. + + @param server_addr + @returns self, for chaining API calls. + """ + self.__server_addr = server_addr + + return self + + def setPreference(self, preference: str) -> ScionBwtestClient: + """ + @brief Preference sorting order for paths. Comma-separated list of available sorting options: latency|bandwidth|hops|mtu + + @param preference + @returns self, for chaining API calls. + """ + self.__preference = preference + + return self + + def setCS(self, cs: str) -> ScionBwtestClient: + """ + @brief set Client->Server test parameter (default "3,1000,30,80kbps") + """ + self.__cs = cs + + return self + + def setSC(self, sc: str) -> ScionBwtestClient: + """ + @brief Server->Client test parameter (default "3,1000,30,80kbps") + """ + self.__sc = sc + + return self + + def setWaitTime(self, wait_time: int) -> ScionBwtestClient: + """ + @brief Set the time to wait before starting the client + """ + self.__wait_time = wait_time + + return self + + + def install(self, node: Node): + """! + @brief Install the service. + """ + if self.__preference: + node.appendStartCommand(ScionBwtestClientTemplates['command_with_preference'].format( + port=str(self.__port), server_addr=self.__server_addr, CS=self.__cs, SC=self.__sc, preference=self.__preference, wait_time=str(self.__wait_time))) + else: + node.appendStartCommand(ScionBwtestClientTemplates['command'].format( + port=str(self.__port), server_addr=self.__server_addr, CS=self.__cs, SC=self.__sc, preference=self.__preference, wait_time=str(self.__wait_time))) + node.appendClassName("ScionBwtestClientService") + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'SCION bandwidth test client object.\n' + return out + + +class ScionBwtestClientService(Service): + """! + @brief SCION bandwidth test client service class. + """ + + def __init__(self): + """! + @brief ScionBwtestClientService constructor. + """ + super().__init__() + self.addDependency('Base', False, False) + self.addDependency('Scion', False, False) + + def _createServer(self) -> Server: + return ScionBwtestClient() + + def getName(self) -> str: + return 'ScionBwtestClientService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'ScionBwtestClientServiceLayer\n' + return out diff --git a/seedemu/services/__init__.py b/seedemu/services/__init__.py index f68f1488d..a873a0d14 100644 --- a/seedemu/services/__init__.py +++ b/seedemu/services/__init__.py @@ -10,9 +10,8 @@ from .DHCPService import DHCPServer, DHCPService from .EthereumService import * from .ScionBwtestService import ScionBwtestService +from .ScionBwtestClientService import ScionBwtestClientService from .KuboService import * from .CAService import CAService, CAServer, RootCAStore from .ChainlinkService.ChainlinkService import ChainlinkService from .ChainlinkService.ChainlinkUserService import ChainlinkUserService - -from .KuboService import * \ No newline at end of file