diff --git a/config/vpnc/config/config-hub.yaml.example b/config/vpnc/config/config-hub.yaml.example index c0786b1..fb05f4d 100644 --- a/config/vpnc/config/config-hub.yaml.example +++ b/config/vpnc/config/config-hub.yaml.example @@ -57,9 +57,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -102,9 +103,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -144,6 +146,10 @@ bgp: asn: 4255555555 router_id: 5.5.5.5 bfd: false # bool: defaults to false + # Prefixes to accept from neighbors. Preferred over static routes. + prefix_set: + - prefix: fd00::/16 + masklength_range: [16, 128] # exact | [int, int] neighbors: - neighbor_asn: 4233333333 neighbor_address: fd00:1:2::1 diff --git a/setup/migrate.py b/setup/migrate.py index ccbcb53..dcb4fcb 100644 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -108,8 +108,8 @@ def _get_version(path: pathlib.Path) -> Version | None: } fa.seek(0) - yaml.dump(v12_svc, fa, explicit_start=True, explicit_end=True) - yaml.dump(v12_svc, fc, explicit_start=True, explicit_end=True) + yaml.dump(v12_svc, fa, explicit_start=True, explicit_end=True, sort_keys=False) + yaml.dump(v12_svc, fc, explicit_start=True, explicit_end=True, sort_keys=False) fa.truncate() fc.truncate() @@ -153,7 +153,19 @@ def _get_version(path: pathlib.Path) -> Version | None: } fa.seek(0) - yaml.dump(v12_rem, fa, explicit_start=True, explicit_end=True) - yaml.dump(v12_rem, fc, explicit_start=True, explicit_end=True) + yaml.dump( + v12_rem, + fa, + explicit_start=True, + explicit_end=True, + sort_keys=False, + ) + yaml.dump( + v12_rem, + fc, + explicit_start=True, + explicit_end=True, + sort_keys=False, + ) fa.truncate() fc.truncate() diff --git a/test/01_base_config/hub00/DEFAULT.yaml b/test/01_base_config/hub00/DEFAULT.yaml index 93c658c..67f996b 100644 --- a/test/01_base_config/hub00/DEFAULT.yaml +++ b/test/01_base_config/hub00/DEFAULT.yaml @@ -57,9 +57,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -102,9 +103,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -144,6 +146,10 @@ bgp: asn: 4255555555 router_id: 5.5.5.5 bfd: true + # Prefixes to accept from neighbors. Preferred over static routes. + prefix_set: + - prefix: fd00::/16 + masklength_range: [16, 128] # exact | [int, int] neighbors: - neighbor_asn: 4233333333 neighbor_address: fd00:1:2::1 diff --git a/test/01_base_config/hub01/DEFAULT.yaml b/test/01_base_config/hub01/DEFAULT.yaml index 29ba965..d24fe3e 100644 --- a/test/01_base_config/hub01/DEFAULT.yaml +++ b/test/01_base_config/hub01/DEFAULT.yaml @@ -57,9 +57,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -102,9 +103,10 @@ network_instances: # List of IPv4 and IPv6 networks natively reachable via this connection. # IPv4 prefixes are ignored for core and downlink in hub mode. routes: - ipv6: + ipv6: [] # IP prefixes for provider client subnets initiating administration traffic to tenants. - - to: fd00::/16 + # Using BGP is preferred over static routes. + # - to: fd00::/16 # via: fd00:1:2::1 # Optional, ignored for tunnel interfaces # IPv4 prefixes are ignored for core and downlink in hub mode. ipv4: [] @@ -144,6 +146,10 @@ bgp: asn: 4266666666 router_id: 6.6.6.6 bfd: false + # Prefixes to accept from neighbors. Preferred over static routes. + prefix_set: + - prefix: fd00::/16 + masklength_range: [16, 128] # exact | [int, int] neighbors: - neighbor_asn: 4233333333 neighbor_address: fd00:1:2::3 diff --git a/vpnc/src/vpnc/ctl/bgp.py b/vpnc/src/vpnc/ctl/bgp.py index f2df430..b930c82 100644 --- a/vpnc/src/vpnc/ctl/bgp.py +++ b/vpnc/src/vpnc/ctl/bgp.py @@ -39,7 +39,9 @@ def show( bgp = service.bgp output = bgp.model_dump(mode="json") - print(yaml.safe_dump(output, explicit_start=True, explicit_end=True)) + print( + yaml.safe_dump(output, explicit_start=True, explicit_end=True, sort_keys=False), + ) @app.command() @@ -125,7 +127,7 @@ def summary( # output = yaml.safe_dump( # service.model_dump(mode="json"), # explicit_start=True, -# explicit_end=True, +# explicit_end=True, sort_keys=False, # ) # with path.open("w+", encoding="utf-8") as f: # f.write(output) diff --git a/vpnc/src/vpnc/ctl/tenant_ni_con.py b/vpnc/src/vpnc/ctl/tenant_ni_con.py index 2779215..41ebd68 100644 --- a/vpnc/src/vpnc/ctl/tenant_ni_con.py +++ b/vpnc/src/vpnc/ctl/tenant_ni_con.py @@ -111,6 +111,7 @@ def show( connection.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ), ) diff --git a/vpnc/src/vpnc/ctl/tenants.py b/vpnc/src/vpnc/ctl/tenants.py index 286b450..33f2bbb 100644 --- a/vpnc/src/vpnc/ctl/tenants.py +++ b/vpnc/src/vpnc/ctl/tenants.py @@ -99,10 +99,24 @@ def show( output = tenant.model_dump(mode="json") if full: - print(yaml.safe_dump(output, explicit_start=True, explicit_end=True)) + print( + yaml.safe_dump( + output, + explicit_start=True, + explicit_end=True, + sort_keys=False, + ), + ) else: output["network_instance_count"] = len(output.pop("network_instances")) - print(yaml.safe_dump(output, explicit_start=True, explicit_end=True)) + print( + yaml.safe_dump( + output, + explicit_start=True, + explicit_end=True, + sort_keys=False, + ), + ) @app.command() @@ -144,6 +158,7 @@ def edit(ctx: typer.Context) -> None: tenant.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ), ) tf.flush() @@ -187,6 +202,7 @@ def edit(ctx: typer.Context) -> None: edited_config.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ) with path.joinpath(f"{tenant_id}.yaml").open(mode="w", encoding="utf-8") as f: f.write(output) @@ -228,6 +244,7 @@ def add( tenant.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ) with path.open("w+", encoding="utf-8") as f: f.write(output) @@ -264,7 +281,7 @@ def add( # updated_remote.metadata.update(all_metadata) # output = yaml.safe_dump( -# updated_remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# updated_remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # with open(path, "w+", encoding="utf-8") as f: # f.write(output) @@ -311,7 +328,7 @@ def add( # updated_remote = vpnc.models.tenant.Tenant(**remote_dict) # output = yaml.safe_dump( -# updated_remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# updated_remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # with open(path, "w+", encoding="utf-8") as f: # f.write(output) @@ -347,6 +364,7 @@ def delete( tenant.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ) print(output) if dry_run: @@ -378,7 +396,9 @@ def commit( path_active_tenant = path_active.joinpath(f"{tenant_id}.yaml") if not path_candidate_tenant.exists(): tenant_config_candidate = vpnc.models.tenant.Tenant( - id=tenant_id, name="", version="0.1.0" + id=tenant_id, + name="", + version="0.1.0", ) else: tenant_config_candidate = helpers.get_tenant_config( @@ -389,7 +409,9 @@ def commit( if not path_active_tenant.exists(): tenant_config_active = vpnc.models.tenant.Tenant( - id=tenant_id, name="", version="0.1.0" + id=tenant_id, + name="", + version="0.1.0", ) else: tenant_config_active = helpers.get_tenant_config(ctx, tenant_id, path_active) @@ -421,6 +443,7 @@ def commit( tenant_config_active.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ), ) print("Revert succeeded.") @@ -449,6 +472,7 @@ def commit( tenant_config_candidate.model_dump(mode="json"), explicit_start=True, explicit_end=True, + sort_keys=False, ), ) print("Commit succeeded.") diff --git a/vpnc/src/vpnc/ctl/tenants_ni.py b/vpnc/src/vpnc/ctl/tenants_ni.py index 6a5d466..3b285a3 100644 --- a/vpnc/src/vpnc/ctl/tenants_ni.py +++ b/vpnc/src/vpnc/ctl/tenants_ni.py @@ -107,7 +107,9 @@ def show( if not network_instance: return output = {instance_id: network_instance.model_dump(mode="json")} - print(yaml.safe_dump(output, explicit_start=True, explicit_end=True)) + print( + yaml.safe_dump(output, explicit_start=True, explicit_end=True, sort_keys=False), + ) @app.command() @@ -206,7 +208,7 @@ class IkeVersion(str, Enum): # remote.connections[tunnel_id] = tunnel # output = yaml.safe_dump( -# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # with open(path, "w+", encoding="utf-8") as f: # f.write(output) @@ -284,7 +286,7 @@ class IkeVersion(str, Enum): # remote.connections[tunnel_id] = updated_tunnel # output = yaml.safe_dump( -# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # with open(path, "w+", encoding="utf-8") as f: # f.write(output) @@ -367,7 +369,7 @@ class IkeVersion(str, Enum): # remote.connections[tunnel_id] = updated_tunnel # output = yaml.safe_dump( -# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # with open(path, "w+", encoding="utf-8") as f: # f.write(output) @@ -403,7 +405,7 @@ class IkeVersion(str, Enum): # remote.connections.pop(tunnel_id) # output = yaml.safe_dump( -# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True +# remote.model_dump(mode="json"), explicit_start=True, explicit_end=True, sort_keys=False, # ) # print(yaml.safe_dump({tunnel_id: tunnel.model_dump(mode="json")})) # if dry_run: diff --git a/vpnc/src/vpnc/models/tenant.py b/vpnc/src/vpnc/models/tenant.py index 914a029..b13362d 100644 --- a/vpnc/src/vpnc/models/tenant.py +++ b/vpnc/src/vpnc/models/tenant.py @@ -31,10 +31,10 @@ # Needed for pydantim ports and type checking from vpnc.models.network_instance import ( - NetworkInstanceCore, # noqa: TCH001 - NetworkInstanceDownlink, # noqa: TCH001 - NetworkInstanceEndpoint, # noqa: TCH001 - NetworkInstanceExternal, # noqa: TCH001 + NetworkInstanceCore, + NetworkInstanceDownlink, + NetworkInstanceEndpoint, + NetworkInstanceExternal, ) if TYPE_CHECKING: @@ -72,7 +72,7 @@ class BGPGlobal(BaseModel): """Define BGP global data structure.""" asn: int = 4200000000 - router_id: IPv4Address + router_id: IPv4Address = IPv4Address("0.0.0.0") # noqa: S104 bfd: bool = False @@ -86,11 +86,45 @@ class BGPNeighbor(BaseModel): priority: int = Field(0, ge=0, le=9) +class Prefix(BaseModel): + """Prefixes accepted by BGP. + + The masklength by default is 'exact', meaning it only accepts routes matching + exactly. + + A tuple of two integers can be specified to give a range. This range is inclusive. + """ + + prefix: IPv6Network | IPv6Address + masklength_range: Literal["exact"] | tuple[int, int] = "exact" + + @field_validator("masklength_range") + @classmethod + def _validate_is_default( + cls, + v: Literal["exact"] | tuple[int, int], + ) -> Literal["exact"] | tuple[int, int]: + if v == "exact": + return v + + lower = v[0] + upper = v[1] + + if lower <= upper and 0 <= lower <= 128 and 0 <= upper <= 128: + return v + + raise PydanticCustomError( + "prefix_set_error", + "The 'masklength_range' property should be 'exact' or a tuple of two integers from 0 to 128 inclusive.", + ) + + class BGP(BaseModel): """Define a BGP data structure.""" - neighbors: list[BGPNeighbor] globals: BGPGlobal + prefix_set: list[Prefix] = Field(default_factory=list) + neighbors: list[BGPNeighbor] = Field(default_factory=list) class ServiceEndpoint(Tenant): @@ -140,6 +174,9 @@ class ServiceHub(Tenant): NetworkInstanceDownlink | NetworkInstanceCore | NetworkInstanceExternal, ] = Field(default_factory=dict) + ## BGP config + bgp: BGP + # VPN CONFIG # IKE local identifier for VPNs local_id: str = r"%any" @@ -159,9 +196,6 @@ class ServiceHub(Tenant): # downlink per tunnel. prefix_downlink_nptv6: IPv6Network = IPv6Network("660::/12") - ## BGP config - bgp: BGP - @field_validator("mode", mode="before") @classmethod def _coerce_type(cls, v: str) -> ServiceMode: diff --git a/vpnc/src/vpnc/services/configuration.py b/vpnc/src/vpnc/services/configuration.py index 1df5013..4c50481 100644 --- a/vpnc/src/vpnc/services/configuration.py +++ b/vpnc/src/vpnc/services/configuration.py @@ -151,14 +151,18 @@ def manage_tenant(path: pathlib.Path) -> None: output = tenant.model_dump(mode="json") try: fha.write( - yaml.safe_dump(output, explicit_start=True, explicit_end=True), + yaml.safe_dump( + output, explicit_start=True, explicit_end=True, sort_keys=False + ), ) except yaml.YAMLError: logger.exception("Invalid YAML found in %s. Skipping.", path) return try: fhb.write( - yaml.safe_dump(output, explicit_start=True, explicit_end=True), + yaml.safe_dump( + output, explicit_start=True, explicit_end=True, sort_keys=False + ), ) except yaml.YAMLError: logger.exception("Invalid YAML found in %s. Skipping.", path) diff --git a/vpnc/src/vpnc/services/frr/__init__.py b/vpnc/src/vpnc/services/frr/__init__.py index 2f84841..06b8ca0 100644 --- a/vpnc/src/vpnc/services/frr/__init__.py +++ b/vpnc/src/vpnc/services/frr/__init__.py @@ -5,7 +5,7 @@ import pathlib import subprocess import time -from typing import TYPE_CHECKING, Any +from typing import Any from jinja2 import Environment, FileSystemLoader from watchdog.events import FileSystemEvent, PatternMatchingEventHandler @@ -15,9 +15,6 @@ from vpnc import config from vpnc.models import enums, tenant -if TYPE_CHECKING: - from ipaddress import IPv4Network, IPv6Network - logger = logging.getLogger("vpnc") BASE_DIR = pathlib.Path(__file__).parent @@ -100,9 +97,7 @@ def generate_config() -> None: # FRR/BGP CONFIG frr_template = TEMPLATES_ENV.get_template("frr.conf.j2") # Subnets expected on the CORE side - prefix_core: list[IPv4Network | IPv6Network] = [] - for connection in net_instance.connections.values(): - prefix_core = [route.to for route in connection.routes.ipv6] + prefix_core = default_tenant.bgp.prefix_set frr_cfg: dict[str, Any] = { "core_ni": config.CORE_NI, diff --git a/vpnc/src/vpnc/services/frr/templates/frr.conf.j2 b/vpnc/src/vpnc/services/frr/templates/frr.conf.j2 index 84656dd..535ee62 100644 --- a/vpnc/src/vpnc/services/frr/templates/frr.conf.j2 +++ b/vpnc/src/vpnc/services/frr/templates/frr.conf.j2 @@ -35,7 +35,11 @@ router bgp {{ as }} vrf {{ core_ni }} exit ! {%- for prefix in prefix_core %} -ipv6 prefix-list UPLINK-PL-IN seq {{ 10 * loop.index }} permit {{ prefix }} le 64 + {%- if prefix.masklength_range == "exact" %} +ipv6 prefix-list UPLINK-PL-IN seq {{ 10 * loop.index }} permit {{ prefix.prefix }} + {%- else %} +ipv6 prefix-list UPLINK-PL-IN seq {{ 10 * loop.index }} permit {{ prefix.prefix }} ge {{ prefix.masklength_range[0] }} le {{ prefix.masklength_range[1] }} + {%- endif %} {%- endfor %} ipv6 prefix-list UPLINK-PL-OUT seq 10 permit 2000::/3 ge 32 ipv6 prefix-list UPLINK-PL-OUT seq 20 permit {{ prefix_downlink_nat64 }} ge 96 le 96 diff --git a/vpnc/src/vpnc/services/routes/__init__.py b/vpnc/src/vpnc/services/routes/__init__.py index 7c741f7..32c403f 100644 --- a/vpnc/src/vpnc/services/routes/__init__.py +++ b/vpnc/src/vpnc/services/routes/__init__.py @@ -140,14 +140,6 @@ def set_routes_up( active_connection: vpnc.models.connections.Connection | None, ) -> None: """Activates routes when connections go down.""" - default_tenant = vpnc.models.tenant.get_default_tenant() - - if ( - net_inst.type == enums.NetworkInstanceType.CORE - and default_tenant.mode == enums.ServiceMode.HUB - ): - return - interface_name_downlink = connection.intf_name(net_inst) interface_name_core = f"{net_inst.id}_C"