feature: export control by neighbour AS and by location
The production router has the same neighbour AS at several exchanges, so per-AS control alone cannot express "only at SwissIX". Every session now has two coordinates, neighbour AS and location, and the scheme gains two functions: 210036:0:<as> / 210036:2:<location> never announce (wins) 210036:1:<as> / 210036:3:<location> announce 210036:0:0 announce nowhere nothing announce everywhere docs/lab/prod/bird-ferrix.conf documents the functions, the per-session call (export where peers_out(<as>, <location>)) and the gate that sessions exporting everything need as well. Status page: location presets, and AS210036 communities are decoded from their structure so any value reads well in the bytes hover. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
98f25a5847
commit
57099da73d
2 files changed
+83
-61
No files matched your search
@@ -120,18 +120,21 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
<script>
|
||||
(() => {
|
||||
// Large-community presets offered next to the field, grouped by who acts
|
||||
// on them. "PEER" is replaced by an AS number the user is asked for. Both
|
||||
// groups share one shape and precedence: 0:<as> never, 1:<as> send,
|
||||
// 0:0 nobody, otherwise everyone.
|
||||
// AS210036: docs/lab/prod/bird-ferrix.conf (lab_export_allowed)
|
||||
// on them. "PEER" is replaced by an AS number the user is asked for.
|
||||
// AS210036: docs/lab/prod/bird-ferrix.conf (lab_export_allowed). Every
|
||||
// session there has a neighbour AS and a location; "never"
|
||||
// (functions 0 and 2) beats "announce" (1 and 3) beats 0:0.
|
||||
// SwissIX: https://www.swissix.ch/resources/action-bgp-communities/
|
||||
const LOCATIONS = { 1: 'SwissIX', 2: 'NL-ix', 3: 'France-IX', 4: 'DE-CIX Frankfurt', 5: 'Free-IX Switzerland', 6: 'Community-IX.ch', 100: 'transit', 200: 'downstream customers' };
|
||||
const LARGE_PRESET_GROUPS = [
|
||||
['AS210036', 'AS210036, our upstream router', [
|
||||
['210036:0:0', 'announce to nobody'],
|
||||
['AS210036', 'AS210036: by neighbour', [
|
||||
['210036:0:0', 'announce nowhere (then add where to)'],
|
||||
['210036:1:42476', 'announce to the SwissIX route servers'],
|
||||
['210036:1:PEER', 'announce to neighbour AS'],
|
||||
['210036:0:PEER', 'do not announce to neighbour AS'],
|
||||
['210036:0:PEER', 'never announce to neighbour AS'],
|
||||
]],
|
||||
['AS210036', 'AS210036: by location', Object.entries(LOCATIONS).filter(([id]) => id < 100).map(([id, name]) => [`210036:3:${id}`, `announce at ${name}`])
|
||||
.concat([['210036:2:100', 'never announce to transit']])],
|
||||
['SwissIX', 'SwissIX route servers (AS42476)', [
|
||||
['42476:0:0', 'send to nobody'],
|
||||
['42476:1:559', 'send to SWITCH (AS559)'],
|
||||
@@ -273,6 +276,16 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
return out;
|
||||
}
|
||||
function largeLabel(c) {
|
||||
// Our own scheme is decoded from its structure, so any value reads well,
|
||||
// not only the ones in the menu.
|
||||
const own = c.match(/^210036:([0-3]):(\d+)$/);
|
||||
if (own) {
|
||||
const [f, t] = [Number(own[1]), Number(own[2])], where = LOCATIONS[t] || `location ${t}`;
|
||||
if (f === 0 && t === 0) return 'AS210036: announce nowhere, unless another community says where';
|
||||
if (f === 0) return `AS210036: never announce to AS${t}`;
|
||||
if (f === 1) return `AS210036: announce to AS${t}${t === 42476 ? ' (the SwissIX route servers)' : ''}`;
|
||||
return `AS210036: ${f === 2 ? 'never announce' : 'announce'} ${t >= 100 ? 'to' : 'at'} ${where}`;
|
||||
}
|
||||
// Exact presets first, so 42476:1:559 reads "SWITCH" rather than "AS559".
|
||||
for (const pass of [false, true]) for (const [who, , presets] of LARGE_PRESET_GROUPS) for (const [pattern, label] of presets) {
|
||||
if (pattern.includes('PEER') !== pass) continue;
|
||||
|
||||
@@ -39,12 +39,9 @@ filter ferrix_import
|
||||
# (210036, *, *) space, so students cannot trigger anything else this
|
||||
# network acts on. Add further namespaces here deliberately.
|
||||
#
|
||||
# And our own export-control communities, evaluated by
|
||||
# lab_export_allowed() below:
|
||||
# 210036:0:<peer-as> do not announce to that neighbour AS
|
||||
# 210036:1:<peer-as> announce to that neighbour AS
|
||||
# 210036:0:0 announce to nobody (unless 210036:1:<peer-as> says so)
|
||||
bgp_large_community.filter([ (42476, *, *), (MY_ASN, 0..1, *) ]);
|
||||
# And our own export-control communities, functions 0 to 3, evaluated by
|
||||
# lab_export_allowed() below.
|
||||
bgp_large_community.filter([ (42476, *, *), (MY_ASN, 0..3, *) ]);
|
||||
|
||||
bgp_ext_community.empty;
|
||||
|
||||
@@ -85,70 +82,82 @@ protocol bgp ferrix
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export control for lab routes.
|
||||
#
|
||||
# Same shape and same precedence as the SwissIX route-server communities, so
|
||||
# students apply one idea at two hops: first here, then at the exchange.
|
||||
# BIRD export filters cannot see which neighbour they are exporting to, and
|
||||
# filters take no arguments; functions do. So every eBGP session states who it
|
||||
# talks to and where, by calling the shared export function with its neighbour
|
||||
# AS and a location:
|
||||
#
|
||||
# 1. 210036:0:<peer-as> never announce to that neighbour
|
||||
# 2. 210036:1:<peer-as> announce to that neighbour
|
||||
# 3. 210036:0:0 announce to nobody
|
||||
# 4. (nothing) announce to everyone <- default
|
||||
# ipv6 { export where peers_out(42476, AT_SWISSIX); ... };
|
||||
#
|
||||
# <peer-as> is the AS a session's wrapper filter passes in (see below). For a
|
||||
# route server that is the route server's AS, e.g. 42476 for SwissIX.
|
||||
# Sessions without their own wrapper cannot be selected individually.
|
||||
# Students steer their prefix with large communities, in this precedence:
|
||||
#
|
||||
# "Only to SwissIX": 210036:0:0 210036:1:42476
|
||||
# "...and there only to SWITCH": add 42476:0:0 42476:1:559
|
||||
# 1. 210036:0:<as> never announce to that neighbour AS
|
||||
# 210036:2:<location> never announce at that location
|
||||
# 2. 210036:1:<as> announce to that neighbour AS
|
||||
# 210036:3:<location> announce at that location
|
||||
# 3. 210036:0:0 announce nowhere (unless rule 2 says otherwise)
|
||||
# 4. nothing announce everywhere <- default
|
||||
#
|
||||
# A "never" always wins, so the two dimensions combine. The shape and
|
||||
# precedence match the SwissIX route-server communities on purpose, so one
|
||||
# idea is applied at two hops.
|
||||
#
|
||||
# only the SwissIX route servers: 210036:0:0 210036:1:42476
|
||||
# ...and there only SWITCH: add 42476:0:0 42476:1:559
|
||||
# every session at SwissIX: 210036:0:0 210036:3:1
|
||||
# Cloudflare, but not at DE-CIX: 210036:0:0 210036:1:13335 210036:2:4
|
||||
# ---------------------------------------------------------------------------
|
||||
function lab_export_allowed(int peer_as)
|
||||
define AT_SWISSIX = 1;
|
||||
define AT_NLIX = 2;
|
||||
define AT_FRANCEIX = 3;
|
||||
define AT_DECIX = 4;
|
||||
define AT_FREEIX = 5;
|
||||
define AT_COMMUNITYIX = 6;
|
||||
define AT_TRANSIT = 100;
|
||||
define AT_DOWNSTREAM = 200;
|
||||
|
||||
function lab_export_allowed(int peer_as; int location)
|
||||
{
|
||||
if (MY_ASN, 0, peer_as) ~ bgp_large_community then return false;
|
||||
if (MY_ASN, 2, location) ~ bgp_large_community then return false;
|
||||
if (MY_ASN, 1, peer_as) ~ bgp_large_community then return true;
|
||||
if (MY_ASN, 3, location) ~ bgp_large_community then return true;
|
||||
if (MY_ASN, 0, 0) ~ bgp_large_community then return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
# Wiring it into a shared export filter
|
||||
# -------------------------------------
|
||||
# BIRD export filters cannot see which neighbour they are exporting to, and
|
||||
# filters take no arguments; functions do. So the shared filter's body lives
|
||||
# in a function taking the neighbour AS, and named filters are thin wrappers
|
||||
# around it. Sessions that should be individually selectable get their own
|
||||
# wrapper; every other session keeps using "peers_out" unchanged and is
|
||||
# evaluated with AS 0, which means it obeys 210036:0:0 but cannot be singled
|
||||
# out. Add a wrapper per neighbour as the need arises.
|
||||
# Decides whether a lab route goes out on a session and, if so, makes it
|
||||
# presentable: the origin must appear as AS210036 (matching the route6
|
||||
# objects) and our action communities must not leak.
|
||||
function lab_route_out(int peer_as; int location)
|
||||
{
|
||||
if !lab_export_allowed(peer_as, location) then return false;
|
||||
bgp_path.delete([ 64512..65534, 4200000000..4294967294 ]);
|
||||
bgp_large_community.delete([ (MY_ASN, 0..3, *) ]);
|
||||
return true;
|
||||
}
|
||||
|
||||
# The shared export function then ends its list of accepted routes with
|
||||
#
|
||||
# function peers_out_fn(int peer_as)
|
||||
# {
|
||||
# bgp_med = 0;
|
||||
# bgp_large_community.add((210036, 10, 2));
|
||||
# if (net = 193.36.104.0/24) then return true;
|
||||
# if (net = 2a0d:d740::/48) then return true;
|
||||
# if (net = 2a0d:d740:105::/48) then return true;
|
||||
# if proto = "delroth_v6" then return true;
|
||||
# if proto = "pkern_v6" then return true;
|
||||
# if proto = "home_v4" then return true;
|
||||
# if proto = "ferrix" then return lab_route_out(peer_as, location);
|
||||
# return false;
|
||||
#
|
||||
# and returns true for everything else it accepts. Sessions that export
|
||||
# everything (downstream customers) need the same gate, or they receive lab
|
||||
# routes regardless of what the student asked for:
|
||||
#
|
||||
# export filter {
|
||||
# if proto = "ferrix" then {
|
||||
# if !lab_export_allowed(peer_as) then return false;
|
||||
# # Origin must appear as AS210036, matching the route6 objects.
|
||||
# bgp_path.delete([ 64512..65534, 4200000000..4294967294 ]);
|
||||
# # Our action communities have done their job; do not leak them.
|
||||
# bgp_large_community.delete([ (MY_ASN, 0..1, *) ]);
|
||||
# return true;
|
||||
# if lab_route_out(210400, AT_DOWNSTREAM) then accept;
|
||||
# reject;
|
||||
# }
|
||||
# return false;
|
||||
# }
|
||||
#
|
||||
# filter peers_out { if peers_out_fn(0) then accept; reject; }
|
||||
# filter peers_out_swissix { if peers_out_fn(42476) then accept; reject; }
|
||||
#
|
||||
# Then only the SwissIX route-server sessions change, to
|
||||
# export filter peers_out_swissix;
|
||||
#
|
||||
# lab_export_allowed() must be defined above peers_out_fn in the file.
|
||||
# accept;
|
||||
# };
|
||||
#
|
||||
# The ferrix branch only ever applies to routes learned from ferrix, so these
|
||||
# communities cannot be used by anyone else to steer your other exports.
|
||||
# communities cannot be used by anyone else to steer other exports.
|
||||
#
|
||||
# If this BIRD rejects "int peer_as; int location", it wants a comma there.
|
||||
#
|
||||
# Check what a session would send, without waiting for a looking glass:
|
||||
# birdc show route 2a0d:d741:1::/48 export <protocol-name> all
|
||||
|
||||
Reference in new issue
Block a user