feature: config zippy on statusz; drop prepend options from the UI
- The status page has a collapsible "Configuration" section showing the config file as read at startup, with its path. Served by the new GET /api/config; RouterInfo::with_config carries the text. - Removed the AS-path prepends field and the SwissIX prepend presets (42476:101..103) from the form to keep the exercise focused. The API still accepts `prepend`. - Docs follow: the handout loses its prepending exercise, the lab README its prepend row, and the BIRD config notes that the 42476 namespace is still admitted for hand-typed values. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f04152f19
commit
eb77090d01
9 files changed
+69
-32
No files matched your search
+8
-5
@@ -33,11 +33,14 @@ async fn main() -> Result<()> {
|
||||
|
||||
if let Some(http_endpoint) = server.config().http_endpoint {
|
||||
let state = AppState {
|
||||
info: Arc::new(RouterInfo::new(
|
||||
server.config().router_id,
|
||||
server.config().local_asn,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)),
|
||||
info: Arc::new(
|
||||
RouterInfo::new(
|
||||
server.config().router_id,
|
||||
server.config().local_asn,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
.with_config(&args.config, &config_string),
|
||||
),
|
||||
registry: server.registry.clone(),
|
||||
local_rib: server.local_rib.clone(),
|
||||
adj_rib_in: server.adj_rib_in.clone(),
|
||||
|
||||
@@ -87,7 +87,10 @@ impl TestServer {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let base_url = format!("http://{}", listener.local_addr()?);
|
||||
let state = AppState {
|
||||
info: std::sync::Arc::new(RouterInfo::new(Ipv4Addr::new(127, 0, 0, 1), self.local_asn, "test")),
|
||||
info: std::sync::Arc::new(
|
||||
RouterInfo::new(Ipv4Addr::new(127, 0, 0, 1), self.local_asn, "test")
|
||||
.with_config("test.textpb", format!("local_asn: {}\n", self.local_asn)),
|
||||
),
|
||||
registry: self.registry.clone(),
|
||||
local_rib: self.local_rib.clone(),
|
||||
adj_rib_in: self.adj_rib_in.clone(),
|
||||
|
||||
@@ -45,6 +45,10 @@ async fn test_status_and_peers() -> Result<()> {
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let config = get_json(&client, &format!("{base}/api/config")).await?;
|
||||
assert_eq!(config["path"], "test.textpb");
|
||||
assert_eq!(config["text"], format!("local_asn: {LOCAL_ASN}\n"));
|
||||
|
||||
let status = get_json(&client, &format!("{base}/api/status")).await?;
|
||||
assert_eq!(status["local_asn"], LOCAL_ASN);
|
||||
assert_eq!(status["router_id"], "127.0.0.1");
|
||||
|
||||
@@ -348,6 +348,20 @@ pub async fn status(State(state): State<AppState>) -> Json<StatusView> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The configuration file the daemon was started with.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ConfigView {
|
||||
pub path: Option<String>,
|
||||
/// Raw file contents as read at startup; later edits to the file are not
|
||||
/// reflected until the daemon restarts.
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn config(State(state): State<AppState>) -> Json<ConfigView> {
|
||||
let (path, text) = state.info.config.clone().unzip();
|
||||
Json(ConfigView { path, text })
|
||||
}
|
||||
|
||||
pub async fn peers(State(state): State<AppState>) -> Json<Vec<PeerView>> {
|
||||
Json(peer_views(&state.registry))
|
||||
}
|
||||
|
||||
+12
-1
@@ -25,7 +25,7 @@ use tokio::net::TcpListener;
|
||||
use tracing::info;
|
||||
|
||||
pub use api::{
|
||||
AnnounceRequest, AttrSummary, LocalRouteView, PeerView, ReceivedRouteView, StatusView,
|
||||
AnnounceRequest, ConfigView, AttrSummary, LocalRouteView, PeerView, ReceivedRouteView, StatusView,
|
||||
WellKnownCommunity, WireView, WithdrawRequest,
|
||||
};
|
||||
|
||||
@@ -40,6 +40,9 @@ pub struct RouterInfo {
|
||||
pub started_at: SystemTime,
|
||||
/// Name of the machine we run on, for display.
|
||||
pub hostname: String,
|
||||
/// The configuration file as read at startup (path and raw text), shown
|
||||
/// on the status page. `None` when the server was configured in code.
|
||||
pub config: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl RouterInfo {
|
||||
@@ -51,8 +54,15 @@ impl RouterInfo {
|
||||
started: Instant::now(),
|
||||
started_at: SystemTime::now(),
|
||||
hostname: hostname(),
|
||||
config: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the configuration file so the status page can show it.
|
||||
pub fn with_config(mut self, path: impl Into<String>, text: impl Into<String>) -> Self {
|
||||
self.config = Some((path.into(), text.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort host name without pulling in a dependency: ask the `hostname`
|
||||
@@ -89,6 +99,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/", get(|| async { Redirect::temporary("/statusz") }))
|
||||
.route("/statusz", get(statusz))
|
||||
.route("/api/status", get(api::status))
|
||||
.route("/api/config", get(api::config))
|
||||
.route("/api/peers", get(api::peers))
|
||||
.route("/api/routes/local", get(api::local_routes).post(api::announce))
|
||||
.route("/api/routes/local", delete(api::withdraw))
|
||||
|
||||
@@ -42,6 +42,10 @@ pre.bytes span[data-kind]:hover, pre.bytes span[data-kind]:focus { outline: 1px
|
||||
#tip td { padding: 1px 6px; vertical-align: top; }
|
||||
#tip td:first-child { color: #666; white-space: nowrap; }
|
||||
#tip td.mono { word-break: break-word; }
|
||||
details { clear: both; margin: 8px 0 0; }
|
||||
summary { cursor: pointer; color: #00c; }
|
||||
summary:hover { text-decoration: underline; }
|
||||
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
|
||||
address { font-size: smaller; margin-top: 2em; color: #444; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -62,6 +66,11 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details id="config-zippy" hidden>
|
||||
<summary>Configuration <span class="dim" id="config-path"></span></summary>
|
||||
<pre id="config-text"></pre>
|
||||
</details>
|
||||
|
||||
<h1 id="peers">Peers (<span id="peers-count">...</span>)</h1>
|
||||
<table class="data">
|
||||
<thead><tr><th>name</th><th>peer</th><th>AS</th><th>state</th><th>opened by</th><th>local address (next hop)</th><th>established</th><th>uptime</th><th>4-byte AS</th><th>detail</th></tr></thead>
|
||||
@@ -74,14 +83,13 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
<tr><td><label for="f-owner">Owner (your name):</label></td><td><input id="f-owner" size="20" maxlength="64" required></td>
|
||||
<td><label for="f-prefix">Prefix:</label></td><td><input id="f-prefix" size="24" required placeholder="2a0d:d741:1::/48"></td></tr>
|
||||
<tr><td><label for="f-origin">Origin:</label></td><td><select id="f-origin"><option>IGP</option><option>EGP</option><option>INCOMPLETE</option></select></td>
|
||||
<td><label for="f-prepend">AS-path prepends:</label></td><td><input id="f-prepend" type="number" min="0" max="16" value="0" style="width:4em">
|
||||
<label for="f-med">MED:</label> <input id="f-med" type="number" min="0" max="4294967295" placeholder="none" style="width:8em"></td></tr>
|
||||
<td><label for="f-med">MED:</label></td><td><input id="f-med" type="number" min="0" max="4294967295" placeholder="none" style="width:8em"></td></tr>
|
||||
</table>
|
||||
<table class="form">
|
||||
<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:101:0">
|
||||
<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>
|
||||
<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">
|
||||
@@ -119,12 +127,6 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
['42476:1:PEER', 'send to peer AS'],
|
||||
['42476:0:0', 'send to nobody'],
|
||||
['42476:1:0', 'send to all (default)'],
|
||||
['42476:101:0', 'prepend 1x to all'],
|
||||
['42476:102:0', 'prepend 2x to all'],
|
||||
['42476:103:0', 'prepend 3x to all'],
|
||||
['42476:101:PEER', 'prepend 1x to peer AS'],
|
||||
['42476:102:PEER', 'prepend 2x to peer AS'],
|
||||
['42476:103:PEER', 'prepend 3x to peer AS'],
|
||||
['42476:901:PEER', 'add NO_EXPORT towards peer AS'],
|
||||
['42476:902:PEER', 'add NO_ADVERTISE towards peer AS'],
|
||||
];
|
||||
@@ -429,7 +431,7 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
prefix: $('f-prefix').value.trim(), owner: myOwner(), origin: $('f-origin').value,
|
||||
prepend: Number($('f-prepend').value || 0), communities: splitList($('f-communities').value),
|
||||
communities: splitList($('f-communities').value),
|
||||
large_communities: splitList($('f-large').value),
|
||||
};
|
||||
if ($('f-med').value !== '') body.med = Number($('f-med').value);
|
||||
@@ -441,7 +443,7 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
if (r.ok) { setMsg(`announced ${j.prefix} to ${up} peer(s)`, 'ok'); reloadAfterAction(); }
|
||||
else setMsg(`${r.status}: ${j.error || 'request failed'}`, 'bad');
|
||||
});
|
||||
$('f-clear').addEventListener('click', () => { ['f-prefix','f-communities','f-large','f-med'].forEach(id => $(id).value = ''); $('f-prepend').value = 0; $('f-origin').value = 'IGP'; setMsg(''); });
|
||||
$('f-clear').addEventListener('click', () => { ['f-prefix','f-communities','f-large','f-med'].forEach(id => $(id).value = ''); $('f-origin').value = 'IGP'; setMsg(''); });
|
||||
$('f-wk').addEventListener('change', e => { if (e.target.value) appendTo('f-communities', e.target.value); e.target.value = ''; });
|
||||
$('f-lc').addEventListener('change', e => {
|
||||
let v = e.target.value; e.target.value = '';
|
||||
@@ -472,6 +474,12 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
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();
|
||||
}).catch(console.error);
|
||||
getJSON('/api/config').then(c => {
|
||||
if (!c.text) return;
|
||||
$('config-path').textContent = c.path ? `(${c.path}, as read at startup)` : '';
|
||||
$('config-text').textContent = c.text;
|
||||
$('config-zippy').hidden = false;
|
||||
}).catch(console.error);
|
||||
renderRefreshControl();
|
||||
loadAll().catch(e => logEvent(`<span class="bad">${esc(e.message)}</span>`));
|
||||
if (REFRESH) {
|
||||
|
||||
+4
-9
@@ -31,26 +31,21 @@ means it is one of the prefixes announced locally: it went around the loop.
|
||||
the AS_PATH attribute. How many bytes does your ASN take up in the path,
|
||||
and why?
|
||||
|
||||
3. **Prepending.** Withdraw, then announce again with *AS-path prepends* set
|
||||
to 3. Compare the AS path you get back with exercise 1. Where in the path
|
||||
did the extra entries appear? What would a router in AS65002 that has two
|
||||
routes to your prefix conclude?
|
||||
|
||||
4. **NO_EXPORT.** Announce with the community `NO_EXPORT` (use the
|
||||
3. **NO_EXPORT.** Announce with the community `NO_EXPORT` (use the
|
||||
well-known picker). Wait ten seconds. Does your prefix come back? Look at
|
||||
the topology and explain which router stopped it and why it still
|
||||
accepted it.
|
||||
|
||||
5. **Asking a neighbour for help.** Announce with the community
|
||||
4. **Asking a neighbour for help.** Announce with the community
|
||||
`65001:100`. r1 has been configured to act on that. What changed in the AS
|
||||
path this time, and who changed it? This is how real operators ask an
|
||||
upstream to prepend on their behalf; look up your ISP's "BGP communities"
|
||||
page later.
|
||||
|
||||
6. **Blackholing.** Announce with `BLACKHOLE`. What happens? When would an
|
||||
5. **Blackholing.** Announce with `BLACKHOLE`. What happens? When would an
|
||||
operator want this?
|
||||
|
||||
7. **Withdraw.** Withdraw your prefix and watch the event log. How many
|
||||
6. **Withdraw.** Withdraw your prefix and watch the event log. How many
|
||||
messages did it take, and what did the withdrawal look like on the wire
|
||||
compared to the announcement? (Click *bytes* before withdrawing to compare.)
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ docker exec r1 vtysh -c 'show bgp neighbors 10.200.0.1 advertised-routes'
|
||||
| `NO_EXPORT` | keeps it, does not send to r2 (RFC 1997) | prefix does not return |
|
||||
| `65001:100` | prepends `65001` three times | returns with path `65002 65001 65001 65001 65001 65000` |
|
||||
| `BLACKHOLE` (65535:666) | drops it on import | prefix does not return |
|
||||
| AS-path prepends on ferrix| nothing special | returns with extra `65000`s in the path |
|
||||
|
||||
## Without containers
|
||||
|
||||
|
||||
@@ -86,10 +86,10 @@ protocol bgp ferrix
|
||||
# accept;
|
||||
# }
|
||||
#
|
||||
# Consequence: AS-path prepends set by students are made of the private ASN
|
||||
# and are stripped here too, so prepending in ferrix has no effect beyond
|
||||
# this router. The SwissIX prepend communities (42476:101..103:*) do work:
|
||||
# the route server prepends AS210036 itself.
|
||||
# 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.
|
||||
#
|
||||
# The session towards the SwissIX route servers must also send large
|
||||
# communities unmodified; check that its export filter does not clear them.
|
||||
|
||||
Reference in new issue
Block a user