seed-emulator icon indicating copy to clipboard operation
seed-emulator copied to clipboard

Support Intra-Domain Topology for ASes

Open amdfxlucas opened this issue 6 months ago • 7 comments

@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. :

  • BRITE
  • INET
  • CAIDA
  • ORBIS

Here is an attempt to support this with SEED as well.

intra_domain_topo_reader09 Screenshot from 2024-02-12 17-15-56

amdfxlucas avatar Feb 15 '24 15:02 amdfxlucas

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 avatar Feb 15 '24 16:02 kevin-w-du

@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.

amdfxlucas avatar Feb 16 '24 10:02 amdfxlucas

@magicnat Can you respond to this? You are more knowledgeable on this part. Thanks.

kevin-w-du avatar Feb 16 '24 18:02 kevin-w-du

@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 avatar Feb 17 '24 16:02 magicnat

@magicnat @kevin-w-du Could you please review https://github.com/seed-labs/seed-emulator/pull/181#issue-2164803611

amdfxlucas avatar Mar 02 '24 13:03 amdfxlucas

@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

amdfxlucas avatar Mar 07 '24 18:03 amdfxlucas

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.

magicnat avatar Mar 07 '24 19:03 magicnat