Open amdfxlucas opened 8 months ago
Thanks a lot for the suggestion. This feature is something that I really want to pursue. Right now I don't have man power to look into this. Hopefully this will change starting this Fall, so we can follow up on this, as well as on the other good suggestions that you made. Thanks.
@kevin-w-du could you please at least briefly confirm my observation that two concurrent routing-daemons (FRR & BIRD ) operating next to each other on the same node, just because a particular feature can't be realised with BIRD alone, is a bit of a smell. With your reassurance I could start working out a solution. My proposed approach would be a RoutingProvider strategy-object responsible for SetUp and configuration of routers.
As of now, for a layer that is about to be rendered , there is no means to detect, if a given desired protocol X is within the capabilities of the routing software , already installed on the router node so far.[^1] Eventually the layer will detect, that another one is needed. But anyway it can't be the responsibility of the layer to set up the routing software[^2], required for its operation, on the router node itself. Rather it should merely have to delegate to the Provider to "please install yourself on this node". [^3] [^1]: except maybe with 'frr' in Node::getSoftware() [^2]: as Evpn and Mpls layers do. This also duplicates the code that does the FRR setup i.e. the start script etc. [^3]: with all the SetUp-Code etc. bundled in one place, the Provider
My point is that unless this relation of layers , their utilised protocols and the requirements they put on the enabling/implementing software, is formalised and captured in an interface, many layers will end up as a hack. And naturally stacking them on top of each other will become increasingly hair-raising and degrade other code.
@magicnat Can you respond to this? You are more knowledgeable on this part. Thanks.
@amdfxlucas
Thank you very much for the comment and the suggestion. I like the idea of RoutingProvider
, and I think this is the way to go if we want to support additional routing features.
We initially went with BIRD as the goal was only to emulate BGP IPv4 unicast routing. We also wanted the students to get some idea on how to configure BGP (e.g., peering, filtering, doing hijacks, etc.) as part of a lab. I used BIRD as I found that the declarative style of its config is easier to work with and to explain (vs. the more "procedural" style of the FRR/Cisco-like config).
MPLS at the time was more of a proof-of-concept (and EVPN support is incomplete) so I just "hacked" FRR on top of that to provide LDP support.
For brnode/rnode mentioned in #20, I'm unsure if this can be easily added without needing to fix all the current createRouter
calls. Maybe an approach similar to the MPLS layer (i.e., check if a node has connections to networks outside the domain) can be used to determine if a router has inter-domain connectivity at compile time?
@magicnat @kevin-w-du Could you please review https://github.com/seed-labs/seed-emulator/pull/181#issue-2164803611
@magicnat I've had a few thoughts about how to implement different routing daemons. I came up with the idea to use some kind of an intermediate routing configuration (specification object) that is syntax independent and any routing provider can understand. Each node has one configuration , that is initially empty. Ever RoutingProtocol Layer in the simulation would then just add its desired protocol to this specification (on the nodes it sees fit i.e. routers for igp or border routers for egp ). The actual set-up of Routing (RoutingLayer::configure) would move from the beginning to the very end , after all routing requirements are collected. (i.e. Ebgp.addDependency('Routing', reverse=True ) so that Routing layer is rendered last ) The individual protocols from the thus obtained routing-blueprint are then allocated to one of the available RoutingProviders according to its capabilities by the RoutingLayer. It will default to try BIRD first, but for some protocols i.e. PIM it will have to dispatch to FRR if it is found in the specification list, as its beyond BIRDs capabilities. This way any additional software is only installed on nodes if necessary.
Could you please have a look at it and give your opinion if its feasible or desirable at all ...
class RoutingProtocol(IntFlag):
NONE = auto() #neutral element for |= operator
MPLS = auto()
SCION = auto()
EBGP = auto()
OSPFv2 = auto()
OSPFv3 = auto()
BABEL = auto()
ISIS = auto()
EIGRP = auto()
RIPv2 = auto()
RIPng = auto()
PIM-SM = auto()
MLD = auto()
....
class RoutingProviderType(IntFlag):
NONE = auto() # neutral element for |= operator
BIRD = auto()
FRR = auto()
# mrouted , Quagga , XORP ..
class Node:
""" this way Layers can access and manipulate the nodes' Spec
an interior router i.e. will have no BGP or SCION entry here whereas a border router will
"""
def getRoutingSpecification(self) -> RoutingConfiguration:
class RouterConfiguration:
"""
nested dictionary that stores all necessary information
for the RoutingProvider to render a config file from it.
Every node has its own instance.
(specification object design pattern)
"""
def __init__(self, router_id):
self.router_id = router_id
self.ipv4_tables = []
self.protocols = []
# Define mapping of protocol types to allowed keyword arguments
self.protocol_args_map = {
"import": ["all", "none", "filter{..}","where"],
"export": ["all", "none", "filter{..}","where"],
"ipv4": ["import", "export","table", "peer_table", "igp_table","next_hop"],
"ipv6": ["import", "export","table", "peer_table", "igp_table","next_hop"],
"area": ["interfaces"],
"device": [],
"kernel": ["ipv4","ipv6", "learn"]
"direct": ["table_name", "interfaces"],
"pipe": ["table_name", "peer_table", "import", "export"],
"ospf": ["ospf_name", "table_name", "area", "ipv4","ipv6"],
"bgp": ["bgp_name, "table_name", "neighbors" , "igp_table", "ipv4","ipv6" ],
"rip":
"eigrp":
..
}
# no more addTables/Pipes method as this is BIRD specific and can be decuced from the configured protocols
# combined with the arguments for each protocol
def add_protocol(self, protocol_type, **kwargs):
# Validate keyword arguments based on protocol type
valid_args = self.protocol_args_map.get(protocol_type, [])
for arg in kwargs:
# this actually had to be done recursively if 'arg' is still a dict
if arg not in valid_args:
raise ValueError(f"Invalid argument '{arg}' for protocol type '{protocol_type}'")
protocol = {'protocol_type': protocol_type}
protocol.update(kwargs)
self.protocols.append(protocol)
# Example Usage:
config = node.getRoutingSpecification()
# OSPF layer adds Ospf
config.add_protocol(protocol_type="ospf", proto_name="ospf1",
ipv4={'table_name': "t_ospf", 'area': "0", 'interfaces': ["dummy0", "ix15", "net0"]})
# actually only the interfaces are necessary here, the table name can be deterministicly deduced from the protocol
# EBGP layer does some logic with the config and determines that ospf is already present
# so it decides to use the 't_ospf' table for IGP
config.add_protocol(protocol_type="bgp", proto_name="c_as152", ipv4={'table_name': "t_bgp", "import": "all" , "export": "all" , "ibgp_table": "t_ospf" } ,
"local": {ip:"10.101.0.150", asn: 150} , "neighbors": [ {ip: "10.101.0.152", asn: 152}]
,"ibgp": { "neighbors": [{"ip": "10.0.0.1","asn":150},
{"ip":"10.0.0.2","asn":150},{"ip": "10.0.0.3", "asn": 150},
{"local": "10.0.0.4","asn": 150} }
)
# protocols specific to BIRD
# device and kernel are always present
# pipes are added on demand for each added protocol (by the BIRD routing provider)
# tables are added as they are first mentioned
config.add_protocol(protocol_type="device")
config.add_protocol(protocol_type="kernel", ipv4={'import': 'all', 'export': 'all'})
config.add_protocol(protocol_type="direct", proto_name="local_nets", ipv4={'table_name': "t_direct", 'interface': "net0"})
config.add_protocol(protocol_type="pipe", proto_name="", ipv4={'table_name': "t_bgp", 'peer_table': "master4", 'import_policy': "none", 'export_policy': "all"})
config.add_protocol(protocol_type="pipe", proto_name="", ipv4={'table_name': "t_ospf", 'peer_table': "master4", 'import_policy': "none", 'export_policy': "all"})
config.add_protocol(protocol_type="pipe", proto_name="",
ipv4={'table_name': "t_direct", 'peer_table': "t_bgp",
'import_policy': "none", 'export_policy': "filter { bgp_large_community.add(LOCAL_COMM); bgp_local_pref = 40; accept; };"})
# the 'import' and 'export' fields can also be meaningful defaults, depending on the reason why the pipe was created
class BIRDRouting(RoutingProvider):
"""
takes a RoutingSpecification and implements it on a node with the BIRD routing daemon
(strategy object design pattern)
"""
@staticmethod
def capabilities() -> RoutingProtocol:
""" @brief which protocols is this routing daemon instance capable of
"""
def activeProtocols(self,node) -> RoutingProtocol:
""" @brief which protocols are configured with this provider on this node (subset of capabilities)
"""
def install( self, node: Node):
# add software or change BaseSystem
def configure(self, node: Node, mask: RoutingProtocol):
"""render the /etc/bird.config file with all protocols that are allocated to this
routing provider in the bitmask """
# this can be implemented any way you want. i.e. with a jinja template
def render_config( spec: RouterConfiguration, mask: RoutingProtocol ) -> str:
# for simplicity the mask is ignored here and just all protocols are taken on by this provider
bird_config = f'router id {spec.router_id};\n'
for protocol in self.protocols:
if protocol['protocol_type'] == 'device':
bird_config += 'protocol device {\n}\n'
elif protocol['protocol_type'] == 'kernel':
bird_config += 'protocol kernel {\n'
bird_config += f" ipv4 {{ import {protocol['ipv4']['import']}; export {protocol['ipv4']['export']}; }};\n"
bird_config += ' learn;\n'
bird_config += '}\n'
elif protocol['protocol_type'] == 'direct':
bird_config += f"protocol direct local_nets {{\n ipv4 {{ table {protocol['ipv4']['table_name']}; import {protocol['ipv4']['import']}; }};\n interface \"{protocol['ipv4']['interface']}\";\n}}\n"
elif protocol['protocol_type'] == 'ospf':
bird_config += f"protocol ospf ospf1 {{\n ipv4 {{ table {protocol['ipv4']['table_name']}; import {protocol['ipv4']['import']}; export {protocol['ipv4']['export']}; }};\n area {protocol['ipv4']['area']} {{\n"
for interface in protocol['ipv4']['interfaces']:
bird_config += f" interface \"{interface}\" {{ hello 1; dead count 2; }};\n"
bird_config += ' };\n}\n'
elif protocol['protocol_type'] == 'pipe':
bird_config += f"protocol pipe {{\n table {protocol['ipv4']['table_name']};\n peer table {protocol['ipv4']['peer_table']};\n import {protocol['ipv4']['import_policy']};\n export {protocol['ipv4']['export_policy']};\n}}\n"
elif protocol['protocol_type'] == 'bgp':
bird_config += f"protocol bgp {}\n"
bird_config += f"ipv4 {{ table {protocol['ipv4']['table_name']};\n import {protocol['ipv4']['import']}; export {protocol['ipv4']['export']};"
bird_config += f"local {protocol['local']['ip']} as {protocol['local']['asn']} \n"
for neighbor in protocol['neighbors']:
bird_config += f"neighbor {neighbor['ip']} as {neighbor['asn']}\n"
for neighb,i in enumerate( protocol['ibgp']['neighbors']):
bird_config += f"protocol bgp ibgp{i} {{\n"
bird_config += f"local {protocol['local']['ip']} as {protocol['local']['asn']}\n"
assert neighb['asn'] == protocol['local']['asn'] # check for misconfiguration
bird_config += f"neighbor {neighb['ip']} as {protocol['local']['asn']}\n"
bird_config += f"}\n"
return bird_config
class FFRouting(RoutingProvider):
@staticmethod
def capabilities() -> RoutingProtocol:
""" @brief which protocols is this routing daemon instance capable of
a whole lot more than BIRD ..
"""
def install(node: Node):
# add /etc/frr.conf file etc. here ..
# FRR has no notion of tables, so it just ignores them
def render_config(spec: RoutingConfiguration, mask: RoutingProtocol ):
# throw an exception here if I'm supposed to implement a protocol which is beyond my
# capabilities
frr_config = f'router id {self.router_id};\n'
for protocol in self.protocols:
elif protocol['protocol_type'] == 'ospf':
frr_config += f"router ospf ospf1 {{\n ospf router-id {self.router_id};\n redistribute connected;\n area {protocol['ipv4']['area']} {{\n"
for interface in protocol['ipv4']['interfaces']:
frr_config += f" interface {interface} area {protocol['ipv4']['area']};\n"
frr_config += ' };\n}\n'
elif protocol['protocol_type'] == 'bgp':
frr_config += f"router bgp {protocol["local"]["asn"]}\n"
frr_config += f" bgp router-id {protocol["local"]["ip"]}\n"
for neighbor in protocol['neighbors']:
frr_config += f"neighbor {neighbor['ip']} remote-as {neighbor['asn']}\n"
return frr_config
Hi @amdfxlucas - thank you so much for all the contributions. I really appreciate the help.
However I just recently moved to a new country and started a new job - and I am still sorting out all the logistics. I will take a closer look once I am more settled down.
@kevin-w-du realistic network simulations require a realistic topology, so most simulators allow to read it from file. There are various formats output by different topology generators i.e. :
Here is an attempt to support this with SEED as well.