feature: AS210036 export-control large communities for lab routes

BIRD side (docs/lab/prod/bird-ferrix.conf): the ferrix import filter now
admits 210036:0:* and 210036:1:*, and lab_export_allowed(peer_as) decides
per eBGP session whether a lab route is announced, with the same shape and
precedence as the SwissIX route-server communities:
  1. 210036:0:<peer-as>  never   2. 210036:1:<peer-as>  announce
  3. 210036:0:0          nobody  4. default             everyone
The per-session export block applies only to routes learned from ferrix,
strips the private ASN and removes the action communities before sending.

Status page: the large-community picker is grouped into "AS210036" and
"SwissIX route servers" presets, including "announce to the SwissIX route
servers" and "send to SWITCH (AS559)"; the bytes hover names both families.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-09-20 17:02:42 +02:00
1 parent eb77090d01
commit b28f59f30d
2 files changed
+89 -29

No files matched your search

+35 -17
View File
@@ -89,8 +89,8 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
<tr><td><label for="f-communities">Communities:</label></td><td><input id="f-communities" size="50" placeholder="65000:100, NO_EXPORT">
<select id="f-wk" aria-label="Add a well-known community"><option value="">+ well-known...</option></select>
<span class="dim">comma separated, ASN:VALUE or a name</span></td></tr>
<tr><td><label for="f-large">Large communities:</label></td><td><input id="f-large" size="50" placeholder="42476:0:6939">
<select id="f-lc" aria-label="Add a large community preset"><option value="">+ SwissIX...</option></select>
<tr><td><label for="f-large">Large communities:</label></td><td><input id="f-large" size="50" placeholder="210036:0:0, 210036:1:42476">
<select id="f-lc" aria-label="Add a large community preset"><option value="">+ presets...</option></select>
<span class="dim">GLOBAL:LOCAL1:LOCAL2</span></td></tr>
<tr><td></td><td><input type="submit" value="Announce"> <input type="button" id="f-clear" value="Clear">
&nbsp; <span id="announce-msg"></span></td></tr>
@@ -119,16 +119,28 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
<script>
(() => {
// Large-community presets offered next to the field. "PEER" is replaced by
// an AS number the user is asked for. Source:
// https://www.swissix.ch/resources/action-bgp-communities/
const LARGE_PRESETS = [
['42476:0:PEER', 'do not send to peer AS'],
['42476:1:PEER', 'send to peer AS'],
['42476:0:0', 'send to nobody'],
['42476:1:0', 'send to all (default)'],
['42476:901:PEER', 'add NO_EXPORT towards peer AS'],
['42476:902:PEER', 'add NO_ADVERTISE towards peer AS'],
// 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)
// SwissIX: https://www.swissix.ch/resources/action-bgp-communities/
const LARGE_PRESET_GROUPS = [
['AS210036', 'AS210036, our upstream router', [
['210036:0:0', 'announce to nobody'],
['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'],
]],
['SwissIX', 'SwissIX route servers (AS42476)', [
['42476:0:0', 'send to nobody'],
['42476:1:559', 'send to SWITCH (AS559)'],
['42476:1:PEER', 'send to peer AS'],
['42476:0:PEER', 'do not send to peer AS'],
['42476:1:0', 'send to all (default)'],
['42476:901:PEER', 'add NO_EXPORT towards peer AS'],
['42476:902:PEER', 'add NO_ADVERTISE towards peer AS'],
]],
];
const $ = id => document.getElementById(id);
@@ -261,9 +273,11 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
return out;
}
function largeLabel(c) {
for (const [pattern, label] of LARGE_PRESETS) {
// 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;
const m = c.match(new RegExp('^' + pattern.replace('PEER', '(\\d+)') + '$'));
if (m) return label.replace('peer AS', 'AS' + (m[1] || ''));
if (m) return `${who}: ${label.replace(/(peer|neighbour) AS/, 'AS' + (m[1] || ''))}`;
}
return null;
}
@@ -292,7 +306,7 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
case 4: rows.push(['MED', `${u(v, 0, 4)} (lower is preferred, compared between routes from the same neighbour AS)`]); break;
case 5: rows.push(['local preference', `${u(v, 0, 4)} (higher is preferred)`]); break;
case 8: for (let o = 0; o + 4 <= v.length; o += 4) { const c = `${u(v, o, 2)}:${u(v, o + 2, 2)}`; rows.push(['community', c + (state.wellKnown[c] ? ` = ${state.wellKnown[c]}` : '')]); } break;
case 32: for (let o = 0; o + 12 <= v.length; o += 12) { const c = `${u(v, o, 4)}:${u(v, o + 4, 4)}:${u(v, o + 8, 4)}`, l = largeLabel(c); rows.push(['large community', c + (l ? ` = SwissIX: ${l}` : '')]); } break;
case 32: for (let o = 0; o + 12 <= v.length; o += 12) { const c = `${u(v, o, 4)}:${u(v, o + 4, 4)}:${u(v, o + 8, 4)}`, l = largeLabel(c); rows.push(['large community', c + (l ? ` = ${l}` : '')]); } break;
case 14: {
const afi = u(v, 0, 2), nhLen = v[3], nh = v.slice(4, 4 + nhLen);
rows.push(['address family', `${AFI[afi] || afi} ${SAFI[v[2]] || v[2]}`]);
@@ -449,7 +463,7 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
let v = e.target.value; e.target.value = '';
if (!v) return;
if (v.includes('PEER')) {
const peer = (prompt('Peer AS number?') || '').trim().replace(/^AS/i, '');
const peer = (prompt('AS number of the peer or neighbour?') || '').trim().replace(/^AS/i, '');
if (!/^\d+$/.test(peer)) { if (peer) setMsg(`"${peer}" is not an AS number`, 'bad'); return; }
v = v.replace('PEER', peer);
}
@@ -469,7 +483,11 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
// ---- boot --------------------------------------------------------------
try { $('f-owner').value = localStorage.getItem('owner') || ''; } catch (_) {}
LARGE_PRESETS.forEach(([value, label]) => { const o = document.createElement('option'); o.value = value; o.textContent = `${value.replace('PEER', '<peer-as>')} ${label}`; $('f-lc').appendChild(o); });
LARGE_PRESET_GROUPS.forEach(([, title, presets]) => {
const g = document.createElement('optgroup'); g.label = title;
presets.forEach(([value, label]) => { const o = document.createElement('option'); o.value = value; o.textContent = `${value.replace('PEER', '<as>')} ${label}`; g.appendChild(o); });
$('f-lc').appendChild(g);
});
getJSON('/api/communities/well-known').then(list => {
list.forEach(c => { state.wellKnown[c.value] = c.name; const o = document.createElement('option'); o.value = c.name; o.textContent = `${c.name} (${c.value})`; $('f-wk').appendChild(o); });
renderLocal(); renderReceived();
+54 -12
View File
@@ -7,6 +7,7 @@
# Inspect: birdc show protocols all ferrix
# birdc show route protocol ferrix all
define MY_ASN = 210036;
define FERRIX_ASN = 4200000001;
define LAB_PREFIXES = [ 2a0d:d741::/40{48,48} ];
@@ -34,10 +35,16 @@ filter ferrix_import
# 42476:1:<peer-as> redistribute to peer 42476:1:0 to all
# 42476:101|102|103:<peer-as or 0> prepend 1x / 2x / 3x
# 42476:901|902:<peer-as> add NO_EXPORT / NO_ADVERTISE
# Everything else is dropped, including our own (210036, *, *) space so
# students cannot trigger anything this network acts on internally. Add
# further namespaces here deliberately.
bgp_large_community.filter([ (42476, *, *) ]);
# Everything else is dropped, including the rest of our own
# (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, *) ]);
bgp_ext_community.empty;
@@ -76,21 +83,56 @@ protocol bgp ferrix
}
# ---------------------------------------------------------------------------
# Your upstream / IXP export filters must let these routes out, and must
# strip ferrix's private ASN so the origin seen by the Internet is AS210036,
# matching the route6 objects. Add, before the final reject of each such
# filter:
# 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.
#
# 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
#
# <peer-as> is the AS number on the session's "neighbor ... as N" line. For a
# route server that is the route server's AS, e.g. 42476 for SwissIX.
#
# "Only to SwissIX": 210036:0:0 210036:1:42476
# "...and there only to SWITCH": add 42476:0:0 42476:1:559
# ---------------------------------------------------------------------------
function lab_export_allowed(int peer_as)
{
if (MY_ASN, 0, peer_as) ~ bgp_large_community then return false;
if (MY_ASN, 1, peer_as) ~ bgp_large_community then return true;
if (MY_ASN, 0, 0) ~ bgp_large_community then return false;
return true;
}
# In the export filter of EVERY eBGP session (transit, IXP route servers,
# bilateral peers), before its final reject, add this block with that
# session's neighbour AS in place of 42476:
#
# if proto = "ferrix" then {
# if !lab_export_allowed(42476) then reject;
# # 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, *) ]);
# accept;
# }
#
# The block only ever applies to routes learned from ferrix, so these
# communities cannot be used by anyone else to steer your other exports.
# A session that lacks the block never announces lab routes at all, which is
# the safe failure.
#
# Check what a session would send, without waiting for a looking glass:
# birdc show route 2a0d:d741:1::/48 export <protocol-name> all
#
# The status page no longer offers AS-path prepending or the SwissIX prepend
# communities (42476:101..103:*), to keep the exercise focused. The import
# filter above still admits the whole 42476 namespace, so a student who types
# one by hand can use it; narrow the filter if that is not wanted.
# filter still admits the whole 42476 namespace, so a student who types one
# by hand can use it; narrow the filter if that is not wanted.
#
# The session towards the SwissIX route servers must also send large
# communities unmodified; check that its export filter does not clear them.
# The session towards the SwissIX route servers must send large communities
# unmodified apart from the delete above; check nothing else clears them.
# ---------------------------------------------------------------------------