From 8d0139925e7364ed7e448a28ad73c5df67ba9bae Mon Sep 17 00:00:00 2001 From: Vincent Lafeychine Date: Sun, 31 Mar 2024 18:12:38 +0200 Subject: [PATCH] feat(bird): Add as_path.{contains,len}, net.len --- roles/bird/filter_plugins/bird.py | 73 ++++++++++++++++++++++++++++++- roles/bird/templates/bird.conf.j2 | 36 +++++++-------- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/roles/bird/filter_plugins/bird.py b/roles/bird/filter_plugins/bird.py index 504178c..b2764d9 100644 --- a/roles/bird/filter_plugins/bird.py +++ b/roles/bird/filter_plugins/bird.py @@ -1,6 +1,7 @@ from __future__ import annotations import itertools +import re from dataclasses import dataclass from ipaddress import IPv4Address from typing import Any, Generic, Iterator, Literal, TypeVar @@ -29,6 +30,30 @@ class AutoList(list[T], Generic[T]): return [parse_obj_as(T, value)] +VARIABLES = { + "net.len": "net.len", +} + + +def interpolate(string: str, ctx: Context) -> str: + pattern = r"(? str: + try: + return VARIABLES[var] + except KeyError: + return quoted(getattr(ctx, var)) + + split = re.split(pattern, string) + parts = [ + (lookup(p[2:-1]) if re.match(pattern, p) else quoted(p)) + for p in split + if p + ] + + return ", ".join(parts) + + class Proto(BaseModel): protos: AutoList[str] @@ -49,7 +74,28 @@ class Not(BaseModel): condition: Condition = Field(alias="not") -Condition = Proto | Source | And | Or | Not +class AsPathContains(BaseModel): + contains: AutoList[int] = Field(alias="as_path.contains") + + +class AsPathLength(BaseModel): + length: list[int] = Field( + ge=0, min_items=2, max_items=2, alias="as_path.len" + ) + + +class IPv4orIPv6(BaseModel): + ipv4: list[int] = Field(ge=0, min_items=2, max_items=2) + ipv6: list[int] = Field(ge=0, min_items=2, max_items=2) + + +class NetLength(BaseModel): + length: IPv4orIPv6 = Field(alias="net.len") + + +Condition = ( + Proto | Source | And | Or | Not | AsPathContains | AsPathLength | NetLength +) And.update_forward_refs() Or.update_forward_refs() @@ -61,6 +107,10 @@ Accept = Literal["accept"] Reject = Literal["reject"] +class RejectWithMsg(BaseModel): + reject: str + + class PrefSrc(BaseModel): pref_src: AutoList[IPvAnyAddress] @@ -70,7 +120,7 @@ class Conditional(BaseModel): actions: AutoList[Action] = Field(alias="then") -Action = Accept | Reject | PrefSrc | Conditional +Action = Accept | Reject | RejectWithMsg | PrefSrc | Conditional Conditional.update_forward_refs() @@ -144,12 +194,31 @@ def str_of_condition(condition: Condition, ctx: bool) -> str: sources = [str(s) for s in sources] return f"krt_source ~ [ {', '.join(sources)} ]" + case AsPathContains(contains=contains): + return ( + f"bgp_path ~ [ {', '.join([str(asn) for asn in contains])} ]" + ) + + case AsPathLength(length=[min_len, max_len]): + return f"{min_len} <= bgp_path.len && bgp_path.len <= {max_len}" + + case NetLength( + length=IPv4orIPv6(ipv4=[min_v4, max_v4], ipv6=[min_v6, max_v6]) + ): + if ctx.ipv4: + return f"{min_v4} <= net.len && net.len <= {max_v4}" + else: + return f"{min_v6} <= net.len && net.len <= {max_v6}" + def lines_of_action(action: Action, ctx: Context) -> Iterable[str]: match action: case "accept" | "reject": yield f"{action};" + case RejectWithMsg(reject=reason): + yield f"reject {interpolate(reason, ctx)};" + case Conditional(condition=condition, actions=actions): yield f"if {str_of_condition(condition, ctx)} then {'{'}" yield from indent( diff --git a/roles/bird/templates/bird.conf.j2 b/roles/bird/templates/bird.conf.j2 index e5884d4..59dbe32 100644 --- a/roles/bird/templates/bird.conf.j2 +++ b/roles/bird/templates/bird.conf.j2 @@ -27,8 +27,8 @@ protocol device { {% for name, kernel in bird__kernel.items() %} {% for version in ["ipv4", "ipv6"] %} -{% set ipv4 = version == "ipv4" %} -protocol kernel {{ name | bird_name(ipv4) }} { +{% set is_ipv4 = version == "ipv4" %} +protocol kernel {{ name | bird_name(is_ipv4) }} { {% if kernel.kernel is defined %} kernel table {{ kernel.kernel }}; {% endif %} @@ -40,9 +40,9 @@ protocol kernel {{ name | bird_name(ipv4) }} { {% endif %} {{ version }} { {% if kernel.table is defined %} - table {{ kernel.table | bird_name(ipv4) }}; + table {{ kernel.table | bird_name(is_ipv4) }}; {% endif %} - {{ import_export(kernel, ipv4) | indent(8) }} + {{ import_export(kernel, is_ipv4) | indent(8) }} }; } {% endfor %} @@ -50,25 +50,25 @@ protocol kernel {{ name | bird_name(ipv4) }} { {% for name, pipe in bird__pipes.items() %} {% for version in ["ipv4", "ipv6"] %} -{% set ipv4 = version == "ipv4" %} -protocol pipe {{ name | bird_name(ipv4) }} { - table {{ pipe.table | bird_name(ipv4) }}; - peer table {{ pipe.peer_table | default("master") | bird_name(ipv4) }}; - {{ import_export(kernel, ipv4) | indent(4) }} +{% set is_ipv4 = version == "ipv4" %} +protocol pipe {{ name | bird_name(is_ipv4) }} { + table {{ pipe.table | bird_name(is_ipv4) }}; + peer table {{ pipe.peer_table | default("master") | bird_name(is_ipv4) }}; + {{ import_export(kernel, is_ipv4) | indent(4) }} } {% endfor %} {% endfor %} {% if bird__ospf is defined %} {% for version in ["ipv4", "ipv6"] %} -{% set ipv4 = version == "ipv4" %} -{% set ospf_version = "v2" if ipv4 else "v3" %} -protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(ipv4) }} { +{% set is_ipv4 = version == "ipv4" %} +{% set ospf_version = "v2" if is_ipv4 else "v3" %} +protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(is_ipv4) }} { {{ version }} { {% if bird__ospf.table is defined %} - table {{ bird__ospf.table | bird_name(ipv4) }}; + table {{ bird__ospf.table | bird_name(is_ipv4) }}; {% endif %} - {{ import_export(bird__ospf, ipv4) | indent(8) }} + {{ import_export(bird__ospf, is_ipv4) | indent(8) }} }; {% for id, area in bird__ospf.areas.items() %} area {{ id }} { @@ -92,8 +92,8 @@ protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(ipv4) }} { {% for name, bgp in bird__bgp.items() %} {% for version in ["ipv4", "ipv6"] %} -{% set ipv4 = version == "ipv4" %} -protocol bgp {{ name | bird_name(ipv4) }} { +{% set is_ipv4 = version == "ipv4" %} +protocol bgp {{ name | bird_name(is_ipv4) }} { local {{ bgp.local.address | ansible.utils.ipaddr(version) | first }} as {{ bgp.local.as }}; @@ -106,12 +106,12 @@ protocol bgp {{ name | bird_name(ipv4) }} { {% endif %} {{ version }} { {% if bgp.table is defined %} - table {{ bgp.table | bird_name(ipv4) }}; + table {{ bgp.table | bird_name(is_ipv4) }}; {% endif %} {% if bgp.next_hop_self is defined %} next hop self; {% endif %} - {{ import_export(bgp, ipv4) | indent(8) }} + {{ import_export(bgp, is_ipv4) | indent(8) }} }; } {% endfor %}