class NetworkIXLan(models.Model): asn = ASNField(verbose_name="ASN") ipaddr4 = InetAddressField( verbose_name="IPv4", validators=[AddressFamilyValidator(4)], blank=True, null=True, ) ipaddr6 = InetAddressField( verbose_name="IPv6", validators=[AddressFamilyValidator(6)], blank=True, null=True, ) is_rs_peer = models.BooleanField(default=False) notes = models.CharField(max_length=255, blank=True) speed = models.PositiveIntegerField() operational = models.BooleanField(default=True) net = models.ForeignKey( Network, default=0, related_name="netixlan_set", verbose_name="Network", on_delete=models.CASCADE, ) ixlan = models.ForeignKey( IXLan, default=0, related_name="netixlan_set", verbose_name="Internet Exchange LAN", on_delete=models.CASCADE, ) ignored_fields = ["ix_id", "name"] class Meta: verbose_name = "Public Peering Exchange Point" verbose_name_plural = "Public Peering Exchange Points"
class NetworkIXLAN(models.Model): asn = ASNField() name = models.CharField(max_length=255) ipaddr6 = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(6)], ) ipaddr4 = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(4)], ) is_rs_peer = models.BooleanField(default=False) ix_id = models.PositiveIntegerField() ixlan_id = models.PositiveIntegerField() objects = NetManager() logger = logging.getLogger("peering.manager.peeringdb") class Meta: ordering = ["asn", "ipaddr6", "ipaddr4"] verbose_name = "Network IX LAN" verbose_name_plural = "Network IX LANs" def save(self, *args, **kwargs): super().save(*args, **kwargs) # Ignore if we do not have any IP addresses if not self.ipaddr6 and not self.ipaddr4: self.logger.debug( "network ixlan with as%s and ixlan id %s ignored" ", no ipv6 and no ipv4", self.asn, self.ixlan_id, ) return # Trigger the build of a new PeerRecord or just ignore if it already # exists, assumes it exists # Note that there is not point of doing the same thing when a Network # or a NetworkIXLAN is deleted because it will automatically delete the # PeerRecord linked to it using the foreign key (with the CASCADE mode) network = None peer_record_exists = True try: # Try guessing the Network given its ASN network = Network.objects.get(asn=self.asn) # Try finding if the PeerRecord already exists PeerRecord.objects.get(network=network, network_ixlan=self) except Network.DoesNotExist: # The network does not exist, well not much to do self.logger.debug( "network with as%s does not exist, required for " "peer record creation", self.asn, ) except PeerRecord.DoesNotExist: # But if the exception is raised, it does not peer_record_exists = False # If the PeerRecord does not exist, create it if not peer_record_exists: PeerRecord.objects.create(network=network, network_ixlan=self) self.logger.debug( "peer record created with as%s and ixlan id %s", self.asn, self.ixlan_id ) else: self.logger.debug( "peer record with as%s and ixlan id %s exists", self.asn, self.ixlan_id ) def __str__(self): return "AS{} on {} - IPv6: {} - IPv4: {}".format( self.asn, self.name, self.ipaddr6, self.ipaddr4 )
class Connection(ChangeLoggedModel, TaggableModel): peeringdb_netixlan = models.ForeignKey("peeringdb.NetworkIXLan", on_delete=models.SET_NULL, blank=True, null=True) state = models.CharField(max_length=20, choices=ConnectionState.choices, default=ConnectionState.ENABLED) vlan = VLANField(verbose_name="VLAN", blank=True, null=True) ipv6_address = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(6)], ) ipv4_address = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(4)], ) internet_exchange_point = models.ForeignKey("peering.InternetExchange", blank=True, null=True, on_delete=models.SET_NULL) router = models.ForeignKey("peering.Router", blank=True, null=True, on_delete=models.SET_NULL) interface = models.CharField(max_length=200, blank=True) description = models.CharField(max_length=200, blank=True) comments = models.TextField(blank=True) objects = NetManager() @property def name(self): return str(self) @property def linked_to_peeringdb(self): """ Tells if the PeeringDB object for this connection still exists. """ return self.peeringdb_netixlan is not None def __str__(self): s = "" if self.internet_exchange_point: s += str(self.internet_exchange_point) if self.router: if s: s += " on " s += str(self.router) if self.interface: s += f" {self.interface}" return s or f"Connection #{self.pk}" def get_absolute_url(self): return reverse("net:connection_details", kwargs={"pk": self.pk}) def link_to_peeringdb(self): """ Retrieves the PeeringDB ID for this IX connection based on the IP addresses that have been recorded. The PeeringDB record will be returned on success. In any other cases `None` will be returned. The value will also be saved in the corresponding field of the model. """ try: netixlan = NetworkIXLan.objects.get(ipaddr6=self.ipv6_address, ipaddr4=self.ipv4_address) except NetworkIXLan.DoesNotExist: return None self.peeringdb_netixlan = netixlan self.save() return netixlan
class NetworkIXLan(models.Model): asn = ASNField(verbose_name="ASN") ipaddr4 = InetAddressField( verbose_name="IPv4", validators=[AddressFamilyValidator(4)], blank=True, null=True, ) ipaddr6 = InetAddressField( verbose_name="IPv6", validators=[AddressFamilyValidator(6)], blank=True, null=True, ) is_rs_peer = models.BooleanField(default=False) notes = models.CharField(max_length=255, blank=True) speed = models.PositiveIntegerField() operational = models.BooleanField(default=True) net = models.ForeignKey( to="peeringdb.Network", default=0, related_name="netixlan_set", verbose_name="Network", on_delete=models.CASCADE, ) ixlan = models.ForeignKey( to="peeringdb.IXLan", default=0, related_name="netixlan_set", verbose_name="Internet Exchange LAN", on_delete=models.CASCADE, ) ignored_fields = ["ix_id", "name"] class Meta: verbose_name = "Public Peering Exchange Point" verbose_name_plural = "Public Peering Exchange Points" @property def cidr4(self): try: return self.cidr(address_family=4) except ValueError: return None @property def cidr6(self): try: return self.cidr(address_family=6) except ValueError: return None def get_ixlan_prefix(self, address_family=0): """ Returns matching `CidrAddressField` containing this `NetworkIXLan`'s IP addresses. When `address_family` is set to `4` or `6` only the prefix also matching the address family will be returned. """ prefixes = IXLanPrefix.objects.filter(ixlan=self.ixlan) if address_family in (4, 6): prefixes = prefixes.filter(prefix__family=address_family) r = [] if address_family != 6: for p in prefixes: if self.ipaddr4 in p.prefix: r.append(p.prefix) break if address_family != 4: for p in prefixes: if self.ipaddr6 in p.prefix: r.append(p.prefix) break return r if len(r) != 1 else r[0] def cidr(self, address_family=4): """ Returns a Python IP interface object with the IP address and prefix length set. """ if address_family not in (4, 6): raise ValueError("Address family must be 4 or 6") if address_family == 4 and not self.ipaddr4: raise ValueError("IPv4 address is not set") if address_family == 6 and not self.ipaddr6: raise ValueError("IPv6 address is not set") prefix = self.get_ixlan_prefix(address_family=address_family) address = self.ipaddr4 if address_family == 4 else self.ipaddr6 return ipaddress.ip_interface(f"{address.ip}/{prefix.prefixlen}")
class InternetExchange(ChangeLoggedModel, TemplateModel): peeringdb_id = models.PositiveIntegerField(blank=True, default=0) name = models.CharField(max_length=128) slug = models.SlugField(unique=True) ipv6_address = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(6)], ) ipv4_address = InetAddressField( store_prefix_length=False, blank=True, null=True, validators=[AddressFamilyValidator(4)], ) comment = models.TextField(blank=True) configuration_template = models.ForeignKey( "ConfigurationTemplate", blank=True, null=True, on_delete=models.SET_NULL ) import_routing_policies = models.ManyToManyField( "RoutingPolicy", blank=True, related_name="%(class)s_import_routing_policies" ) export_routing_policies = models.ManyToManyField( "RoutingPolicy", blank=True, related_name="%(class)s_export_routing_policies" ) router = models.ForeignKey( "Router", blank=True, null=True, on_delete=models.SET_NULL ) check_bgp_session_states = models.BooleanField(default=False) bgp_session_states_update = models.DateTimeField(blank=True, null=True) communities = models.ManyToManyField("Community", blank=True) objects = NetManager() logger = logging.getLogger("peering.manager.peering") class Meta: ordering = ["name"] permissions = [ ("view_configuration", "Can view Internet Exchange's configuration"), ("deploy_configuration", "Can deploy Internet Exchange's configuration"), ] def get_absolute_url(self): return reverse("peering:internet_exchange_details", kwargs={"slug": self.slug}) def get_peering_sessions_list_url(self): return reverse( "peering:internet_exchange_peering_sessions", kwargs={"slug": self.slug} ) def get_peer_list_url(self): return reverse("peering:internet_exchange_peers", kwargs={"slug": self.slug}) def get_peering_sessions(self): return self.internetexchangepeeringsession_set.all() def get_autonomous_systems(self): autonomous_systems = [] for session in self.internetexchangepeeringsession_set.all(): if session.autonomous_system not in autonomous_systems: autonomous_systems.append(session.autonomous_system) return autonomous_systems def is_peeringdb_valid(self): """ Tells if the PeeringDB ID for this IX is still valid. This function will return true if the PeeringDB record for this IX is valid or if this IX does not have a Peering DB ID set. In any other cases, the return value will be false. """ if self.peeringdb_id: peeringdb_record = PeeringDB().get_ix_network(self.peeringdb_id) if not peeringdb_record: return False return True def get_peeringdb_id(self): """ Retrieves the PeeringDB ID for this IX based on the IP addresses that have been recorded. The ID of the PeeringDB record will be returned on success. In any other cases 0 will be returned. """ network_ixlan = PeeringDB().get_ix_network_by_ip_address( ipv6_address=self.ipv6_address, ipv4_address=self.ipv4_address ) return network_ixlan.id if network_ixlan else 0 def get_prefixes(self): """ Returns a list of prefixes found in PeeringDB for this IX. """ return PeeringDB().get_prefixes_for_ix_network(self.peeringdb_id) def _get_configuration_variables(self): peers6 = {} peers4 = {} # Sort peering sessions based on IP protocol version for session in self.internetexchangepeeringsession_set.all(): if session.ip_address_version == 6: if session.autonomous_system.asn not in peers6: peers6[ session.autonomous_system.asn ] = session.autonomous_system.to_dict() peers6[session.autonomous_system.asn].update({"sessions": []}) peers6[session.autonomous_system.asn]["sessions"].append( session.to_dict() ) if session.ip_address_version == 4: if session.autonomous_system.asn not in peers4: peers4[ session.autonomous_system.asn ] = session.autonomous_system.to_dict() peers4[session.autonomous_system.asn].update({"sessions": []}) peers4[session.autonomous_system.asn]["sessions"].append( session.to_dict() ) return { "my_asn": settings.MY_ASN, "internet_exchange": self.to_dict(), "peering_groups": [ {"ip_version": 6, "peers": peers6}, {"ip_version": 4, "peers": peers4}, ], } def generate_configuration(self): if not self.configuration_template: return "" return self.configuration_template.render(self._get_configuration_variables()) def get_available_peers(self): # Not linked to PeeringDB, cannot determine peers if not self.peeringdb_id: return None # Get the IX LAN we are belonging to api = PeeringDB() network_ixlan = api.get_ix_network(self.peeringdb_id) # Get all peering sessions currently existing existing_sessions = self.get_peering_sessions() ipv4_sessions = [] ipv6_sessions = [] for session in existing_sessions: ip = ipaddress.ip_address(session.ip_address) if ip.version == 6: ipv6_sessions.append(str(ip)) elif ip.version == 4: ipv4_sessions.append(str(ip)) else: self.logger.debug("peering session with strange ip: %s", ip) # Find all peers belonging to the same IX and order them by ASN # Exclude our own ASN and already existing sessions return PeerRecord.objects.filter( Q(network_ixlan__ixlan_id=network_ixlan.ixlan_id) & ~Q(network__asn=settings.MY_ASN) & ( ~Q(network_ixlan__ipaddr6__in=ipv6_sessions) | ~Q(network_ixlan__ipaddr4__in=ipv4_sessions) ) ).order_by("network__asn") def _import_peering_sessions(self, sessions=[], prefixes=[]): # No sessions or no prefixes, can't work with that if not sessions or not prefixes: return None # Values to be returned number_of_peering_sessions = 0 number_of_autonomous_systems = 0 ignored_autonomous_systems = [] with transaction.atomic(): # For each session check if the address fits in one of the prefixes for session in sessions: for prefix in prefixes: # No point of checking if a session fits inside a prefix if # they are not using the same IP version if session["ip_address"].version is not prefix.version: self.logger.debug( "ip %s cannot fit in prefix %s (not same ip version) ignoring", str(session["ip_address"]), str(prefix), ) continue self.logger.debug( "checking if ip %s fits in prefix %s", str(session["ip_address"]), str(prefix), ) # If the address fits, create a new InternetExchangePeeringSession # object and a new AutonomousSystem object if they does not exist # already if session["ip_address"] in prefix: ip_address = str(session["ip_address"]) remote_asn = session["remote_asn"] self.logger.debug( "ip %s fits in prefix %s", ip_address, str(prefix) ) if not InternetExchangePeeringSession.does_exist( ip_address=ip_address, internet_exchange=self ): self.logger.debug( "session %s with as%s does not exist", ip_address, remote_asn, ) # Grab the AS, create it if it does not exist in # the database yet autonomous_system = AutonomousSystem.does_exist(remote_asn) if not autonomous_system: self.logger.debug( "as%s not present importing from peeringdb", remote_asn, ) autonomous_system = AutonomousSystem.create_from_peeringdb( remote_asn ) # Do not count the AS if it does not have a # PeeringDB record if autonomous_system: self.logger.debug("as%s created", remote_asn) number_of_autonomous_systems += 1 else: if remote_asn not in ignored_autonomous_systems: ignored_autonomous_systems.append(remote_asn) self.logger.debug( "could not create as%s, session %s ignored", remote_asn, ip_address, ) # Only add a peering session if we were able to # actually use the AS it is linked to if autonomous_system: self.logger.debug("creating session %s", ip_address) values = { "autonomous_system": autonomous_system, "internet_exchange": self, "ip_address": ip_address, } peering_session = InternetExchangePeeringSession( **values ) peering_session.save() number_of_peering_sessions += 1 self.logger.debug("session %s created", ip_address) else: self.logger.debug( "session %s with as%s already exists", ip_address, remote_asn, ) else: self.logger.debug( "ip %s do not fit in prefix %s", str(session["ip_address"]), str(prefix), ) return ( number_of_autonomous_systems, number_of_peering_sessions, ignored_autonomous_systems, ) def import_peering_sessions_from_router(self): log = 'ignoring peering session on {}, reason: "{}"' if not self.router: log = log.format(self.name.lower(), "no router attached") elif not self.router.platform: log = log.format(self.name.lower(), "router with unsupported platform") else: log = None # No point of discovering from router if platform is none or is not # supported. if log: self.logger.debug(log) return False # Build a list based on prefixes based on PeeringDB records prefixes = self.get_prefixes() # No prefixes found if not prefixes: self.logger.debug("no prefixes found for %s", self.name.lower()) return None else: self.logger.debug( "found %s prefixes (%s) for %s", len(prefixes), ", ".join([str(prefix) for prefix in prefixes]), self.name.lower(), ) # Gather all existing BGP sessions from the router connected to the IX bgp_sessions = self.router.get_napalm_bgp_neighbors() return self._import_peering_sessions(bgp_sessions, prefixes) def update_peering_session_states(self): # Check if we are able to get BGP details log = 'ignoring session states on {}, reason: "{}"' if not self.router: log = log.format(self.name.lower(), "no router attached") elif not self.router.can_napalm_get_bgp_neighbors_detail(): log = log.format( self.name.lower(), "router with unsupported platform {}".format(self.router.platform), ) elif not self.check_bgp_session_states: log = log.format(self.name.lower(), "check disabled") else: log = None # If we cannot check for BGP details, don't do anything if log: self.logger.debug(log) return False # Get all BGP sessions detail bgp_neighbors_detail = self.router.get_napalm_bgp_neighbors_detail() # An error occured, probably if not bgp_neighbors_detail: return False with transaction.atomic(): for vrf, as_details in bgp_neighbors_detail.items(): for asn, sessions in as_details.items(): # Check BGP sessions found for session in sessions: ip_address = session["remote_address"] self.logger.debug( "looking for session %s in %s", ip_address, self.name.lower(), ) # Check if the BGP session is on this IX peering_session = InternetExchangePeeringSession.does_exist( internet_exchange=self, ip_address=ip_address ) if peering_session: # Get the BGP state for the session state = session["connection_state"].lower() received = session["received_prefix_count"] advertised = session["advertised_prefix_count"] self.logger.debug( "found session %s in %s with state %s", ip_address, self.name.lower(), state, ) # Update fields peering_session.bgp_state = state peering_session.received_prefix_count = ( received if received > 0 else 0 ) peering_session.advertised_prefix_count = ( advertised if advertised > 0 else 0 ) # Update the BGP state of the session if peering_session.bgp_state == BGP_STATE_ESTABLISHED: peering_session.last_established_state = timezone.now() peering_session.save() else: self.logger.debug( "session %s in %s not found", ip_address, self.name.lower(), ) # Save last session states update self.bgp_session_states_update = timezone.now() self.save() return True def __str__(self): return self.name
class Connection(ChangeLoggedModel, TaggableModel): peeringdb_netixlan = models.ForeignKey(to="peeringdb.NetworkIXLan", on_delete=models.SET_NULL, blank=True, null=True) state = models.CharField(max_length=20, choices=ConnectionState.choices, default=ConnectionState.ENABLED) vlan = VLANField(verbose_name="VLAN", blank=True, null=True) ipv6_address = InetAddressField( store_prefix_length=True, blank=True, null=True, validators=[AddressFamilyValidator(6)], ) ipv4_address = InetAddressField( store_prefix_length=True, blank=True, null=True, validators=[AddressFamilyValidator(4)], ) internet_exchange_point = models.ForeignKey(to="peering.InternetExchange", blank=True, null=True, on_delete=models.CASCADE) router = models.ForeignKey(to="peering.Router", blank=True, null=True, on_delete=models.SET_NULL) interface = models.CharField(max_length=200, blank=True) description = models.CharField(max_length=200, blank=True) config_context = models.JSONField(blank=True, null=True) comments = models.TextField(blank=True) objects = NetManager() @property def name(self): return str(self) @property def linked_to_peeringdb(self): """ Tells if the PeeringDB object for this connection still exists. """ return self.peeringdb_netixlan is not None def __str__(self): s = "" if self.internet_exchange_point: s += str(self.internet_exchange_point) if self.router: if s: s += " on " s += str(self.router) if self.interface: s += f" {self.interface}" return s or f"Connection #{self.pk}" def get_absolute_url(self): return reverse("net:connection_view", args=[self.pk]) def link_to_peeringdb(self): """ Retrieves the PeeringDB ID for this IX connection based on the IP addresses that have been recorded. The PeeringDB record will be returned on success. In any other cases `None` will be returned. The value will also be saved in the corresponding field of the model. """ # If data imported from PeeringDB doesn't have IPs set, ignore it if self.ipv4_address is None and self.ipv6_address is None: return None # Prepare value for database lookup ipaddr6 = (self.ipv6_address.ip if hasattr(self.ipv6_address, "ip") else self.ipv6_address) ipaddr4 = (self.ipv4_address.ip if hasattr(self.ipv4_address, "ip") else self.ipv4_address) try: netixlan = NetworkIXLan.objects.get(ipaddr6=ipaddr6, ipaddr4=ipaddr4) logger.debug( f"linked connection {self} (pk: {self.pk}) to peeringdb") except NetworkIXLan.DoesNotExist: logger.debug( f"linking connection {self} (pk: {self.pk}) to peeringdb failed" ) return None self.peeringdb_netixlan = netixlan self.save() return netixlan