diff --git a/examples/scion/S09-scion-ip-gateway/README.md b/examples/scion/S09-scion-ip-gateway/README.md new file mode 100644 index 000000000..31959926e --- /dev/null +++ b/examples/scion/S09-scion-ip-gateway/README.md @@ -0,0 +1,55 @@ +# SCION IP Gateway + +In this example we demonstrate how to set up SCION IP gateways between ASes. + +first we have to import the ScionSIGService class from seedemu.services + +```python +from seedemu.services import ScionSIGService +``` + +Then we create the SIG service + +```python +sig = ScionSIGService() +``` + +Now we can cerate hosts in ASes and install the SIG service. For this we also have to generate a SIG configuration using the setSigConfig() function. in the ScionAutonomosSystem class. + +```python +# SIG in AS-150 +as150.createHost("sig0").joinNetwork('net0') + +as150.setSigConfig(sig_name="sig0",node_name="sig0",other_ia=(1,153), local_net = "172.16.11.0/24", remote_net = "172.16.12.0/24") +config = as150.getSigConfig("sig0") + +sig.install("sig150").setConfig("sig0",config) +emu.addBinding(Binding('sig150', filter=Filter(nodeName='sig0', asn=150))) +``` +Note that sig_name, node_name, other_ia, local_net, remote_net always have to be set where as the other parameters only need to be set if there are several sigs on the same node + +Now we can create a SIG client in another AS + +```python +as153.createHost("sig").joinNetwork('net0') + +as153.setSigConfig(sig_name="sig0",node_name="sig", other_ia=(1,150), local_net = "172.16.12.0/24", remote_net = "172.16.11.0/24") +as153.setSigConfig(sig_name="sig1",node_name="sig", other_ia=(1,151), local_net = "172.16.13.0/24", remote_net = "172.16.14.0/24", ctrl_port=30260, data_port=30261, probe_port=30857) + +config_sig0 = as153.getSigConfig("sig0") +config_sig1 = as153.getSigConfig("sig1") + + +sig.install("sig153").setConfig(sig_name="sig0",config=config_sig0) +sig.install("sig153").setConfig(sig_name="sig1",config=config_sig1) + +emu.addBinding(Binding('sig153', filter=Filter(nodeName='sig', asn=153))) +``` + +Here we also see an example of having to set the ports in the sig config if there are several sigs on the same node. + +At the end we should not forget to add the sig layer to the emulator + +```python +emu.addService(sig) +``` diff --git a/examples/scion/S09-scion-ip-gateway/scion-ip-gateway.py b/examples/scion/S09-scion-ip-gateway/scion-ip-gateway.py new file mode 100644 index 000000000..6e161af9a --- /dev/null +++ b/examples/scion/S09-scion-ip-gateway/scion-ip-gateway.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +from seedemu.compiler import Docker, Graphviz +from seedemu.core import Emulator, Binding, Filter +from seedemu.layers import ScionBase, ScionRouting, ScionIsd, Scion, Ospf +from seedemu.layers.Scion import LinkType as ScLinkType +from seedemu.services import ScionSIGService + + + +# Initialize +emu = Emulator() +base = ScionBase() +routing = ScionRouting() +scion_isd = ScionIsd() +scion = Scion() +ospf = Ospf() +sig = ScionSIGService() + +# SCION ISDs +base.createIsolationDomain(1) + +# Internet Exchange +base.createInternetExchange(100) + +# AS-150 +as150 = base.createAutonomousSystem(150) +scion_isd.addIsdAs(1, 150, is_core=True) +as150.createNetwork('net0') +as150_cs = as150.createControlService('cs1').joinNetwork('net0') +as150_router = as150.createRouter('br0') +as150_router.joinNetwork('net0').joinNetwork('ix100') +as150_router.crossConnect(153, 'br0', '10.50.0.2/29') + + +# node with node_name has to exist before its possible to create sig config +as150.createHost("sig0").joinNetwork('net0') + +as150.setSigConfig(sig_name="sig0",node_name="sig0",other_ia=(1,153), local_net = "172.16.11.0/24", remote_net = "172.16.12.0/24") +config = as150.getSigConfig("sig0") + +sig.install("sig150").setConfig("sig0",config) +emu.addBinding(Binding('sig150', filter=Filter(nodeName='sig0', 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_router = as151.createRouter('br0').joinNetwork('net0').joinNetwork('ix100') + +as151.createHost("sig").joinNetwork('net0') + +# there has to be a host with node name in AS +as151.setSigConfig(sig_name="sig0",node_name="sig", other_ia=(1,153), local_net = "172.16.14.0/24", remote_net = "172.16.13.0/24") + +config = as151.getSigConfig("sig0") + + +sig.install("sig151").setConfig("sig0",config) +emu.addBinding(Binding('sig151', filter=Filter(nodeName='sig', 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.createRouter('br0').joinNetwork('net0').joinNetwork('ix100') + +# 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_cs = as153.createControlService('cs1').joinNetwork('net0') +as153_router = as153.createRouter('br0') +as153_router.joinNetwork('net0') +as153_router.crossConnect(150, 'br0', '10.50.0.3/29') + +as153.createHost("sig").joinNetwork('net0') + +as153.setSigConfig(sig_name="sig0",node_name="sig", other_ia=(1,150), local_net = "172.16.12.0/24", remote_net = "172.16.11.0/24") +as153.setSigConfig(sig_name="sig1",node_name="sig", other_ia=(1,151), local_net = "172.16.13.0/24", remote_net = "172.16.14.0/24", ctrl_port=30260, data_port=30261, probe_port=30857) + +config_sig0 = as153.getSigConfig("sig0") +config_sig1 = as153.getSigConfig("sig1") + + +sig.install("sig153").setConfig(sig_name="sig0",config=config_sig0) +sig.install("sig153").setConfig(sig_name="sig1",config=config_sig1) + +emu.addBinding(Binding('sig153', filter=Filter(nodeName='sig', asn=153))) + + + + +# Inter-AS routing +scion.addIxLink(100, (1, 150), (1, 151), ScLinkType.Core) +scion.addIxLink(100, (1, 151), (1, 152), ScLinkType.Core) +scion.addIxLink(100, (1, 152), (1, 150), 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(ospf) +emu.addLayer(sig) + +emu.render() + +# Compilation +emu.compile(Docker(internetMapEnabled=True), './seed-compiled', override=True) +emu.compile(Graphviz(), './seed-compiled/graphviz', override=True) diff --git a/requirements.txt b/requirements.txt index 1705dcb59..e1a12c0a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,13 +37,13 @@ pycryptodome==3.21.0 python-dateutil==2.9.0.post0 PyYAML==6.0.2 referencing==0.36.2 -requests==2.22.0 +requests==2.32.3 rlp==2.0.1 rpds-py==0.23.1 six==1.17.0 toolz==1.0.0 typing_extensions==4.12.2 -urllib3==1.25.11 +urllib3==1.26.20 varint==1.0.2 web3==5.31.1 websocket-client==1.8.0 diff --git a/seedemu/core/ScionAutonomousSystem.py b/seedemu/core/ScionAutonomousSystem.py index bb5d81009..574baa160 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -142,6 +142,7 @@ class ScionAutonomousSystem(AutonomousSystem): __beaconing_policy: Dict[str, Dict] __note: str # optional free form parameter that contains interesting information about AS. This will be included in beacons if it is set __generateStaticInfoConfig: bool + __sigs_config: Dict[str, Dict] # data structure to hold configuration for sig nodes def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): """! @@ -157,6 +158,7 @@ def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): self.__beaconing_policy = {} self.__note = None self.__generateStaticInfoConfig = False + self.__sigs_config = {} # data structure to hold configuration for sig nodes def scope(self)-> Scope: return Scope(ScopeTier.AS, as_id=self.getAsn()) @@ -306,6 +308,17 @@ def getTopology(self, isd: int) -> Dict: "interfaces": rnode.getScionInterfaces() } + # SIGs + sigs = {} + for name in self.__sigs_config.keys(): + hostName = self.__sigs_config[name]["node_name"] + node = self.getHost(hostName) + addr = node.getInterfaces()[0].getAddress() + sigs[name] = { + 'ctrl_addr': f"{addr}:{self.__sigs_config[name]['ctrl_port']}", + 'data_addr': f"{addr}:{self.__sigs_config[name]['data_port']}" + } + return { 'attributes': self.getAsAttributes(isd), 'isd_as': f'{IA(isd, self.getScionAsn())}', @@ -314,6 +327,7 @@ def getTopology(self, isd: int) -> Dict: 'discovery_service': control_services, 'border_routers': border_routers, 'dispatched_ports': '31000-32767', + 'sigs': sigs, } def createControlService(self, name: str) -> Node: @@ -345,6 +359,50 @@ def getControlService(self, name: str) -> Node: """ return self.__control_services[name] + def _checkPorts(self, ctrl_port: int, data_port: int, probe_port: int, node_name: str) -> bool: + for sig_name in self.__sigs_config.keys(): + if self.__sigs_config[sig_name]["node_name"] == node_name: + if self.__sigs_config[sig_name]["ctrl_port"] == ctrl_port or self.__sigs_config[sig_name]["data_port"] == data_port or self.__sigs_config[sig_name]["probe_port"] == probe_port: + return False + return True + + def setSigConfig(self, sig_name: str, node_name: str, other_ia: IA, local_net: str, remote_net: str, ctrl_port: int = 30256, data_port: int = 30056, probe_port: int = 30856, debug_level: str = "debug") -> ScionAutonomousSystem: + """! + @brief Set the configuration for a SIG. + + @param sig_name Name of the SIG. + @param other_ia IA of the other AS. + """ + + assert sig_name not in self.__sigs_config, 'SIG with name {} already has a configuration.'.format(sig_name) + assert node_name in self.getHosts(), 'Host with name {} does not exist.'.format(node_name) + assert self._checkPorts(ctrl_port, data_port, probe_port, node_name), 'Ports are already in use.' + + + self.__sigs_config[sig_name] = { + "local_net": local_net, + "remote_net": remote_net, + "ctrl_port": ctrl_port, + "data_port": data_port, + "probe_port": probe_port, + "other_ia": other_ia, + "debug_level": debug_level, + "node_name": node_name + } + + return self + + + def getSigConfig(self, sig_name: str) -> Dict: + """! + @brief Get the configuration for a SIG. + + @param sig_name Name of the SIG. + @returns Configuration. + """ + assert sig_name in self.__sigs_config, 'SIG with name {} does not have a configuration.'.format(sig_name) + return self.__sigs_config[sig_name] + def setNote(self, note: str) -> ScionAutonomousSystem: """! @brief Set a note for the AS. diff --git a/seedemu/layers/ScionRouting.py b/seedemu/layers/ScionRouting.py index 79585d6aa..8095fa6d9 100644 --- a/seedemu/layers/ScionRouting.py +++ b/seedemu/layers/ScionRouting.py @@ -119,7 +119,7 @@ def supportedModes(cls) -> OptionMode: return OptionMode.BUILD_TIME @classmethod def default(cls): - return SetupSpecification.LOCAL_BUILD(CheckoutSpecification()) + return SetupSpecification.PACKAGES(CheckoutSpecification()) # ------------------------------------------------------------------- @@ -329,7 +329,7 @@ def configure_base(self, emulator: Emulator) -> List[Router]: if type == 'rs' : if not has_bgp: - raise RuntimeError('SCION has no concept of Route Servers.') + continue else: self._installBird(obj) self._configure_rs(obj) diff --git a/seedemu/layers/__init__.py b/seedemu/layers/__init__.py index 8eae360e8..ff4279700 100644 --- a/seedemu/layers/__init__.py +++ b/seedemu/layers/__init__.py @@ -9,4 +9,4 @@ from .ScionRouting import ScionRouting from .ScionIsd import ScionIsd from .Scion import Scion, SetupSpecification, CheckoutSpecification -from .EtcHosts import EtcHosts \ No newline at end of file +from .EtcHosts import EtcHosts diff --git a/seedemu/services/ScionSIGService.py b/seedemu/services/ScionSIGService.py new file mode 100644 index 000000000..7758eb9ce --- /dev/null +++ b/seedemu/services/ScionSIGService.py @@ -0,0 +1,195 @@ +from __future__ import annotations +from typing import Dict + +from seedemu.core import Node, Server, Service + +import json +from ipaddress import IPv4Network + + +_Templates: Dict[str, str] = {} + +_Templates["sig"] = """\ +[log] + [log.console] + level = "{log_level}" + +[tunnel] +name = "{sig_name}" +src_ipv4 = "{src_ip}" + +[gateway] +traffic_policy_file = "/etc/scion/{sig_name}.json" +ctrl_addr = "{ctrl_addr}:{ctrl_port}" +data_addr = "{data_addr}:{data_port}" +probe_addr = "{probe_addr}:{probe_port}" +""" + +_CommandTemplates: Dict[str, str] = {} + +_CommandTemplates["sig_setup"] = "ip address add {ip_addr} dev lo" + +_CommandTemplates["sig_start"] = """\ +until pgrep -xf "scion-ip-gateway --config /etc/scion/{name}.toml" > /dev/null 2>&1; +do + sleep 5; + nohup scion-ip-gateway --config /etc/scion/{name}.toml >> /var/log/{name}.log 2>&1 & +done +echo "scion-ip-gateway started" +""" + +sig_default_software = [] + +sig_default_software = ["iperf3", "net-tools"] + +class ScionSIGServer(Server): + """! + @brief SCION IP Gateway server. + """ + + __config : Dict[str, Dict[str, str]] # maps sig_name to sig_config + + def __init__(self): + """! + @brief ScionSIGServer constructor. + """ + super().__init__() + self.__config = {} + + def setConfig(self, sig_name: str, config: Dict[str,str]) -> ScionSIGServer: + """! + @brief Set the configuration. + + @param config Configuration dictionary. + """ + self.__config[sig_name] = config + + return self + + def getConfig(self, sig_name: str) -> Dict[str,str]: + """ + Get the configuration for the SIG. + """ + return self.__config[sig_name] + + def getSIGs(self) -> List[str]: + """ + Get the names of the SIGs. + """ + return list(self.__config.keys()) + + def _append_sig_command(self, node: Node): + """ + Append commands for starting the SCION SIG stack on the node. + """ + for sig_name in self.__config.keys(): + # get config + config = self.__config[sig_name] + ip = IPv4Network(config["local_net"]).network_address + 1 + # add setup command + node.appendStartCommand(_CommandTemplates["sig_setup"].format(ip_addr=ip)) + node.appendStartCommand(_CommandTemplates['sig_start'].format(name=sig_name)) + + def _provision_sig_config(self, node: Node, sig_name: str): + + config = self.__config[sig_name] + + + # provision sig.json + + isd, as_num = config["other_ia"] + remote_net = config["remote_net"] + + sig_json = { + "ASes": { + f"{isd}-{as_num}": { + "Nets": [ + remote_net + ] + } + }, + "ConfigVersion": 9001 + } + + node.setFile(f"/etc/scion/{sig_name}.json", json.dumps(sig_json, indent=4)) + + # provision sig.toml + + local_net = config["local_net"] + + local_net = IPv4Network(local_net) + src_ip = local_net.network_address + 1 + + src_ip = str(src_ip) + + ip = node.getInterfaces()[0].getAddress() + ctrl_port = config["ctrl_port"] + data_port = config["data_port"] + debug_level = config["debug_level"] + probe_port = config["probe_port"] + + sig_toml = _Templates["sig"].format( + log_level=debug_level, + src_ip=src_ip, + ctrl_addr=ip, + ctrl_port=ctrl_port, + probe_addr=ip, + probe_port=probe_port, + data_addr=ip, + data_port=data_port, + sig_name=sig_name + ) + + node.setFile(f"/etc/scion/{sig_name}.toml", sig_toml) + + def _provision_sig_configs(self, node: Node): + """ + @brief Provision the SIG configurations. + """ + for sig_name in self.__config.keys(): + self._provision_sig_config(node, sig_name) + + def install(self, node: Node): + """! + @brief Install the service. + """ + [node.addSoftware(software) for software in sig_default_software] # add default software + + node.addBuildCommand( + "apt-get update && apt-get install -y scion-sig") + + self._append_sig_command(node) + + self._provision_sig_configs(node) + + node.appendClassName("ScionSIGService") + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'SCION IP Gateway server object.\n' + return out + + +class ScionSIGService(Service): + """! + @brief SCION IP Gateway service. + """ + + def __init__(self): + """! + @brief ScionSIGService constructor. + """ + super().__init__() + self.addDependency('Base', False, False) + self.addDependency('Scion', False, False) + + def _createServer(self) -> Server: + return ScionSIGServer() + + def getName(self) -> str: + return 'ScionSIGService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'ScionSIGServiceLayer\n' + return out diff --git a/seedemu/services/__init__.py b/seedemu/services/__init__.py index eaeb6b2e3..52b10047b 100644 --- a/seedemu/services/__init__.py +++ b/seedemu/services/__init__.py @@ -15,4 +15,5 @@ from .CAService import CAService, CAServer, RootCAStore from .ChainlinkService import * from .TrafficService import * -from .DevService import * \ No newline at end of file +from .DevService import * +from .ScionSIGService import *