def setUpTestData(cls): # VLANs cls.vlan = { 'VLAN1': VLAN.objects.create(name="vlan1", long_name="VLAN 1", ip_network=ipaddress.IPv4Network("10.0.0.0/16")), 'VLAN2': VLAN.objects.create(name="vlan2", long_name="VLAN 2", ip_network=ipaddress.IPv4Network("10.1.0.0/16")), } # Owners cls.owner = [ Owner.objects.create(name="owner1", long_name="Owner 1", contact=""), Owner.objects.create(name="owner2", long_name="Owner 2", contact=""), ] # ISDs cls.isd = [ ISD.objects.create(isd_id=1, name="Region 1"), ] # ASes cls.asys = { 'A': AS.objects.create(asn=ASN("ff00:0:0"), isd=cls.isd[0], name="AS A", owner=cls.owner[0], is_core=False), 'B': AS.objects.create(asn=ASN("ff00:0:1"), isd=cls.isd[0], name="AS B", owner=cls.owner[1], is_core=False), } # Peering Clients cls.client = { 'A-1': PeeringClient.objects.create(asys=cls.asys['A'], name="1"), 'A-2': PeeringClient.objects.create(asys=cls.asys['A'], name="2"), 'B-1': PeeringClient.objects.create(asys=cls.asys['B'], name="1"), 'B-2': PeeringClient.objects.create(asys=cls.asys['B'], name="2"), 'B-3': PeeringClient.objects.create(asys=cls.asys['B'], name="3") } # Interfaces hosts1 = cls.vlan['VLAN1'].ip_network.hosts() hosts2 = cls.vlan['VLAN2'].ip_network.hosts() cls.interface = { 'A-1-VLAN1': Interface.objects.create(peering_client=cls.client['A-1'], vlan=cls.vlan['VLAN1'], public_ip=next(hosts1), first_port=50000, last_port=51000), 'A-2-VLAN1': Interface.objects.create(peering_client=cls.client['A-2'], vlan=cls.vlan['VLAN1'], public_ip=next(hosts1), first_port=50000, last_port=51000), 'A-2-VLAN2': Interface.objects.create(peering_client=cls.client['A-2'], vlan=cls.vlan['VLAN2'], public_ip=next(hosts2), first_port=51000, last_port=52000), 'B-1-VLAN1': Interface.objects.create(peering_client=cls.client['B-1'], vlan=cls.vlan['VLAN1'], public_ip=next(hosts1), first_port=50000, last_port=51000), 'B-2-VLAN1': Interface.objects.create(peering_client=cls.client['B-1'], vlan=cls.vlan['VLAN1'], public_ip=next(hosts1), first_port=51000, last_port=52000), 'B-2-VLAN2': Interface.objects.create(peering_client=cls.client['B-2'], vlan=cls.vlan['VLAN2'], public_ip=next(hosts2), first_port=50000, last_port=51000), 'B-3-VLAN2': Interface.objects.create(peering_client=cls.client['B-2'], vlan=cls.vlan['VLAN2'], public_ip=next(hosts2), first_port=51000, last_port=52000), }
def ListPolicies(self, request, context): """List policies of the AS making the request.""" asn_str, client = get_client_from_metadata( context.invocation_metadata()) asn = ASN(asn_str) # Prepare common selection criteria common_selection = {'asys__asn': asn} if request.vlan: common_selection['vlan__name'] = request.vlan if request.asn and request.asn != asn_str: context.abort(grpc.StatusCode.PERMISSION_DENIED, "Cannot list policies of other ASes") if request.WhichOneof("accept_") is not None: common_selection['accept'] = request.accept # Return matching default policies if request.WhichOneof('peer') is None or request.peer_everyone: for policy in DefaultPolicy.objects.filter(**common_selection): yield PolicyProtoSerializer(policy).message # Return matching AS policies if request.WhichOneof('peer') is None or request.peer_asn: policies = AsPeerPolicy.objects.filter(**common_selection) if request.peer_asn: try: policies = policies.filter( peer_as__asn=ASN(request.peer_asn)) except ValueError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid ASN") for policy in policies.filter(**common_selection): yield PolicyProtoSerializer(policy).message # Return matching owner policies if request.WhichOneof('peer') is None or request.peer_owner: policies = OwnerPeerPolicy.objects.filter(**common_selection) if request.peer_owner: policies = policies.filter(peer_owner__name=request.peer_owner) for policy in policies.filter(**common_selection): yield PolicyProtoSerializer(policy).message # Return matching ISD policies if request.WhichOneof('peer') is None or request.peer_isd: policies = IsdPeerPolicy.objects.filter(**common_selection) if request.peer_isd: try: policies = policies.filter( peer_isd__isd_id=int(request.peer_isd)) except ValueError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid ISD") for policy in policies.filter(**common_selection): yield PolicyProtoSerializer(policy).message
def CreatePolicy(self, request, context): """Create a new policy.""" asn_str, client = get_client_from_metadata( context.invocation_metadata()) asn = ASN(asn_str) if request.asn != asn_str: context.abort(grpc.StatusCode.PERMISSION_DENIED, "Cannot create policies for other ASes") serializer = PolicyProtoSerializer(message=request) if not serializer.is_valid(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, _fmt_validation_errors(serializer.errors)) _assert_policy_write_permission(context, asn, client, request.vlan) try: policy = serializer.save() except ValidationError as e: msg, code = _translate_validation_errors(e) context.abort(code, msg) # Update links and notify clients policy_resolver.update_accepted_peers(policy.vlan, policy.asys) policy_resolver.update_links(policy.vlan, policy.asys) return serializer.message
def _set_policies( self, request, context ) -> Tuple[typing.List[peering_pb2.Policy], typing.List[str]]: # Delete old policies. Parse the new ones, and try saving them to the DB. # Returns the unsuccessful policies and matching error descriptions. # context.abort() is called on fatal errors to abort the RPC and trigger a transaction # rollback. asn_str, client = get_client_from_metadata( context.invocation_metadata()) asn = ASN(asn_str) # Delete previous policies if request.vlan: try: vlan_id = VLAN.objects.get(name=request.vlan).id except VLAN.DoesNotExist: context.abort(grpc.StatusCode.NOT_FOUND, "VLAN does not exist") _assert_policy_write_permission(context, asn, client, request.vlan) _delete_policies(asn, vlan_id) else: _assert_policy_write_permission(context, asn, client) _delete_policies(asn) # Create new policies rejected_policies = [] errors = [] for policy in request.policies: serializer = PolicyProtoSerializer(message=policy) if policy.asn != asn_str: rejected_policies.append(policy) errors.append("Policy ASN belongs to foreign AS") continue if request.vlan and policy.vlan != request.vlan: rejected_policies.append(policy) errors.append("VLAN excluded by filter") continue if not serializer.is_valid(): rejected_policies.append(policy) errors.append(_fmt_validation_errors(serializer.errors)) continue try: serializer.save() except ValidationError as e: msg, _ = _translate_validation_errors(e) rejected_policies.append(policy) errors.append(msg) continue # Update links and notify clients asys = AS.objects.get(asn=asn) for vlan in asys.get_connected_vlans(): policy_resolver.update_accepted_peers(vlan, asys) policy_resolver.update_links(vlan, asys) return rejected_policies, errors
def DestroyPolicy(self, request, context): """Delete a policy.""" asn_str, client = get_client_from_metadata( context.invocation_metadata()) asn = ASN(asn_str) if request.asn != asn_str: context.abort(grpc.StatusCode.PERMISSION_DENIED, "Cannot delete policies of other ASes") serializer = PolicyProtoSerializer(message=request) if not serializer.is_valid(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, _fmt_validation_errors(serializer.errors)) try: policy = serializer.get() except ObjectDoesNotExist: context.abort(grpc.StatusCode.NOT_FOUND, "Policy does not exist") _assert_policy_write_permission(context, asn, client, request.vlan) policy.delete() # Update links and notify clients policy_resolver.update_accepted_peers(policy.vlan, policy.asys) policy_resolver.update_links(policy.vlan, policy.asys) return Empty()
def to_internal_value(self, data): try: return self.get_queryset().get(asn=ASN(data)) except ValueError: raise serializers.ValidationError("Invalid ASN.") except AS.DoesNotExist: raise serializers.ValidationError("AS does not exist.")
def to_python(self, value: Union[None, int, str, ASN]) -> Optional[ASN]: if isinstance(value, ASN): return value if value is None: return value try: return ASN(value) except ValueError as e: raise ValidationError( "Invalid AS number: '%(value)s'. Error: %(msg)s", code='invalid_asn', params={ 'value': value, 'msg': str(e) })
def setUpTestData(cls): # VLANs cls.vlan = [ VLAN.objects.create(name="prod", long_name="Production", ip_network=ipaddress.IPv4Network("10.0.0.0/16")), ] # Owners cls.owner = [ Owner.objects.create(name="owner1", long_name="Owner 1", contact=""), Owner.objects.create(name="owner2", long_name="Owner 2", contact=""), ] # ISDs cls.isd = [ ISD.objects.create(isd_id=1, name="Region 1"), ISD.objects.create(isd_id=2, name="Region 2"), ] # ASes cls.asys = { 'A': AS.objects.create(asn=ASN("ff00:0:0"), isd=cls.isd[0], name="AS 0", owner=cls.owner[0], is_core=True), 'B': AS.objects.create(asn=ASN("ff00:0:1"), isd=cls.isd[1], name="AS 1", owner=cls.owner[0], is_core=True), 'C': AS.objects.create(asn=ASN("ff00:0:2"), isd=cls.isd[0], name="AS 2", owner=cls.owner[0], is_core=True), 'D': AS.objects.create(asn=ASN("ff00:0:3"), isd=cls.isd[1], name="AS 3", owner=cls.owner[0], is_core=False), 'E': AS.objects.create(asn=ASN("ff00:0:4"), isd=cls.isd[1], name="AS 4", owner=cls.owner[0], is_core=False), 'F': AS.objects.create(asn=ASN("ff00:0:5"), isd=cls.isd[0], name="AS 5", owner=cls.owner[1], is_core=False), 'G': AS.objects.create(asn=ASN("ff00:0:6"), isd=cls.isd[1], name="AS 6", owner=cls.owner[1], is_core=False), 'H': AS.objects.create(asn=ASN("ff00:0:7"), isd=cls.isd[1], name="AS 7", owner=cls.owner[0], is_core=False), } # Peering Clients clients = [] for asys in cls.asys.values(): clients.append(PeeringClient.objects.create(asys=asys, name="default")) # Interfaces for vlan in cls.vlan: for daemon, ip in zip(clients, cls.vlan[0].ip_network.hosts()): Interface.objects.create(peering_client=daemon, vlan=vlan, public_ip=ip, first_port=50000, last_port=51000)
def test_asn_parsing(self): for string, integer in self.TEST_CASES: with self.subTest(string=string, integer=integer): self.assertEqual(int(ASN(string)), integer) with self.assertRaisesRegex(ValueError, r"Out of range"): ASN(-1) with self.assertRaisesRegex(ValueError, r"Out of range"): ASN(2**48) with self.assertRaisesRegex(ValueError, r"^Invalid decimal ASN"): ASN(str(2**32)) with self.assertRaisesRegex(ValueError, r"^Invalid hexadecimal ASN"): ASN("ffff:fffff:ffff") with self.assertRaisesRegex(ValueError, r"Wrong number of colon-separated groups"): ASN("ff:ff:ff:ff:ff:ff")
def SetPortRange(self, request, context): """Set the UDP port range used for SCION underlay connections.""" asn_str, _ = get_client_from_metadata(context.invocation_metadata()) asn = ASN(asn_str) # Validate arguments and retrieve the interface try: vlan = VLAN.objects.get(name=request.interface_vlan) except VLAN.DoesNotExist: context.abort(grpc.StatusCode.NOT_FOUND, "VLAN does not exist") try: ip = ipaddress.ip_address(request.interface_ip) except ValueError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid IP address") try: interface = Interface.objects.get(vlan=vlan, public_ip=ip) except (Interface.DoesNotExist, Interface.MultipleObjectsReturned): context.abort(grpc.StatusCode.NOT_FOUND, "Interface not found") recreate_links = not (request.first_port <= interface.first_port and request.last_port >= interface.last_port) try: interface.first_port = request.first_port interface.last_port = request.last_port interface.save() except ValidationError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid port range") if recreate_links: # Recreate the interface's links with the new ports for link in interface.query_links().all(): link.delete() policy_resolver.update_links(vlan, AS.objects.get(asn=asn)) return peering_pb2.google_dot_protobuf_dot_empty__pb2.Empty()
def GetOwner(self, request, context): """Retrieve information on an AS owner by owner name or by an AS.""" # Build query query = Owner.objects if request.name: query = query.filter(name=request.name) if request.asn: try: query = query.filter(ases__asn=ASN(request.asn)) except ValueError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid ASN") # Query database owner = None try: owner = query.get() except Owner.DoesNotExist: context.abort(grpc.StatusCode.NOT_FOUND, "No matching owner found") # Build response buffer = info_pb2.Owner() _fill_owner_protobuf(owner, buffer) return buffer
def intercept_service(self, continuation, handler_call_details): asn, client = get_client_from_metadata( handler_call_details.invocation_metadata) if not asn or not client: return self._abortion # Retrieve the expected token try: query = PeeringClient.objects.values_list('secret_token', flat=True) token = query.get(asys__asn=ASN(asn), name=client) if len(token) < 2 * API_TOKEN_BYTES: return self._abortion except ValueError: return self._abortion except PeeringClient.DoesNotExist: return self._abortion expected_metadata = (TOKEN_HEADER_KEY, token) if expected_metadata in handler_call_details.invocation_metadata: return continuation(handler_call_details) else: return self._abortion
def get_queryset(self): # Filter for VLAN self.vlan = get_object_or_404(VLAN, name=self.kwargs['name']) ases = self.vlan.members.values_list('asys', flat=True).all() queryset = AS.objects.filter(id__in=ases) # Filter according to query string self.query = self.request.GET.get("query") if self.query: if self.query.startswith("AS"): try: queryset = queryset.filter(asn=ASN(self.query[2:])) except ValueError: queryset = AS.objects.none() else: queryset = queryset.filter( Q(name__icontains=self.query) | Q(owner__long_name__icontains=self.query)) # Ordering self.sort_order = self.request.GET.get(ORDER_KEY, ORDER_ASC) validate_sort_order(self.sort_order) self.order_by = self.request.GET.get(SORT_BY_KEY) if self.order_by in ["name", "asn", "owner"]: queryset = queryset.order_by(self.order_by) elif self.order_by == "isd": queryset = queryset.order_by("isd__isd_id") elif self.order_by == "links": queryset = queryset.annotate(link_count=Count('links')) elif self.order_by: raise Http404() if self.sort_order == ORDER_DESC: queryset = queryset.reverse() return queryset
def StreamChannel(self, request_iterator, context): """Server side of the persistent bidirectional gRPC stream peering clients maintain with the coordinator. Since bidirectional gRPC streams in Python use blocking generators for sending and receiving, an additional thread just for reading request from the stream is created for every connection. The request listener threads forwards received requests to the main thread handling the connection via the associated ClientConnection object. """ asn, client_name = get_client_from_metadata( context.invocation_metadata()) asn = ASN(asn) # Register the connection try: conn = ClientRegistry.createConnection(asn, client_name) except KeyError as e: context.abort(grpc.StatusCode.NOT_FOUND, str(e)) except ClientConnections.AlreadyConnected as e: context.abort(grpc.StatusCode.ALREADY_EXISTS, str(e)) except: context.abort(grpc.StatusCode.INTERNAL, "Internal error") # Enqueue link create messages for all existing links. try: client = PeeringClient.objects.get(asys__asn=asn, name=client_name) for interface in Interface.objects.filter( peering_client=client).all(): for link in interface.query_links().all(): if link.interface_a == interface: update = create_link_update( peering_pb2.LinkUpdate.Type.CREATE, link_type=link.link_type, local_interface=link.interface_a, local_port=link.port_a, remote_interface=link.interface_b, remote_port=link.port_b) else: update = create_link_update( peering_pb2.LinkUpdate.Type.CREATE, link_type=link.link_type, local_interface=link.interface_b, local_port=link.port_b, remote_interface=link.interface_a, remote_port=link.port_a) conn.send_link_update(update) except: ClientRegistry.destroyConnection(conn) raise # Launch a new thread to listen for requests from the client. def stream_listener(): try: for request in request_iterator: conn.stream_request_received(request) except grpc.RpcError: pass finally: conn.request_stream_closed() listener = threading.Thread( target=stream_listener, name="gRPC stream listener for {}-{}".format(asn, client_name)) listener.start() # Run the event loop to process requests and generate responses. try: for response in conn.run(): yield response finally: ClientRegistry.destroyConnection(conn) listener.join()
def from_db_value(self, value: Optional[int], expression, connection) -> Optional[ASN]: if value is None: return value return ASN(value)
def setUpTestData(cls): # Owners cls.owner = [ Owner.objects.create(name="owner1", long_name="Owner 1", contact="Contact Info A"), ] # ISDs cls.isd = [ ISD.objects.create(isd_id=1, name="Region 1"), ISD.objects.create(isd_id=2, name="Region 2"), ] # ASes cls.asys = [ AS.objects.create(asn=ASN("ff00:0:0"), isd=cls.isd[0], name="AS 0", owner=cls.owner[0], is_core=True), AS.objects.create(asn=ASN("ff00:0:1"), isd=cls.isd[0], name="AS 1", owner=cls.owner[0], is_core=False), AS.objects.create(asn=ASN("ff00:0:2"), isd=cls.isd[0], name="AS 2", owner=cls.owner[0], is_core=False), AS.objects.create(asn=ASN("ff00:0:3"), isd=cls.isd[1], name="AS 3", owner=cls.owner[0], is_core=True), AS.objects.create(asn=ASN("ff00:0:4"), isd=cls.isd[1], name="AS 4", owner=cls.owner[0], is_core=False), ] # VLANs cls.vlan = [ VLAN.objects.create( name="prod", long_name="Production", ip_network=ipaddress.IPv4Network("10.0.0.0/16")), VLAN.objects.create( name="test", long_name="Testing", ip_network=ipaddress.IPv4Network("10.1.0.0/16")), ] # Peering Clients clients = [] for asys in cls.asys: clients.append( PeeringClient.objects.create(asys=asys, name="default")) # Interfaces for vlan in cls.vlan: for client, ip in zip(clients, vlan.ip_network.hosts()): Interface.objects.create(peering_client=client, vlan=vlan, public_ip=ip) # AS 0 has an additional client client = PeeringClient.objects.create(asys=cls.asys[0], name="backup") Interface.objects.create(peering_client=client, vlan=cls.vlan[0], public_ip="10.0.0.200")
def _set_up_test_data(cls): # Owners cls.owner = [ Owner.objects.create(name="owner1", long_name="Owner Name 1", contact="Contact Info A"), Owner.objects.create(name="owner2", long_name="Owner 2", contact="Contact Info B"), Owner.objects.create(name="owner3", long_name="Owner 3", contact="Contact Info C"), ] # ISDs cls.isd = [ ISD.objects.create(isd_id=1, name="Region 1"), ISD.objects.create(isd_id=2, name="Region 2"), ] # ASes cls.asys = [ AS.objects.create(asn=ASN("ff00:0:0"), isd=cls.isd[0], name="AS 0", owner=cls.owner[0], is_core=True), AS.objects.create(asn=ASN("ff00:0:1"), isd=cls.isd[0], name="AS 1", owner=cls.owner[0], is_core=False), AS.objects.create(asn=ASN("ff00:0:2"), isd=cls.isd[0], name="AS 2", owner=cls.owner[0], is_core=True), AS.objects.create(asn=ASN("ff00:0:3"), isd=cls.isd[0], name="AS 3", owner=cls.owner[1], is_core=False), AS.objects.create(asn=ASN("ff00:0:4"), isd=cls.isd[1], name="AS 4", owner=cls.owner[1], is_core=False), AS.objects.create(asn=ASN("ff00:0:5"), isd=cls.isd[1], name="AS 5", owner=cls.owner[1], is_core=False), ] # VLANs cls.vlan = [ VLAN.objects.create(name="prod", long_name="Production", ip_network=ipaddress.IPv4Network("10.0.0.0/16")), VLAN.objects.create(name="test", long_name="Testing", ip_network=ipaddress.IPv4Network("10.1.0.0/16")), ] # Peering Clients clients = [] for asys in cls.asys: clients.append(PeeringClient.objects.create(asys=asys, name="default")) # Interfaces for vlan in cls.vlan: for client, ip in zip(clients, vlan.ip_network.hosts()): Interface.objects.create(peering_client=client, vlan=vlan, public_ip=ip, first_port=50000, last_port=51000)
def testArbitration(self): stub = peering_pb2_grpc.PeeringStub(self.channel) default_call_cred = [(ASN_HEADER_KEY, "ff00:0:0"), (CLIENT_NAME_HEADER_KEY, "default")] backup_call_cred = [(ASN_HEADER_KEY, "ff00:0:0"), (CLIENT_NAME_HEADER_KEY, "backup")] default_request_queue = queue.Queue() backup_request_queue = queue.Queue() self.assertIsNone(ClientRegistry.get_clients(ASN("ff00:0:0"))) # Connect the backup client backup_channel = stub.StreamChannel(iter(backup_request_queue.get, None), metadata=backup_call_cred) request = peering_pb2.StreamMessageRequest() request.arbitration.election_id = 0 backup_request_queue.put(request) response = next(backup_channel) self.assertEqual(response.arbitration.vlan, "prod") self.assertEqual(response.arbitration.status, peering_pb2.ArbitrationUpdate.Status.PRIMARY) clients = ClientRegistry.get_clients(ASN("ff00:0:0")) self.assertEqual(len(clients.connections), 1) self.assertTrue(clients.is_primary_client("backup", "prod")) self.assertFalse(clients.is_primary_client("default", "test")) # Connect the default client default_channel = stub.StreamChannel(iter(default_request_queue.get, None), metadata=default_call_cred) request = peering_pb2.StreamMessageRequest() request.arbitration.election_id = 100 default_request_queue.put(request) response = next(default_channel) self.assertEqual(response.arbitration.vlan, "prod") self.assertEqual(response.arbitration.status, peering_pb2.ArbitrationUpdate.Status.PRIMARY) response = next(default_channel) self.assertEqual(response.arbitration.vlan, "test") self.assertEqual(response.arbitration.status, peering_pb2.ArbitrationUpdate.Status.PRIMARY) response = next(backup_channel) self.assertEqual(response.arbitration.vlan, "prod") self.assertEqual(response.arbitration.status, peering_pb2.ArbitrationUpdate.Status.NOT_PRIMARY) self.assertEqual(len(clients.connections), 2) self.assertFalse(clients.is_primary_client("backup", "prod")) self.assertTrue(clients.is_primary_client("default", "prod")) self.assertTrue(clients.is_primary_client("default", "test")) # Disconnect the default client default_request_queue.put(None) for response in default_channel: self.assertTrue(False, "Unexpected additional messages on stream") response = next(backup_channel) self.assertEqual(response.arbitration.status, peering_pb2.ArbitrationUpdate.Status.PRIMARY) self.assertEqual(len(clients.connections), 1) self.assertTrue(clients.is_primary_client("backup", "prod")) self.assertFalse(clients.is_primary_client("default", "test")) # Disconnect the backup client backup_request_queue.put(None) for response in backup_channel: self.assertTrue(False, "Unexpected additional messages on stream") self.assertIsNone(ClientRegistry.get_clients(ASN("ff00:0:0")))
def test_asn_deparsing(self): for string, integer in self.TEST_CASES: with self.subTest(string=string, integer=integer): self.assertEqual(str(ASN(integer)), string)
def to_python(self, value: str) -> ASN: # ValueError is thrown to indicate no match. return ASN(value.replace("-", ":"))