diff --git a/scionlab/forms/user_as_form.py b/scionlab/forms/user_as_form.py index b70198c8..3f82aa8c 100644 --- a/scionlab/forms/user_as_form.py +++ b/scionlab/forms/user_as_form.py @@ -53,6 +53,15 @@ def _crispy_helper(instance): ), css_class="card", ), + Div( + Div( + Row( + 'enable_fabrid', + ), + css_class="card-header", + ), + css_class="card", + ), ) # We need this to render the UserASForm along with the AttachmentConfForm @@ -109,6 +118,11 @@ class UserASForm(forms.Form): label="Provide VPN", help_text="Allow Users to connect to your AP via VPN" ) + enable_fabrid = forms.BooleanField( + required=False, + label="FABRID", + help_text="Enable FABRID support for this AS.", + ) def _get_attachment_conf_form_set(self, data, instance: UserAS): """ @@ -145,7 +159,8 @@ def __init__(self, data=None, *args, **kwargs): 'installation_type': self.instance.installation_type, 'public_ip': host.public_ip, 'become_user_ap': is_ap, - 'provide_vpn': has_vpn + 'provide_vpn': has_vpn, + 'enable_fabrid': self.instance.fabrid_enabled, }) self.helper = _crispy_helper(self.instance) super().__init__(data, *args, initial=initial, **kwargs) @@ -195,6 +210,7 @@ def save(self, commit=True): public_ip=self.cleaned_data['public_ip'], wants_user_ap=self.cleaned_data['become_user_ap'], wants_vpn=self.cleaned_data['provide_vpn'], + fabrid_enabled=self.cleaned_data['enable_fabrid'], ) self.attachment_conf_form_set.save(user_as) return user_as @@ -205,6 +221,7 @@ def save(self, commit=True): public_ip=self.cleaned_data['public_ip'], wants_user_ap=self.cleaned_data['become_user_ap'], wants_vpn=self.cleaned_data['provide_vpn'], + fabrid_enabled=self.cleaned_data['enable_fabrid'], ) self.attachment_conf_form_set.save(self.instance) return self.instance diff --git a/scionlab/migrations/0010_as_fabrid_enabled.py b/scionlab/migrations/0010_as_fabrid_enabled.py new file mode 100644 index 00000000..8a1d7bf1 --- /dev/null +++ b/scionlab/migrations/0010_as_fabrid_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2024-09-25 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scionlab', '0009_allhosts_dirty'), + ] + + operations = [ + migrations.AddField( + model_name='as', + name='fabrid_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/scionlab/models/core.py b/scionlab/models/core.py index 88a4c7f3..3e247301 100644 --- a/scionlab/models/core.py +++ b/scionlab/models/core.py @@ -122,13 +122,14 @@ def validate_crypto(self): class ASManager(models.Manager): - def create(self, isd, as_id, is_core=False, label=None, mtu=None, owner=None, - init_certificates=True): + def create(self, isd, as_id, is_core=False, fabrid_enabled=False, label=None, mtu=None, + owner=None, init_certificates=True): """ Create the AS and initialise the required keys :param ISD isd: :param str as_id: valid AS-identifier string :param bool is_core: should this AS be created as a core AS? + :param bool fabrid_enabled: should this AS enable FABRID? :param str label: optional :param User owner: optional :returns: AS @@ -139,6 +140,7 @@ def create(self, isd, as_id, is_core=False, label=None, mtu=None, owner=None, as_id=as_id, as_id_int=as_id_int, is_core=is_core, + fabrid_enabled=fabrid_enabled, label=label, mtu=mtu or DEFAULT_LINK_MTU, owner=owner, @@ -154,8 +156,8 @@ def create(self, isd, as_id, is_core=False, label=None, mtu=None, owner=None, return as_ def create_with_default_services(self, isd, as_id, public_ip, - is_core=False, label=None, mtu=None, owner=None, - bind_ip=None, internal_ip=None, + is_core=False, fabrid_enabled=False, label=None, mtu=None, + owner=None, bind_ip=None, internal_ip=None, init_certificates=True): """ Create the AS, initialise the required keys and create a default Host object @@ -164,6 +166,7 @@ def create_with_default_services(self, isd, as_id, public_ip, :param ISD isd: :param str as_id: valid AS-identifier string :param bool is_core: should this AS be created as a core AS? + :param bool fabrid_enabled: should this AS enable FABRID? :param str label: optional :param User owner: optional :param str internal_ip: AS-internal IP for the Host, defaults to 127.0.0.1 @@ -175,6 +178,7 @@ def create_with_default_services(self, isd, as_id, public_ip, isd=isd, as_id=as_id, is_core=is_core, + fabrid_enabled=fabrid_enabled, label=label, mtu=mtu, owner=owner, @@ -229,6 +233,8 @@ class AS(TimestampedModel): is_core = models.BooleanField(default=False) + fabrid_enabled = models.BooleanField(default=False) + objects = ASManager() class Meta: diff --git a/scionlab/models/user_as.py b/scionlab/models/user_as.py index 0a0a6f4c..a2800536 100644 --- a/scionlab/models/user_as.py +++ b/scionlab/models/user_as.py @@ -54,6 +54,7 @@ def create(self, public_ip: str = "", wants_user_ap: bool = False, wants_vpn: bool = False, + fabrid_enabled: bool = False, as_id: str = None, label: str = None) -> 'UserAS': """Create a UserAS @@ -64,6 +65,7 @@ def create(self, :param str public_ip: optional public IP address for the host of the AS :param bool wants_user_ap: optional boolean if the User AS should be AP :param str wants_vpn: optional boolean if the User AP should provide a VPN + :param bool fabrid_enabled: optional boolean if the User AS should enable FABRID :param str as_id: optional as_id, if None is given, the next free ID is chosen :param str label: optional label @@ -85,6 +87,7 @@ def create(self, as_id=as_id, as_id_int=as_id_int, installation_type=installation_type, + fabrid_enabled=fabrid_enabled, master_as_key=AS._make_master_as_key() ) @@ -149,7 +152,7 @@ def get_absolute_url(self): return urls.reverse('user_as_detail', kwargs={'pk': self.pk}) def update(self, label: str, installation_type: str, public_ip: str = "", - wants_user_ap: bool = False, wants_vpn: bool = False): + wants_user_ap: bool = False, wants_vpn: bool = False, fabrid_enabled: bool = False): """ Updates the `UserAS` fields and immediately saves """ @@ -160,6 +163,7 @@ def update(self, label: str, installation_type: str, public_ip: str = "", elif self.installation_type == UserAS.VM: self._unset_bind_ips_for_vagrant() self.installation_type = installation_type + self.fabrid_enabled = fabrid_enabled host = self.host host.update(public_ip=public_ip) if self.is_attachment_point(): diff --git a/scionlab/scion/config.py b/scionlab/scion/config.py index 2bbf1897..bf75e708 100644 --- a/scionlab/scion/config.py +++ b/scionlab/scion/config.py @@ -301,6 +301,13 @@ def build_br_conf(self, router): logging_conf = self._build_logging_conf(router.instance_name) metrics_conf = self._build_metrics_conf(router.metrics_port) conf = _chain_dicts(general_conf, logging_conf, metrics_conf) + if self.topo_info.AS.fabrid_enabled: + conf.update({ + 'router': { + 'drkey': ['FABRID'], + 'fabrid': True, + } + }) return conf def build_cs_conf(self, service): @@ -335,6 +342,26 @@ def build_cs_conf(self, service): 'mode': 'in-process', }, }) + if service.AS.fabrid_enabled: + conf.update({ + 'fabrid': { + 'enabled': True, + 'path': os.path.join(self.config_dir, 'fabrid-policies'), + }, + 'drkey': { + 'level1_db': { + 'connection': '%s.drkey-level1.db' % os.path.join(self.var_dir, + service.instance_name), + }, + 'secret_value_db': { + 'connection': '%s.drkey-secret.db' % os.path.join(self.var_dir, + service.instance_name), + }, + 'delegation': { + 'FABRID': [router.host.internal_ip for router in self.topo_info.routers], + }, + }, + }) return conf @@ -356,6 +383,13 @@ def build_sciond_conf(self, host): 'connection': '%s.trust.db' % os.path.join(self.var_dir, instance_name), }, }) + if self.topo_info.AS.fabrid_enabled: + conf.update({ + 'drkey_level2_db': { + 'connection': '%s.drkey-level2.db' % os.path.join(self.var_dir, instance_name), + }, + }) + return conf def build_beacon_policy(self, service): diff --git a/scionlab/tests/test_user_as_models.py b/scionlab/tests/test_user_as_models.py index 37dbc384..09df8ac6 100644 --- a/scionlab/tests/test_user_as_models.py +++ b/scionlab/tests/test_user_as_models.py @@ -100,6 +100,7 @@ def create_and_check_useras(testcase, wants_user_ap=False, public_ip="", wants_vpn=False, + fabrid_enabled=False, installation_type=UserAS.PKG, label='label foo', **kwargs) -> UserAS: @@ -114,6 +115,7 @@ def create_and_check_useras(testcase, installation_type, isd, label=label, + fabrid_enabled=fabrid_enabled, public_ip=public_ip, wants_vpn=wants_vpn, wants_user_ap=wants_user_ap @@ -135,6 +137,7 @@ def create_and_check_useras(testcase, att_confs, owner, vpn_choice, + fabrid_enabled, installation_type, label, wants_user_ap, @@ -150,6 +153,7 @@ def check_useras(testcase, att_confs: List[AttachmentConf], owner, vpn_choice: VPNChoice, + fabrid_enabled, installation_type, label, is_ap, @@ -165,6 +169,7 @@ def check_useras(testcase, testcase.assertEqual(user_as.owner, owner) testcase.assertEqual(user_as.label, label) testcase.assertEqual(user_as.installation_type, installation_type) + testcase.assertEqual(user_as.fabrid_enabled, fabrid_enabled) testcase.assertEqual(user_as.is_attachment_point(), is_ap) if is_ap: ap = user_as.attachment_point_info @@ -251,6 +256,7 @@ def update_useras(testcase, user_as.update( label=kwargs.get('label', user_as.label), installation_type=kwargs.get('installation_type', user_as.installation_type), + fabrid_enabled=kwargs.get('fabrid_enabled', user_as.fabrid_enabled), public_ip=public_ip, wants_user_ap=wants_user_ap, wants_vpn=wants_vpn, @@ -344,6 +350,7 @@ def _get_random_useras_params(seed, vpn_choice, **kwargs): kwargs.setdefault('installation_type', r.choice((UserAS.VM, UserAS.PKG, UserAS.SRC))) randstr = r.getrandbits(1024).to_bytes(1024 // 8, 'little').decode('utf8', 'ignore') kwargs.setdefault('label', randstr) + kwargs.setdefault('fabrid_enabled', bool(r.getrandbits(1))) return kwargs