diff --git a/.gitignore b/.gitignore index 531b1b6..2c669e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ /target __pycache__/ -slides/reveal.js/ -slides/dist/ .DS_Store diff --git a/Cargo.lock b/Cargo.lock index 797ab74..ab238bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,7 +225,6 @@ dependencies = [ "clap", "eyre", "futures", - "libc", "prometheus", "protobuf", "protobuf-codegen", diff --git a/Cargo.toml b/Cargo.toml index 3caa5aa..ed88546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ protobuf = { package = "protobuf", version = "3.7.2" } protobuf-codegen = { package = "protobuf-codegen", version = "3.7.2" } clap = { version = "4.5.53", features = ["derive"] } futures = "0.3" -libc = "0.2" axum = "0.8.9" tokio-stream = { version = "0.1.19", features = ["sync"] } reqwest = { version = "0.13.5", default-features = false, features = ["json"] } diff --git a/bin/bgp/src/main.rs b/bin/bgp/src/main.rs index bbe137a..8b2a7bc 100644 --- a/bin/bgp/src/main.rs +++ b/bin/bgp/src/main.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use std::time::Duration; use bgp_server::config::Config; -use bgp_server::ping::PingMonitor; use bgp_server::proto::config::BgpConfig; use bgp_server::server::BgpServerBuilder; use bgp_server::tracing::TracingArgs; @@ -33,11 +32,6 @@ async fn main() -> Result<()> { let server = BgpServerBuilder::new(config).bind().await?; - let pings = match &server.config().ping_monitor { - Some(prefixes) => PingMonitor::start(prefixes.clone(), server.local_rib.clone()), - None => PingMonitor::disabled(), - }; - if let Some(http_endpoint) = server.config().http_endpoint { let state = AppState { info: Arc::new( @@ -51,7 +45,6 @@ async fn main() -> Result<()> { registry: server.registry.clone(), local_rib: server.local_rib.clone(), adj_rib_in: server.adj_rib_in.clone(), - pings, }; tokio::spawn(async move { if let Err(e) = bgp_web::serve(state, http_endpoint).await { diff --git a/crates/integration-tests/src/test_server.rs b/crates/integration-tests/src/test_server.rs index 76129c8..90c05fe 100644 --- a/crates/integration-tests/src/test_server.rs +++ b/crates/integration-tests/src/test_server.rs @@ -1,9 +1,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use bgp_rib::{AdjRibIn, LocalRib, LocalRibPolicy}; -use bgp_primitives::ip_prefix::IpPrefix; use bgp_server::config::Config; -use bgp_server::ping::PingMonitor; use bgp_server::server::BgpServerBuilder; use bgp_server::session::SessionRegistry; use bgp_web::{AppState, RouterInfo}; @@ -23,9 +21,6 @@ pub struct TestServer { pub local_rib: LocalRib, /// Live view of the server's established sessions. pub registry: SessionRegistry, - /// Ping monitor without a socket, watching 2001:db8::/32; tests feed it - /// through `record`. - pub pings: PingMonitor, pub local_asn: u32, } @@ -66,7 +61,6 @@ impl TestServer { collision_detect_established_state: false, local_route_policy: LocalRibPolicy::default(), http_endpoint: None, - ping_monitor: None, }; customize(&mut config); @@ -80,13 +74,8 @@ impl TestServer { let local_rib = bound.local_rib.clone(); let registry = bound.registry.clone(); - let pings = PingMonitor::without_socket( - vec![IpPrefix::try_from("2001:db8::/32").expect("valid prefix")], - Some(local_rib.clone()), - ); - let handle = tokio::spawn(bound.run()); - Ok(Self { handle, addr, adj_rib_in, local_rib, registry, pings, local_asn }) + Ok(Self { handle, addr, adj_rib_in, local_rib, registry, local_asn }) } } @@ -105,7 +94,6 @@ impl TestServer { registry: self.registry.clone(), local_rib: self.local_rib.clone(), adj_rib_in: self.adj_rib_in.clone(), - pings: self.pings.clone(), }; let handle = tokio::spawn(bgp_web::serve_on(state, listener)); Ok((base_url, HttpGuard { handle })) diff --git a/crates/integration-tests/tests/pings.rs b/crates/integration-tests/tests/pings.rs deleted file mode 100644 index 2553028..0000000 --- a/crates/integration-tests/tests/pings.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! The ping monitor as seen through the HTTP API. The socket is left out -//! (it needs privileges); observations are fed in directly. - -use std::time::Duration; - -use bgp_integration_tests::test_server::TestServer; -use bgp_primitives::ip_prefix::IpPrefix; -use bgp_rib::LocalRoute; -use bgp_server::ping::PingObservation; -use eyre::Result; -use serde_json::Value; - -const TIMEOUT: Duration = Duration::from_secs(5); - -fn ping(src: &str, dst: &str, hop_limit: u8, seq: u16) -> PingObservation { - PingObservation { - src: src.parse().unwrap(), - dst: dst.parse().unwrap(), - interface: Some("swissix0".into()), - hop_limit: Some(hop_limit), - ident: 1, - seq, - bytes: 64, - packet: [&[128, 0, 0xab, 0xcd, 0, 1][..], &seq.to_be_bytes(), b"hi"].concat(), - } -} - -#[tokio::test] -async fn pings_are_listed_with_the_route_they_were_sent_to() -> Result<()> { - let server = TestServer::start(65000).await?; - let (base, _http) = server.serve_http().await?; - let client = reqwest::Client::new(); - server - .local_rib - .announce(LocalRoute::new(IpPrefix::try_from("2001:db8:1a::/48").unwrap(), "alice", Default::default())) - .await?; - - server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 0)).await; - server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 1)).await; - server.pings.record(ping("2a02:1210::9", "2001:db8:ff::1", 243, 0)).await; - server.pings.record(ping("2a02:1210::9", "2a0d:d740::1", 243, 0)).await; // not a watched prefix - - let view: Value = client.get(format!("{base}/api/pings")).send().await?.error_for_status()?.json().await?; - assert_eq!(view["status"], "listening"); - assert_eq!(view["prefixes"][0], "2001:db8::/32"); - assert_eq!(view["total_flows"], 2); - - let flows = view["flows"].as_array().unwrap(); - let alice = flows.iter().find(|f| f["dst"] == "2001:db8:1a::1").unwrap(); - assert_eq!(alice["src"], "2001:67c:10ec::5"); - assert_eq!(alice["prefix"], "2001:db8:1a::/48"); - assert_eq!(alice["owner"], "alice"); - assert_eq!(alice["interface"], "swissix0"); - assert_eq!((alice["hop_limit"].as_u64(), alice["hops"].as_u64()), (Some(57), Some(7))); - assert_eq!((alice["count"].as_u64(), alice["seq"].as_u64()), (Some(2), Some(1))); - assert_eq!(alice["hex"], "8000abcd000100016869"); - assert!(alice["last_seen_unix"].as_u64().unwrap() > 1_700_000_000); - - let stray = flows.iter().find(|f| f["dst"] == "2001:db8:ff::1").unwrap(); - assert!(stray["prefix"].is_null() && stray["owner"].is_null()); - assert_eq!(stray["hops"], 12); - - let limited: Value = client.get(format!("{base}/api/pings?limit=1")).send().await?.json().await?; - assert_eq!(limited["total_flows"], 2); - assert_eq!(limited["flows"].as_array().unwrap().len(), 1); - - let metrics = client.get(format!("{base}/metricz")).send().await?.text().await?; - assert!(metrics.contains("ping_monitor_echo_requests_total")); - Ok(()) -} - -#[tokio::test] -async fn pings_appear_on_the_event_stream() -> Result<()> { - let server = TestServer::start(65000).await?; - let (base, _http) = server.serve_http().await?; - let mut resp = reqwest::Client::new().get(format!("{base}/api/events")).send().await?.error_for_status()?; - - // The subscription exists once the response has started; a ping recorded - // before that would be missed, so keep pinging until one shows up. - let pings = server.pings.clone(); - let pinger = tokio::spawn(async move { - for seq in 0.. { - pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, seq)).await; - tokio::time::sleep(Duration::from_millis(50)).await; - } - }); - - let mut buf = String::new(); - let event = tokio::time::timeout(TIMEOUT, async { - loop { - let chunk = resp.chunk().await?.ok_or_else(|| eyre::eyre!("stream ended"))?; - buf.push_str(std::str::from_utf8(&chunk)?); - for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) { - let ev: Value = serde_json::from_str(line.trim())?; - if ev["type"] == "ping_seen" { - return Ok::<_, eyre::Error>(ev); - } - } - } - }) - .await??; - pinger.abort(); - - assert_eq!(event["ping"]["src"], "2001:67c:10ec::5"); - assert_eq!(event["ping"]["dst"], "2001:db8:1a::1"); - assert_eq!(event["ping"]["interface"], "swissix0"); - assert!(event["new_burst"].is_boolean()); - Ok(()) -} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 722ce14..9ba77ba 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -9,7 +9,6 @@ bgp-rib.workspace = true bytes.workspace = true clap.workspace = true futures.workspace = true -libc.workspace = true eyre.workspace = true prometheus.workspace = true tokio.workspace = true @@ -20,6 +19,3 @@ thiserror.workspace = true [build-dependencies] protobuf-codegen.workspace = true - -[dev-dependencies] -tokio = { workspace = true, features = ["test-util"] } diff --git a/crates/server/proto/config.proto b/crates/server/proto/config.proto index 3f33577..4314e3b 100644 --- a/crates/server/proto/config.proto +++ b/crates/server/proto/config.proto @@ -38,21 +38,6 @@ message BgpConfig { // instead of being refused until the hold timer expires. bool collision_detect_established_state = 8; - // Watch for pings (ICMPv6 echo requests) sent to the lab prefixes and show - // them on the status page. Unset disables it. - PingMonitor ping_monitor = 9; -} - -// Shows who is pinging addresses that this host answers for. -// -// The host must already own the addresses, e.g. through an AnyIP route -// ("ip -6 route add local 2001:db8::/40 dev lo"); the kernel keeps answering -// the pings and we only receive a copy. Opening the raw ICMPv6 socket this -// needs takes CAP_NET_RAW on Linux. IPv6 only. -message PingMonitor { - // Only pings to destinations inside these prefixes are shown. Defaults to - // the IPv6 prefixes in local_route_policy.allowed_prefixes. - repeated string prefixes = 1; } // Limits on locally originated routes. diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index 5671fa6..e8bcfbe 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -2,7 +2,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::time::Duration; -use bgp_primitives::constants::AddressFamilyId; use bgp_primitives::ip_prefix::IpPrefix; use bgp_rib::LocalRibPolicy; use eyre::{Result, WrapErr, bail, eyre}; @@ -32,9 +31,6 @@ pub struct Config { pub local_route_policy: LocalRibPolicy, /// Address for the HTTP API; `None` disables it. pub http_endpoint: Option, - /// IPv6 prefixes whose incoming pings are shown; `None` disables the - /// ping monitor. - pub ping_monitor: Option>, } #[derive(Debug, Clone)] @@ -131,27 +127,6 @@ impl Config { None => None, }; - let ping_monitor = match proto.ping_monitor.into_option() { - Some(monitor) => { - let prefixes: Vec = if monitor.prefixes.is_empty() { - local_route_policy.allowed_prefixes.clone() - } else { - monitor - .prefixes - .iter() - .map(|p| IpPrefix::try_from(p.as_str()).wrap_err_with(|| format!("Invalid ping_monitor prefix {p:?}"))) - .collect::>()? - }; - let prefixes: Vec = - prefixes.into_iter().filter(|p| p.address_family == AddressFamilyId::Ipv6).collect(); - if prefixes.is_empty() { - bail!("ping_monitor needs IPv6 prefixes, either its own or local_route_policy.allowed_prefixes"); - } - Some(prefixes) - } - None => None, - }; - let connect_retry = if proto.connect_retry_seconds == 0 { DEFAULT_CONNECT_RETRY } else { @@ -168,7 +143,6 @@ impl Config { collision_detect_established_state: proto.collision_detect_established_state, local_route_policy, http_endpoint, - ping_monitor, }) } } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index d97fc15..3dd9ee0 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -5,7 +5,6 @@ pub mod connection; pub mod dialer; pub mod fsm; pub mod metrics; -pub mod ping; pub mod server; pub mod session; pub mod tracing; diff --git a/crates/server/src/metrics.rs b/crates/server/src/metrics.rs index 233bb26..37ce935 100644 --- a/crates/server/src/metrics.rs +++ b/crates/server/src/metrics.rs @@ -46,22 +46,6 @@ pub static ADJ_RIB_IN_ROUTES: LazyLock = LazyLock::new(|| { .expect("metric registration") }); -pub static PING_ECHO_REQUESTS: LazyLock = LazyLock::new(|| { - register_int_counter!( - "ping_monitor_echo_requests_total", - "ICMPv6 echo requests seen for the watched prefixes" - ) - .expect("metric registration") -}); - -pub static PING_EVENTS_SUPPRESSED: LazyLock = LazyLock::new(|| { - register_int_counter!( - "ping_monitor_events_suppressed_total", - "Ping events not published because of the per-second limit" - ) - .expect("metric registration") -}); - /// Forces registration of every metric so they appear in scrapes before /// their first observation. pub fn init() { @@ -72,8 +56,6 @@ pub fn init() { LazyLock::force(&COLLISIONS); LazyLock::force(&LOCAL_ROUTES); LazyLock::force(&ADJ_RIB_IN_ROUTES); - LazyLock::force(&PING_ECHO_REQUESTS); - LazyLock::force(&PING_EVENTS_SUPPRESSED); } pub fn message_type_name(msg: &Message) -> &'static str { diff --git a/crates/server/src/ping.rs b/crates/server/src/ping.rs deleted file mode 100644 index 54219de..0000000 --- a/crates/server/src/ping.rs +++ /dev/null @@ -1,412 +0,0 @@ -//! Shows who is pinging the prefixes this router originates. -//! -//! The host answers for the lab address space itself (an AnyIP route on the -//! loopback interface), so the kernel already replies to every ping. A raw -//! ICMPv6 socket gets a copy of each echo request as well; we never reply and -//! never see forwarded traffic. For every packet we learn the source, the -//! destination, the interface it arrived on and the hop limit it arrived -//! with, which is enough to show a student that their route is in use and -//! which way the traffic came in. -//! -//! Anyone on the Internet can send these packets, with any source address, so -//! everything here is bounded: a fixed number of flows is remembered and the -//! number of events published per second is capped. - -#[cfg(unix)] -mod sys; - -use std::collections::HashMap; -use std::net::Ipv6Addr; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use bgp_primitives::constants::AddressFamilyId; -use bgp_primitives::ip_prefix::IpPrefix; -use bgp_rib::{LocalRib, LocalRoute}; -use tokio::sync::{Mutex, broadcast}; -use tokio::time::Instant; -use tracing::{error, info, warn}; - -use crate::metrics::{PING_ECHO_REQUESTS, PING_EVENTS_SUPPRESSED}; - -/// Flows (source and destination pairs) remembered. When full, the flow that -/// has been quiet the longest makes room. -const MAX_FLOWS: usize = 2048; -/// A flow that keeps pinging publishes an event this often, carrying the -/// running count. -const EVENT_INTERVAL: Duration = Duration::from_secs(2); -/// A flow that was quiet for this long starts a new burst, which is what the -/// event log reports. -const BURST_GAP: Duration = Duration::from_secs(10); -/// How much of each packet is kept for display. -pub const MAX_PACKET_BYTES: usize = 256; -/// Events published per second across all flows. Packets beyond this are -/// still counted and show up in [`PingMonitor::flows`]. -const MAX_EVENTS_PER_SECOND: u32 = 50; - -/// One echo request, as seen by the socket. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PingObservation { - pub src: Ipv6Addr, - pub dst: Ipv6Addr, - /// Name of the interface the packet arrived on. - pub interface: Option, - /// Hop limit left when the packet arrived. Senders start at 64, 128 or - /// 255, so this tells roughly how many routers the packet crossed. - pub hop_limit: Option, - pub ident: u16, - pub seq: u16, - /// Size of the ICMPv6 message, header included. - pub bytes: usize, - /// The ICMPv6 message as received, from its type byte on, cut off after - /// [`MAX_PACKET_BYTES`]. The IPv6 header is not part of it: a raw ICMPv6 - /// socket never sees one, which is why the fields above come separately. - pub packet: Vec, -} - -/// The local route a ping was addressed to. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PingedRoute { - pub prefix: IpPrefix, - pub owner: String, -} - -/// All pings from one source to one destination. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PingFlow { - /// The most recent packet. - pub last: PingObservation, - pub count: u64, - pub first_seen: SystemTime, - pub last_seen: SystemTime, - /// The most specific local route covering the destination right now; - /// `None` when nobody announces it. - pub route: Option, -} - -#[derive(Debug, Clone)] -pub struct PingEvent { - pub flow: PingFlow, - /// True for the first event of a burst: a new flow, or one that had gone - /// quiet. False for the periodic updates while it keeps pinging. - pub new_burst: bool, -} - -/// Whether the monitor is watching, for display. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PingMonitorStatus { - /// Not configured. - Disabled, - Listening, - /// Configured, but the socket could not be opened. - Failed(String), -} - -struct FlowState { - flow: PingFlow, - last_packet: Instant, - last_event: Option, - /// A burst started and its first event has not gone out yet. - burst_pending: bool, -} - -#[derive(Default)] -struct Table { - flows: HashMap<(Ipv6Addr, Ipv6Addr), FlowState>, - window_start: Option, - window_events: u32, -} - -impl Table { - fn take_event_slot(&mut self, now: Instant) -> bool { - if self.window_start.is_none_or(|start| now.duration_since(start) >= Duration::from_secs(1)) { - self.window_start = Some(now); - self.window_events = 0; - } - if self.window_events >= MAX_EVENTS_PER_SECOND { - return false; - } - self.window_events += 1; - true - } - - fn make_room(&mut self) { - if self.flows.len() < MAX_FLOWS { - return; - } - if let Some(key) = self.flows.iter().min_by_key(|(_, s)| s.last_packet).map(|(k, _)| *k) { - self.flows.remove(&key); - } - } -} - -struct Inner { - prefixes: Vec, - local_rib: Option, - status: PingMonitorStatus, - table: Mutex, - events: broadcast::Sender, -} - -/// Handle to the ping monitor. Cheap to clone. -#[derive(Clone)] -pub struct PingMonitor { - inner: Arc, -} - -impl PingMonitor { - fn new(prefixes: Vec, local_rib: Option, status: PingMonitorStatus) -> Self { - let (events, _) = broadcast::channel(256); - Self { inner: Arc::new(Inner { prefixes, local_rib, status, table: Mutex::default(), events }) } - } - - /// A monitor that watches nothing. - pub fn disabled() -> Self { - Self::new(vec![], None, PingMonitorStatus::Disabled) - } - - /// A monitor without a socket, fed through [`PingMonitor::record`]. - pub fn without_socket(prefixes: Vec, local_rib: Option) -> Self { - Self::new(prefixes, local_rib, PingMonitorStatus::Listening) - } - - /// Opens the socket and starts watching for pings to `prefixes`. Failing - /// to open it (usually: no CAP_NET_RAW) is not fatal; the daemon runs on - /// without the monitor and the status page says why. - pub fn start(prefixes: Vec, local_rib: LocalRib) -> Self { - #[cfg(unix)] - let socket = sys::EchoSocket::open(); - #[cfg(not(unix))] - let socket: std::io::Result<()> = Err(std::io::Error::other("not supported on this platform")); - - match socket { - Ok(socket) => { - let shown: Vec = prefixes.iter().map(ToString::to_string).collect(); - info!(prefixes = ?shown, "Ping monitor listening for ICMPv6 echo requests"); - let monitor = Self::new(prefixes, Some(local_rib), PingMonitorStatus::Listening); - #[cfg(unix)] - tokio::spawn(monitor.clone().run(socket)); - monitor - } - Err(e) => { - warn!(err = %e, "Ping monitor disabled: cannot open a raw ICMPv6 socket (needs CAP_NET_RAW)"); - Self::new(prefixes, None, PingMonitorStatus::Failed(e.to_string())) - } - } - } - - #[cfg(unix)] - async fn run(self, mut socket: sys::EchoSocket) { - loop { - match socket.recv().await { - Ok(observation) => self.record(observation).await, - Err(e) => { - error!(err = %e, "Ping monitor socket failed"); - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - } - } - - pub fn status(&self) -> &PingMonitorStatus { - &self.inner.status - } - - /// The prefixes being watched. - pub fn prefixes(&self) -> &[IpPrefix] { - &self.inner.prefixes - } - - pub fn subscribe(&self) -> broadcast::Receiver { - self.inner.events.subscribe() - } - - /// Accounts for one echo request. Packets to destinations outside the - /// watched prefixes are ignored. - pub async fn record(&self, observation: PingObservation) { - let destination = host_prefix(observation.dst); - if !self.inner.prefixes.iter().any(|p| p.contains(&destination)) { - return; - } - PING_ECHO_REQUESTS.inc(); - - let now = Instant::now(); - let wall = SystemTime::now(); - let key = (observation.src, observation.dst); - let mut guard = self.inner.table.lock().await; - let table = &mut *guard; - - let is_new = !table.flows.contains_key(&key); - if is_new { - table.make_room(); - } - let state = table.flows.entry(key).or_insert_with(|| FlowState { - flow: PingFlow { last: observation.clone(), count: 0, first_seen: wall, last_seen: wall, route: None }, - last_packet: now, - last_event: None, - burst_pending: false, - }); - if is_new || now.duration_since(state.last_packet) >= BURST_GAP { - state.burst_pending = true; - } - state.flow.last = observation; - state.flow.count += 1; - state.flow.last_seen = wall; - state.last_packet = now; - - let due = state.burst_pending || state.last_event.is_none_or(|t| now.duration_since(t) >= EVENT_INTERVAL); - if !due { - return; - } - let new_burst = state.burst_pending; - let mut flow = state.flow.clone(); - if !table.take_event_slot(now) { - PING_EVENTS_SUPPRESSED.inc(); - return; - } - if let Some(state) = table.flows.get_mut(&key) { - state.last_event = Some(now); - state.burst_pending = false; - } - drop(guard); - - flow.route = resolve(&self.local_routes().await, &destination); - // No subscribers is fine. - let _ = self.inner.events.send(PingEvent { flow, new_burst }); - } - - /// Every remembered flow, most recently seen first. - pub async fn flows(&self) -> Vec { - let mut flows: Vec = self.inner.table.lock().await.flows.values().map(|s| s.flow.clone()).collect(); - let routes = self.local_routes().await; - for flow in &mut flows { - flow.route = resolve(&routes, &host_prefix(flow.last.dst)); - } - flows.sort_by(|a, b| b.last_seen.cmp(&a.last_seen).then_with(|| a.last.dst.cmp(&b.last.dst))); - flows - } - - async fn local_routes(&self) -> Vec { - match &self.inner.local_rib { - Some(rib) => rib.snapshot().await, - None => vec![], - } - } -} - -fn host_prefix(addr: Ipv6Addr) -> IpPrefix { - IpPrefix { address_family: AddressFamilyId::Ipv6, prefix: addr.octets().to_vec(), length: 128 } -} - -fn resolve(routes: &[LocalRoute], destination: &IpPrefix) -> Option { - routes - .iter() - .filter(|r| r.prefix.contains(destination)) - .max_by_key(|r| r.prefix.length) - .map(|r| PingedRoute { prefix: r.prefix.clone(), owner: r.owner.clone() }) -} - -#[cfg(test)] -mod tests { - use super::*; - use bgp_rib::{LocalRibPolicy, LocalRouteAttrs}; - - fn lab() -> Vec { - vec![IpPrefix::try_from("2a0d:d741::/40").unwrap()] - } - - fn ping(src: &str, dst: &str, seq: u16) -> PingObservation { - PingObservation { - src: src.parse().unwrap(), - dst: dst.parse().unwrap(), - interface: Some("swissix0".into()), - hop_limit: Some(57), - ident: 7, - seq, - bytes: 64, - packet: vec![128, 0, 0, 0, 0, 7, (seq >> 8) as u8, seq as u8], - } - } - - #[tokio::test(start_paused = true)] - async fn counts_flows_and_names_the_route_owner() { - let rib = LocalRib::spawn(LocalRibPolicy::default()); - let prefix = IpPrefix::try_from("2a0d:d741:1a::/48").unwrap(); - rib.announce(LocalRoute::new(prefix.clone(), "alice", LocalRouteAttrs::default())).await.unwrap(); - let monitor = PingMonitor::without_socket(lab(), Some(rib.clone())); - let mut events = monitor.subscribe(); - - monitor.record(ping("2001:67c:10ec::5", "2a0d:d741:1a::1", 0)).await; - monitor.record(ping("2001:67c:10ec::5", "2a0d:d741:1a::1", 1)).await; - monitor.record(ping("2001:67c:10ec::5", "2a0d:d741:5::1", 0)).await; - - let first = events.try_recv().unwrap(); - assert!(first.new_burst); - assert_eq!(first.flow.route, Some(PingedRoute { prefix, owner: "alice".into() })); - let other = events.try_recv().unwrap(); - assert_eq!(other.flow.route, None, "nobody announces 2a0d:d741:5::/48"); - assert!(events.try_recv().is_err(), "the second packet of a flow is not an event of its own"); - - let flows = monitor.flows().await; - assert_eq!(flows.len(), 2); - let alice = flows.iter().find(|f| f.route.is_some()).unwrap(); - assert_eq!((alice.count, alice.last.seq), (2, 1)); - - // The owner shown follows the routing table, not the first packet. - rib.withdraw(alice.route.as_ref().unwrap().prefix.clone(), None).await.unwrap(); - assert!(monitor.flows().await.iter().all(|f| f.route.is_none())); - } - - #[tokio::test(start_paused = true)] - async fn ignores_destinations_outside_the_watched_prefixes() { - let monitor = PingMonitor::without_socket(lab(), None); - monitor.record(ping("2001:db8::1", "2a0d:d740::1", 0)).await; - monitor.record(ping("2001:db8::1", "2001:7f8:24::c9", 0)).await; - assert!(monitor.flows().await.is_empty()); - } - - #[tokio::test(start_paused = true)] - async fn a_steady_ping_is_an_occasional_update_and_a_pause_starts_a_new_burst() { - let monitor = PingMonitor::without_socket(lab(), None); - let mut events = monitor.subscribe(); - let mut seen = vec![]; - for seq in 0..6 { - monitor.record(ping("2001:db8::1", "2a0d:d741:1::1", seq)).await; - while let Ok(ev) = events.try_recv() { - seen.push((ev.flow.count, ev.new_burst)); - } - tokio::time::advance(Duration::from_secs(1)).await; - } - assert_eq!(seen, vec![(1, true), (3, false), (5, false)]); - - tokio::time::advance(BURST_GAP).await; - monitor.record(ping("2001:db8::1", "2a0d:d741:1::1", 6)).await; - let ev = events.try_recv().unwrap(); - assert_eq!((ev.flow.count, ev.new_burst), (7, true)); - } - - #[tokio::test(start_paused = true)] - async fn a_flood_of_spoofed_sources_is_bounded() { - let monitor = PingMonitor::without_socket(lab(), None); - let mut events = monitor.subscribe(); - for n in 0..(MAX_FLOWS as u32 + 500) { - let src = Ipv6Addr::from(0x2001_0db8_0000_0000_0000_0000_0000_0000u128 + n as u128); - monitor.record(ping(&src.to_string(), "2a0d:d741:1::1", 0)).await; - } - assert_eq!(monitor.flows().await.len(), MAX_FLOWS); - let mut published = 0; - while events.try_recv().is_ok() { - published += 1; - } - assert_eq!(published, MAX_EVENTS_PER_SECOND, "time stood still, so this was all one second"); - - // A real student still gets through once the flood's second is over, - // even though the first attempt at announcing the burst was dropped. - monitor.record(ping("2001:67c:10ec::5", "2a0d:d741:1::1", 0)).await; - assert!(events.try_recv().is_err()); - tokio::time::advance(Duration::from_secs(1)).await; - monitor.record(ping("2001:67c:10ec::5", "2a0d:d741:1::1", 1)).await; - let ev = events.try_recv().unwrap(); - assert_eq!((ev.flow.count, ev.new_burst), (2, true)); - } -} diff --git a/crates/server/src/ping/sys.rs b/crates/server/src/ping/sys.rs deleted file mode 100644 index 683c50b..0000000 --- a/crates/server/src/ping/sys.rs +++ /dev/null @@ -1,265 +0,0 @@ -//! The raw ICMPv6 socket behind the ping monitor. -//! -//! All the `unsafe` in the daemon lives here: the socket options and the -//! ancillary data we need (`ICMP6_FILTER`, `IPV6_PKTINFO`, `IPV6_HOPLIMIT`) -//! have no safe wrapper in the standard library or tokio. - -use std::collections::HashMap; -use std::ffi::CStr; -use std::io; -use std::mem::{size_of, size_of_val, zeroed}; -use std::net::Ipv6Addr; -use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; - -use libc::{c_int, c_void}; -use tokio::io::Interest; -use tokio::io::unix::AsyncFd; - -use super::{MAX_PACKET_BYTES, PingObservation}; - -const ICMP6_ECHO_REQUEST: u8 = 128; - -#[cfg(any(target_os = "linux", target_os = "android"))] -const ICMP6_FILTER: c_int = 1; -#[cfg(not(any(target_os = "linux", target_os = "android")))] -const ICMP6_FILTER: c_int = 18; - -/// `struct icmp6_filter`: one bit per ICMPv6 type. -type Icmp6Filter = [u32; 8]; - -/// A filter that lets only echo requests through. Linux and the BSDs agree on -/// the layout and disagree on the meaning: on Linux a set bit blocks the -/// type, on the BSDs (and macOS) a set bit passes it. -fn echo_request_only() -> Icmp6Filter { - let (word, bit) = ((ICMP6_ECHO_REQUEST >> 5) as usize, 1u32 << (ICMP6_ECHO_REQUEST & 31)); - if cfg!(any(target_os = "linux", target_os = "android")) { - let mut filter = [u32::MAX; 8]; - filter[word] &= !bit; - filter - } else { - let mut filter = [0u32; 8]; - filter[word] |= bit; - filter - } -} - -fn check(ret: c_int) -> io::Result { - if ret < 0 { Err(io::Error::last_os_error()) } else { Ok(ret) } -} - -fn setsockopt(fd: RawFd, level: c_int, name: c_int, value: &T) -> io::Result<()> { - // SAFETY: `value` is a live reference and the length given is its size. - check(unsafe { - libc::setsockopt(fd, level, name, (value as *const T).cast::(), size_of::() as libc::socklen_t) - })?; - Ok(()) -} - -/// Receives a copy of every ICMPv6 echo request the kernel delivers to this -/// host. The kernel still answers them; we never send anything. -pub struct EchoSocket { - fd: AsyncFd, - /// Interface names by index. Looked up once: interfaces on a router do - /// not change name, and under a ping flood the lookup would be three - /// system calls per packet. - interfaces: HashMap>, -} - -impl EchoSocket { - /// Opens the socket. Needs CAP_NET_RAW on Linux. macOS also hands echo - /// requests to unprivileged ICMPv6 datagram sockets, which is enough to - /// try the monitor out on a laptop. (On Linux those only ever see the - /// replies to their own pings, so there is no such fallback there.) - pub fn open() -> io::Result { - Self::open_as(libc::SOCK_RAW).or_else(|e| { - if cfg!(target_os = "macos") { Self::open_as(libc::SOCK_DGRAM) } else { Err(e) } - }) - } - - fn open_as(socket_type: c_int) -> io::Result { - // SAFETY: plain system call; the result is checked before use. - let raw = check(unsafe { libc::socket(libc::AF_INET6, socket_type, libc::IPPROTO_ICMPV6) })?; - // SAFETY: `raw` is a descriptor we just opened and nobody else owns. - let fd = unsafe { OwnedFd::from_raw_fd(raw) }; - - // SAFETY: fcntl on a descriptor we own, with integer arguments. - unsafe { - let flags = check(libc::fcntl(raw, libc::F_GETFL))?; - check(libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK))?; - check(libc::fcntl(raw, libc::F_SETFD, libc::FD_CLOEXEC))?; - } - - setsockopt(raw, libc::IPPROTO_ICMPV6, ICMP6_FILTER, &echo_request_only())?; - let on: c_int = 1; - setsockopt(raw, libc::IPPROTO_IPV6, libc::IPV6_RECVPKTINFO, &on)?; - setsockopt(raw, libc::IPPROTO_IPV6, libc::IPV6_RECVHOPLIMIT, &on)?; - - Ok(Self { fd: AsyncFd::with_interest(fd, Interest::READABLE)?, interfaces: HashMap::new() }) - } - - /// Waits for the next echo request. - pub async fn recv(&mut self) -> io::Result { - loop { - let mut guard = self.fd.readable().await?; - let packet = match guard.try_io(|fd| recv_packet(fd.as_raw_fd())) { - Ok(result) => result?, - Err(_would_block) => continue, - }; - let Some(packet) = packet else { continue }; - let interface = self.interfaces.entry(packet.ifindex).or_insert_with(|| interface_name(packet.ifindex)).clone(); - return Ok(PingObservation { - src: packet.src, - dst: packet.dst, - interface, - hop_limit: packet.hop_limit, - ident: packet.ident, - seq: packet.seq, - bytes: packet.bytes, - packet: packet.data, - }); - } - } -} - -struct Packet { - src: Ipv6Addr, - dst: Ipv6Addr, - ifindex: u32, - hop_limit: Option, - ident: u16, - seq: u16, - bytes: usize, - data: Vec, -} - -/// Reads one packet. `Ok(None)` is a packet we cannot use (not an echo -/// request, or the kernel did not say where it was going). -fn recv_packet(fd: RawFd) -> io::Result> { - // A raw ICMPv6 socket delivers the ICMPv6 message without the IPv6 - // header, so everything about the header comes from ancillary data. - let mut data = [0u8; 2048]; - let mut control = [0u64; 16]; // u64 for the alignment cmsghdr needs - // SAFETY: all-zero is a valid value for these plain C structs. - let mut from: libc::sockaddr_in6 = unsafe { zeroed() }; - let mut iov = libc::iovec { iov_base: data.as_mut_ptr().cast(), iov_len: data.len() }; - // SAFETY: as above. msghdr has private padding on some targets, so it - // cannot be built with a struct literal. - let mut msg: libc::msghdr = unsafe { zeroed() }; - msg.msg_name = (&mut from as *mut libc::sockaddr_in6).cast(); - msg.msg_namelen = size_of::() as _; - msg.msg_iov = &mut iov; - msg.msg_iovlen = 1 as _; - msg.msg_control = control.as_mut_ptr().cast(); - msg.msg_controllen = size_of_val(&control) as _; - - // SAFETY: every pointer in `msg` refers to a local that outlives the call - // and the lengths given match the buffers. - let n = unsafe { libc::recvmsg(fd, &mut msg, 0) }; - if n < 0 { - return Err(io::Error::last_os_error()); - } - let n = n as usize; - if n < 8 || data[0] != ICMP6_ECHO_REQUEST { - return Ok(None); - } - - let mut dst = None; - let mut hop_limit = None; - // SAFETY: the CMSG_* macros walk the control buffer the kernel just - // filled in, bounded by msg_controllen. Payloads are read unaligned. - unsafe { - let mut cmsg = libc::CMSG_FIRSTHDR(&msg); - while !cmsg.is_null() { - let header = &*cmsg; - if header.cmsg_level == libc::IPPROTO_IPV6 { - if header.cmsg_type == libc::IPV6_PKTINFO { - let info: libc::in6_pktinfo = std::ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast()); - dst = Some((Ipv6Addr::from(info.ipi6_addr.s6_addr), info.ipi6_ifindex as u32)); - } else if header.cmsg_type == libc::IPV6_HOPLIMIT { - let value: c_int = std::ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast()); - hop_limit = u8::try_from(value).ok(); - } - } - cmsg = libc::CMSG_NXTHDR(&msg, cmsg); - } - } - let Some((dst, ifindex)) = dst else { return Ok(None) }; - - Ok(Some(Packet { - src: Ipv6Addr::from(from.sin6_addr.s6_addr), - dst, - ifindex, - hop_limit, - ident: u16::from_be_bytes([data[4], data[5]]), - seq: u16::from_be_bytes([data[6], data[7]]), - bytes: n, - data: data[..n.min(MAX_PACKET_BYTES)].to_vec(), - })) -} - -fn interface_name(index: u32) -> Option { - let mut name = [0 as libc::c_char; libc::IF_NAMESIZE]; - // SAFETY: the buffer is IF_NAMESIZE bytes, as if_indextoname requires, - // and on success holds a NUL-terminated string. - unsafe { - if libc::if_indextoname(index, name.as_mut_ptr()).is_null() { - return None; - } - Some(CStr::from_ptr(name.as_ptr()).to_string_lossy().into_owned()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn filter_passes_only_echo_requests() { - let filter = echo_request_only(); - let blocked = |t: u8| { - let set = filter[(t >> 5) as usize] & (1 << (t & 31)) != 0; - if cfg!(any(target_os = "linux", target_os = "android")) { set } else { !set } - }; - assert!(!blocked(128), "echo request passes"); - for t in [1u8, 3, 129, 133, 134, 135, 136] { - assert!(blocked(t), "type {t} is dropped in the kernel"); - } - } - - /// Pings ::1 and expects to see the request. Skipped where the socket - /// cannot be opened, i.e. on Linux without CAP_NET_RAW. - #[tokio::test] - async fn sees_a_ping_to_loopback() { - let mut socket = match EchoSocket::open() { - Ok(socket) => socket, - Err(e) => return eprintln!("skipped: cannot open an ICMPv6 socket: {e}"), - }; - let ping = if cfg!(target_os = "macos") { "ping6" } else { "ping" }; - let Ok(mut child) = std::process::Command::new(ping) - .args(["-c", "1", "::1"]) - .stdout(std::process::Stdio::null()) - .spawn() - else { - return eprintln!("skipped: no {ping} command"); - }; - - let seen = tokio::time::timeout(std::time::Duration::from_secs(5), async { - loop { - let ping = socket.recv().await.unwrap(); - if ping.dst == Ipv6Addr::LOCALHOST { - return ping; - } - } - }) - .await; - let _ = child.wait(); - let seen = seen.expect("echo request to ::1 was not seen"); - assert_eq!(seen.src, Ipv6Addr::LOCALHOST); - assert_eq!(seen.hop_limit, Some(64)); - assert!(seen.seq <= 1, "first sequence number is 0 (BSD ping6) or 1 (iputils)"); - assert!(seen.interface.as_deref().is_some_and(|name| name.starts_with("lo")), "{:?}", seen.interface); - assert!(seen.bytes >= 8); - assert_eq!(seen.packet[..2], [128, 0], "echo request, code 0"); - assert_eq!(seen.packet[4..8], [seen.ident.to_be_bytes(), seen.seq.to_be_bytes()].concat()[..]); - } -} diff --git a/crates/web/src/api.rs b/crates/web/src/api.rs index 6163040..ad32d94 100644 --- a/crates/web/src/api.rs +++ b/crates/web/src/api.rs @@ -11,7 +11,6 @@ use bgp_primitives::ip_prefix::IpPrefix; use bgp_primitives::parser::ParserContext; use bgp_rib::{LocalRibError, LocalRoute, LocalRouteAttrs, SnapshotEntry}; use bgp_server::advertise::build_announce; -use bgp_server::ping::{PingFlow, PingMonitorStatus}; use bgp_server::session::{ConfiguredPeer, DialState, SessionHandle, SessionRegistry}; use bytes::BytesMut; use serde::{Deserialize, Serialize}; @@ -476,112 +475,3 @@ pub async fn received_routes(State(state): State) -> Json, - pub owner: Option, - /// Interface the most recent packet arrived on. - pub interface: Option, - /// Hop limit left when the most recent packet arrived. - pub hop_limit: Option, - /// Routers the packet crossed, assuming the sender started at the usual - /// 64, 128 or 255. - pub hops: Option, - pub ident: u16, - pub seq: u16, - /// Size of the most recent ICMPv6 message. - pub bytes: usize, - /// That message as lowercase hex, from the ICMPv6 type byte on (no IPv6 - /// header). Long packets are cut off, so this can be shorter than `bytes`. - pub hex: String, - pub count: u64, - pub first_seen_unix: u64, - pub last_seen_unix: u64, -} - -fn estimated_hops(hop_limit: u8) -> u8 { - let initial = [64u8, 128, 255].into_iter().find(|&start| start >= hop_limit).unwrap_or(255); - initial - hop_limit -} - -impl From<&PingFlow> for PingView { - fn from(flow: &PingFlow) -> Self { - Self { - src: flow.last.src.to_string(), - dst: flow.last.dst.to_string(), - prefix: flow.route.as_ref().map(|r| r.prefix.to_string()), - owner: flow.route.as_ref().map(|r| r.owner.clone()), - interface: flow.last.interface.clone(), - hop_limit: flow.last.hop_limit, - hops: flow.last.hop_limit.map(estimated_hops), - ident: flow.last.ident, - seq: flow.last.seq, - bytes: flow.last.bytes, - hex: flow.last.packet.iter().map(|b| format!("{b:02x}")).collect(), - count: flow.count, - first_seen_unix: unix_secs(flow.first_seen), - last_seen_unix: unix_secs(flow.last_seen), - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct PingMonitorView { - /// `listening`, `disabled` (not configured) or `failed`. - pub status: &'static str, - /// Why the monitor failed to start. - pub detail: Option, - /// Pings to destinations inside these prefixes are shown. - pub prefixes: Vec, - /// Flows remembered; `flows` holds the most recently seen of them. - pub total_flows: usize, - pub flows: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct PingQuery { - pub limit: Option, -} - -pub async fn pings(State(state): State, Query(query): Query) -> Json { - let (status, detail) = match state.pings.status() { - PingMonitorStatus::Listening => ("listening", None), - PingMonitorStatus::Disabled => ("disabled", None), - PingMonitorStatus::Failed(why) => ("failed", Some(why.clone())), - }; - let flows = state.pings.flows().await; - Json(PingMonitorView { - status, - detail, - prefixes: state.pings.prefixes().iter().map(ToString::to_string).collect(), - total_flows: flows.len(), - flows: flows.iter().take(query.limit.unwrap_or(DEFAULT_PING_FLOWS)).map(PingView::from).collect(), - }) -} - -#[cfg(test)] -mod ping_tests { - use super::estimated_hops; - - #[test] - fn hops_are_counted_from_the_nearest_usual_starting_value() { - assert_eq!(estimated_hops(64), 0); - assert_eq!(estimated_hops(57), 7); - assert_eq!(estimated_hops(118), 10); - assert_eq!(estimated_hops(243), 12); - assert_eq!(estimated_hops(255), 0); - } -} diff --git a/crates/web/src/events.rs b/crates/web/src/events.rs index 0732e59..6c20676 100644 --- a/crates/web/src/events.rs +++ b/crates/web/src/events.rs @@ -1,12 +1,11 @@ -//! Server-sent event stream merging session, local-route, received-route and -//! ping events into one feed the UI can follow. +//! Server-sent event stream merging session, local-route and received-route +//! changes into one feed the UI can follow. use std::convert::Infallible; use axum::extract::State; use axum::response::sse::{Event, KeepAlive, Sse}; use bgp_rib::{LocalRibEvent, RibEvent}; -use bgp_server::ping::PingEvent; use bgp_server::session::SessionEvent; use futures::stream::{Stream, StreamExt}; use serde::Serialize; @@ -14,7 +13,7 @@ use tokio_stream::wrappers::BroadcastStream; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use crate::AppState; -use crate::api::{LocalRouteView, PeerView, PingView, ReceivedRouteView, summarize}; +use crate::api::{LocalRouteView, PeerView, ReceivedRouteView, summarize}; #[derive(Debug, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] @@ -27,10 +26,6 @@ pub enum WireEvent { LocalWithdrawn { prefix: String }, ReceivedAnnounced { route: ReceivedRouteView }, ReceivedWithdrawn { peer_addr: String, peer_asn: u32, prefix: String }, - /// Somebody is pinging an address in the watched prefixes. `new_burst` - /// marks the start of a run of pings; the events that follow while it - /// continues only update the count. - PingSeen { ping: PingView, new_burst: bool }, /// The client fell behind and missed `missed` events on `stream`; it /// should refetch the corresponding table. Lagged { stream: &'static str, missed: u64 }, @@ -54,13 +49,6 @@ impl WireEvent { } } - fn from_ping(ev: Result) -> Self { - match ev { - Ok(ev) => Self::PingSeen { ping: PingView::from(&ev.flow), new_burst: ev.new_burst }, - Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { stream: "pings", missed: n }, - } - } - fn from_received(ev: Result) -> Self { match ev { Ok(RibEvent::Announced { peer, prefix, entry }) => Self::ReceivedAnnounced { @@ -94,10 +82,7 @@ pub async fn stream( let local = BroadcastStream::new(state.local_rib.subscribe()).map(WireEvent::from_local); let received = BroadcastStream::new(state.adj_rib_in.subscribe()).map(WireEvent::from_received); - let pings = BroadcastStream::new(state.pings.subscribe()).map(WireEvent::from_ping); - - let routes = futures::stream::select(local, received); - let merged = futures::stream::select(futures::stream::select(sessions, pings), routes).map(|ev| { + let merged = futures::stream::select(sessions, futures::stream::select(local, received)).map(|ev| { Ok(Event::default() .event("bgp") .json_data(&ev) diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index 1cc3ac5..c4dec55 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -2,7 +2,7 @@ //! inspecting state, a server-sent event stream, and Prometheus metrics. //! //! Everything here is a thin layer over handles owned by the BGP server -//! ([`SessionRegistry`], [`LocalRib`], [`AdjRibIn`], [`PingMonitor`]); no state lives +//! ([`SessionRegistry`], [`LocalRib`], [`AdjRibIn`]); no routing state lives //! in this crate. mod api; @@ -19,15 +19,13 @@ use axum::http::header; use axum::response::{Html, IntoResponse, Redirect}; use axum::routing::{delete, get, post}; use bgp_rib::{AdjRibIn, LocalRib}; -use bgp_server::ping::PingMonitor; use bgp_server::session::SessionRegistry; use eyre::{Result, WrapErr}; use tokio::net::TcpListener; use tracing::info; pub use api::{ - AnnounceRequest, AttrSummary, ConfigView, LocalRouteView, PeerView, PingMonitorView, PingView, - ReceivedRouteView, StatusView, + AnnounceRequest, AttrSummary, ConfigView, LocalRouteView, PeerView, ReceivedRouteView, StatusView, WellKnownCommunity, WireView, WithdrawRequest, }; @@ -86,7 +84,6 @@ pub struct AppState { pub registry: SessionRegistry, pub local_rib: LocalRib, pub adj_rib_in: AdjRibIn, - pub pings: PingMonitor, } /// The status page, embedded so the binary is self-contained. @@ -109,7 +106,6 @@ pub fn router(state: AppState) -> Router { .route("/api/routes/local/withdraw", post(api::withdraw)) .route("/api/routes/local/wire", get(api::wire)) .route("/api/routes/received", get(api::received_routes)) - .route("/api/pings", get(api::pings)) .route("/api/communities/well-known", get(api::well_known_communities)) .route("/api/events", get(events::stream)) .route("/metricz", get(metrics::metricz)) diff --git a/docs/lab/HANDOUT.md b/docs/lab/HANDOUT.md deleted file mode 100644 index 229a22c..0000000 --- a/docs/lab/HANDOUT.md +++ /dev/null @@ -1,62 +0,0 @@ -# Announcing a prefix with BGP - -You have a BGP router. It speaks to two neighbouring autonomous systems, and -whatever you announce will travel around a loop and come back to you, so you -can see exactly what the network did with it. - -Open the router's status page (your instructor will give you the address, -usually `http://:8080/statusz`). Everything on the page is live. - -**Your prefix:** `10.100..0/24` where `N` is the number you were given. -Use your name as *owner*: only you can withdraw what you announce. - -## The topology - -``` - you ──announce──▶ AS65000 (ferrix) ──▶ AS65001 (r1) ──▶ AS65002 (r2) ──▶ back to AS65000 -``` - -Everything under **Received routes** came in from a neighbour. A yellow row -means it is one of the prefixes announced locally: it went around the loop. - -## Exercises - -1. **Plain announcement.** Announce your prefix with no options. Watch the - event log, then find your prefix in *Received routes*. What is its AS - path, and why is it that long? Which ASN appears first, and what does that - tell you about the direction the route travelled? - -2. **Read the bytes.** Click *bytes* on your route under *Local routes*. - Hover the coloured segments. Find the 16 bytes of marker, the length, and - the AS_PATH attribute. How many bytes does your ASN take up in the path, - and why? - -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. - -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. - -5. **Blackholing.** Announce with `BLACKHOLE`. What happens? When would an - operator want this? - -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.) - -## Things to notice - -- Your router never sent anyone else's routes onward. Only the two FRR - routers re-advertise. What is a router that re-advertises called, and what - would happen to the AS path if ferrix did it too? -- The AS path is BGP's loop prevention. Your prefix came back with your own - ASN in the path and the router accepted it anyway. What should it have - done, and why is this lab allowed to cheat? -- Communities are just 32-bit tags with no built-in meaning except a few - well-known ones. Everything r1 did with `65001:100` was policy someone - wrote. Whose job is it to document what a community means? diff --git a/docs/lab/README.md b/docs/lab/README.md deleted file mode 100644 index 6056a48..0000000 --- a/docs/lab/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# BGP teaching lab - -A small topology in which a prefix announced from the ferrix_bgp web UI is -carried around a loop of two routers and arrives back, so students can watch -what their path attributes and communities do. - -``` - announce re-advertise - ┌─────────┐ ──────▶ ┌────────┐ ──────▶ ┌────────┐ - │ ferrix │ │ r1 │ │ r2 │ - │ AS65000 │ │ AS65001│ │ AS65002│ - │ web UI │ ◀──────────────────────────────────────┘ - └─────────┘ comes back with AS path 65002 65001 65000 -``` - -- **ferrix** (AS65000, `10.200.0.1`) dials r1 and r2 and announces whatever - students enter on `http://localhost:8080/statusz`. It does not forward - routes it learns, so the only routes it sends are the students'. -- **r1** (AS65001, FRR) applies policy to routes from ferrix based on the - communities they carry, then passes them to r2. It originates - `192.0.2.0/24`. -- **r2** (AS65002, FRR) refuses routes directly from ferrix, so it only learns - student prefixes via r1, and re-advertises everything to ferrix. It - originates `198.51.100.0/24`. - -## Running it - -From the repository root: - -``` -docker compose -f docs/lab/docker-compose.yml up --build -``` - -Then open . Both peers should show -`Established` within a few seconds and the received table should already -list the two prefixes r1 and r2 originate. - -Useful on the routers: - -``` -docker exec r1 vtysh -c 'show bgp ipv4 unicast' -docker exec r2 vtysh -c 'show bgp ipv4 unicast 10.100.1.0/24' -docker exec r1 vtysh -c 'show bgp neighbors 10.200.0.1 advertised-routes' -``` - -## Policy on r1 - -| Students set | r1 does | Visible effect in ferrix | -|---------------------------|---------------------------------------------|-------------------------------------------------------| -| nothing | forwards to r2 | prefix returns, AS path `65002 65001 65000` | -| `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 | - -## Without containers - -`fake_peer` in `crates/integration-tests/examples` is enough for the core -loop on one machine: it echoes every prefix back with its own ASN prepended -and honours `NO_EXPORT`. See `bin/bgp/README.md`. - -## Caveats - -These container files were written without a container runtime to hand, so -they have not been executed. Things most likely to need a touch: - -- the FRR image tag, and FRR's route-map / community-list syntax if your - version differs from 10.x; -- `no bgp ebgp-requires-policy` is required on FRR 7.4+ or eBGP routes are - silently dropped; it is present in both configs; -- ferrix binds port 179 inside its container, which requires no special - capability there. If you run it on the host instead, either run as root, - grant `CAP_NET_BIND_SERVICE`, or change `listen_endpoints` and the FRR - neighbor port. -- Both ferrix and the FRR routers dial each other, so connection collisions - (RFC 4271 §6.8) are normal at start-up: one of the two TCP connections is - closed with a Cease / Connection Collision Resolution NOTIFICATION and the - other carries the session. ferrix has the lowest BGP Identifier here, so - the connection the FRR router opened is the one that survives; the - "opened by" column on the status page shows which side that was. -- ferrix does not check AS paths for its own ASN. That is what lets the - looped route be accepted; a production BGP speaker would reject it. diff --git a/docs/lab/docker-compose.yml b/docs/lab/docker-compose.yml deleted file mode 100644 index e0bdeb7..0000000 --- a/docs/lab/docker-compose.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Teaching lab: ferrix_bgp plus two FRR routers forming a loop, so a prefix -# announced from the web UI travels ferrix -> r1 -> r2 -> ferrix and shows up -# in the "received routes" table with the AS path it collected on the way. -# -# AS65000 ferrix 10.200.0.1 web UI on http://localhost:8080/statusz -# AS65001 r1 10.200.0.2 applies community-driven policy -# AS65002 r2 10.200.0.3 learns only via r1, re-advertises to ferrix -# -# Usage (from the repository root): -# docker compose -f docs/lab/docker-compose.yml up --build -# -# NOTE: this topology has not been run by the author of this file on the -# machine it was written on (no container runtime was available). Expect to -# adjust image tags and FRR config syntax for the FRR version you pull. -services: - ferrix: - build: - context: ../.. - container_name: ferrix - volumes: - - ./ferrix.textpb:/etc/ferrix/server.textpb:ro - ports: - - "8080:8080" - networks: - lab: - ipv4_address: 10.200.0.1 - - r1: - image: quay.io/frrouting/frr:10.2.1 - container_name: r1 - privileged: true - volumes: - - ./r1:/etc/frr:ro - networks: - lab: - ipv4_address: 10.200.0.2 - - r2: - image: quay.io/frrouting/frr:10.2.1 - container_name: r2 - privileged: true - volumes: - - ./r2:/etc/frr:ro - networks: - lab: - ipv4_address: 10.200.0.3 - -networks: - lab: - driver: bridge - ipam: - config: - - subnet: 10.200.0.0/24 diff --git a/docs/lab/ferrix.textpb b/docs/lab/ferrix.textpb deleted file mode 100644 index 755e9af..0000000 --- a/docs/lab/ferrix.textpb +++ /dev/null @@ -1,30 +0,0 @@ -router_id: "10.200.0.1" -local_asn: 65000 -listen_endpoints { address: "0.0.0.0" port: 179 } - -peers { - name: "r1" - endpoint { address: "10.200.0.2" port: 179 } - remote_asn: 65001 - hold_time_seconds: 30 -} -peers { - name: "r2" - endpoint { address: "10.200.0.3" port: 179 } - remote_asn: 65002 - hold_time_seconds: 30 -} -connect_retry_seconds: 5 - -# If r1 or r2 is restarted it will dial us again while we still believe the -# old session is up. Let that new connection replace the stale one straight -# away instead of refusing it until the hold timer (30 s here) expires. -collision_detect_established_state: true - -# Students may only announce out of 10.100.0.0/16; one /24 each is plenty. -local_route_policy { - allowed_prefixes: "10.100.0.0/16" - max_routes_per_owner: 4 -} - -http_endpoint { address: "0.0.0.0" port: 8080 } diff --git a/docs/lab/handout/.gitignore b/docs/lab/handout/.gitignore deleted file mode 100644 index f0f9532..0000000 --- a/docs/lab/handout/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.aux -*.log -*.out -*.fls -*.fdb_latexmk -*.synctex.gz -build.log -class.pdf diff --git a/docs/lab/handout/build-class.py b/docs/lab/handout/build-class.py deleted file mode 100755 index b4db1ee..0000000 --- a/docs/lab/handout/build-class.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""One exercise sheet per student, with their prefix filled in, in one PDF. - - python3 build-class.py 100 # students 1..100 -> class.pdf - python3 build-class.py 100 -o out.pdf - python3 build-class.py 5 12 -o few.pdf # students 5..12 - -Student n gets 2a0d:d741:::/48; the lab /40 allows 1..255. Each -sheet is compiled separately and must come out to exactly two pages, so -the result prints double-sided with one student per leaf. Needs pdflatex -and pdfunite (poppler). -""" -import argparse -import os -import shutil -import subprocess -import sys -import tempfile - -HERE = os.path.dirname(os.path.abspath(__file__)) - - -def page_count(pdf): - out = subprocess.run(["pdfinfo", pdf], capture_output=True, text=True, check=True).stdout - return int(next(line.split()[-1] for line in out.splitlines() if line.startswith("Pages:"))) - - -def main(): - ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) - ap.add_argument("first", type=int, help="first student number, or the count if it is the only number") - ap.add_argument("last", type=int, nargs="?") - ap.add_argument("-o", "--output", default=os.path.join(HERE, "class.pdf")) - args = ap.parse_args() - first, last = (1, args.first) if args.last is None else (args.first, args.last) - if not 1 <= first <= last <= 255: - sys.exit("student numbers must be within 1..255: the lab /40 has 255 usable /48s") - - with tempfile.TemporaryDirectory() as tmp: - for name in os.listdir(HERE): - if name.endswith((".tex", ".cls", ".sty", ".bst", ".bib")): - shutil.copy(os.path.join(HERE, name), tmp) - parts = [] - for n in range(first, last + 1): - job = f"student-{n:03d}" - tex = f"\\def\\studentnum{{{n}}}\\def\\studenthex{{{n:x}}}\\input{{handout.tex}}" - for _ in range(2): # twice, for the figure and table references - r = subprocess.run(["pdflatex", "-interaction=nonstopmode", "-halt-on-error", "-jobname", job, tex], - cwd=tmp, capture_output=True, text=True) - if r.returncode != 0: - sys.exit(f"student {n}: pdflatex failed:\n{r.stdout[-2000:]}") - pdf = os.path.join(tmp, job + ".pdf") - pages = page_count(pdf) - if pages != 2: - sys.exit(f"student {n}: sheet is {pages} pages, not 2; double-sided printing would misalign") - parts.append(pdf) - print(f"\rstudent {n}: 2a0d:d741:{n:x}::/48", end="", flush=True) - print() - subprocess.run(["pdfunite", *parts, args.output], check=True) - print(f"wrote {args.output}: students {first}..{last}, {page_count(args.output)} pages") - - -if __name__ == "__main__": - main() diff --git a/docs/lab/handout/handout.pdf b/docs/lab/handout/handout.pdf deleted file mode 100644 index 4c25825..0000000 Binary files a/docs/lab/handout/handout.pdf and /dev/null differ diff --git a/docs/lab/handout/handout.tex b/docs/lab/handout/handout.tex deleted file mode 100644 index 0d4db8f..0000000 --- a/docs/lab/handout/handout.tex +++ /dev/null @@ -1,195 +0,0 @@ -% Exercise sheet for the "Hands-on BGP" lecture: two A4 pages, one sheet. -% Springer LNCS class (llncs.cls ships with TeX Live). Build: latexmk -pdf handout.tex -\documentclass[a4paper]{llncs} - -\usepackage[a4paper,margin=1in]{geometry} -\usepackage[T1]{fontenc} -\usepackage[utf8]{inputenc} -\usepackage{microtype} -\usepackage{booktabs} -\usepackage{amssymb} -\usepackage{listings} -\usepackage{tikz} -\usepackage[bitstream-charter]{mathdesign} -\usetikzlibrary{positioning,arrows.meta} -\PassOptionsToPackage{hyphens}{url} -\usepackage[hidelinks]{hyperref} -\renewcommand\UrlFont{\ttfamily\small} -\pagestyle{empty} -% Floats sit in the text here, so they need less air than in a paper. -\setlength{\intextsep}{7pt} -\setlength{\abovecaptionskip}{4pt} -\setlength{\belowcaptionskip}{0pt} - -% ---- things to change per class ------------------------------------------- -\newcommand{\laburl}{https://bgplab.rayhaan.net/} -% build-class.py compiles one copy per student with \studentnum and -% \studenthex defined (e.g. 26 and 1a); without them this is the blank sheet. -\providecommand{\studentnum}{} -\providecommand{\studenthex}{} -\ifx\studentnum\empty - \newcommand{\myprefix}{\texttt{2a0d:d741:}\underline{\texttt{\,n\,}}\texttt{::/48}} - \newcommand{\nhex}{} -\else - \newcommand{\myprefix}{\texttt{2a0d:d741:\studenthex::/48}} - \newcommand{\nhex}{\studenthex} -\fi - -% ---- exercise machinery ------------------------------------------------------ -\lstset{basicstyle=\ttfamily\small,columns=fullflexible,keepspaces=true, - aboveskip=0.5em,belowskip=0.3em} -\newcounter{task}[section] -\renewcommand{\thetask}{\thesection.\arabic{task}} -\newcommand{\task}[1]{\refstepcounter{task}\smallskip\noindent - \textbf{Task \thetask}\enspace\textit{#1}\enspace\ignorespaces} -\newcommand{\answer}[1][1]{\par\nobreak - \foreach \i in {1,...,#1}{\vspace{1.0em}\noindent\makebox[\linewidth]{\dotfill}\par}\smallskip} -% A labelled answer box: the label on its own line, then #1 dotted lines to -% write on (default 2). Used side by side by \fillintwo and \fillinthree. -\newcommand{\writelines}[1]{\foreach \i in {1,...,#1}{\vspace{1.0em}\makebox[\linewidth]{\dotfill}\par}} -\newcommand{\fillbox}[3][2]{\parbox[t]{#2}{#3\par\nopagebreak\writelines{#1}}} -\newcommand{\fillintwo}[3][2]{\par\nopagebreak\smallskip\noindent - \fillbox[#1]{0.485\linewidth}{#2}\hfill\fillbox[#1]{0.485\linewidth}{#3}\par\vspace{0.4em}} -\newcommand{\fillinthree}[4][2]{\par\nopagebreak\smallskip\noindent - \fillbox[#1]{0.32\linewidth}{#2}\hfill\fillbox[#1]{0.32\linewidth}{#3}\hfill\fillbox[#1]{0.32\linewidth}{#4}\par\vspace{0.4em}} -\newcommand{\cbox}{$\square$\;} -\newcommand{\reach}{\par\nopagebreak\smallskip\noindent - Reachable from:\hspace{1.2em}\cbox laptop on the university network\hspace{2em}\cbox phone on mobile data\par\smallskip} - -\begin{document} - -\title{Hands on BGP: Announcing and Steering an IPv6 Prefix} -\author{Rayhaan Jaufeerally} -\institute{Network Security, ETHZ\\ Lab page: \url{\laburl} (username ``netsec'', password ``perrig'')} -\maketitle - -\medskip\noindent -\ifx\studentnum\empty -\parbox[t]{0.44\linewidth}{Name:~\dotfill}\hfill -\parbox[t]{0.27\linewidth}{Prefix: \myprefix} - -\smallskip\noindent -Your prefix is \myprefix, where \texttt{n} is your number \emph{in -hexadecimal}.\par\noindent -Student 10 has \texttt{2a0d:d741:a::/48} and student 26 has -\texttt{2a0d:d741:1a::/48}. -\else -\parbox[t]{0.44\linewidth}{Name:~\dotfill}\hfill -\parbox[t]{0.5\linewidth}{Student \studentnum\quad Prefix: \myprefix} - -\smallskip\noindent -Your prefix is \myprefix: student number \studentnum, written in -hexadecimal. Make sure to use this prefix in the exercise. -\fi - -% ============================================================================== -\section{Who Announces the Address You Are Using?} - -\task{Find your IPv6 address.} -Open \texttt{https://bgp.tools} in a browser: the top of the page shows the address -you are connecting from, the prefix it belongs to, and the network that -announces that prefix. -\fillinthree{Your IPv6 address:}{its prefix:}{announced by AS:} - -\task{Ask the registry.} -The RIPE database records who holds address space -(\texttt{inet6num}, or \texttt{inetnum} for IPv4) and who may announce it -(\texttt{route6}, \texttt{route}). Look up your IPv6 address, then your lab prefix: -\begin{lstlisting}[escapeinside={(*}{*)}] -whois whois 2a0d:d741:(*\nhex*)::/48 -\end{lstlisting} -\fillintwo{Your IPv6 address: held by (\texttt{netname})}{\texttt{origin} AS:} -\fillintwo{Your lab prefix: \texttt{origin} AS}{is it announced right now?} - -% ============================================================================== -\section{Announcing Your Prefix} - -\begin{figure}[h] -\centering -\begin{tikzpicture}[font=\footnotesize, node distance=5mm and 11mm, - box/.style={draw, rounded corners=1.5pt, minimum height=7.5mm, inner xsep=4pt, align=center}, - bgp/.style={-{Stealth[length=2mm]}, dashed}, web/.style={-{Stealth[length=2mm]}}] - \node[box] (you) {you\\[-1pt]\scriptsize browser}; - \node[box, right=of you] (ferrix) {lab daemon\\[-1pt]\scriptsize AS4200000001}; - \node[box, right=of ferrix] (pr) {\texttt{pr01.rue}\\[-1pt]\scriptsize AS210036}; - \node[box, right=of pr] (rs) {SwissIX route servers\\[-1pt]\scriptsize AS42476}; - \node[box, right=of rs] (sw) {SWITCH (ETH)\\[-1pt]\scriptsize AS559}; - \node[box, below=4mm of rs] (other) {other exchanges, transit}; - \draw[web] (you) -- node[above, inner sep=1.5pt]{\tiny HTTPS} (ferrix); - \draw[bgp] (ferrix) -- node[above, inner sep=1.5pt]{\tiny BGP} (pr); - \draw[bgp] (pr) -- (rs); - \draw[bgp] (rs) -- (sw); - \draw[bgp] (pr) |- (other); -\end{tikzpicture} -\caption{The lab. Dashed arrows are BGP sessions; everything right of -\texttt{pr01.rue} is the real Internet.} -\label{fig:lab} -\end{figure} - -\task{Announce.} -Open \url{\laburl} (username ``netsec'' password ``perrig''). Enter your name as -\emph{owner} and your prefix, and press \emph{Announce}. - -\task{Read what you sent.} -Click \emph{bytes} next to your route and hover over the coloured segments. -\fillintwo{AS number(s) in \texttt{AS\_PATH}:}{attribute carrying your prefix:} - -\task{See it from outside.} -Wait a moment, then look your prefix up on \texttt{https://bgp.tools/super-lg}. -\fillintwo{Origin AS the world sees:}{an AS path you found:} -\noindent The lab daemon's AS4200000001 is missing from that path. Where did it go, and why? -\answer[2] - -\task{Reach it.} -The router answers for every address in the lab space, so your prefix is -reachable exactly when your announcement got out:\quad -\texttt{ping -6 2a0d:d741:\nhex::1} -\reach - -% ============================================================================== -\section{Steering Your Prefix with Large Communities} - -A Large Community is a tag added to a route. It can have any meaning that the network -which issued it decides. In BGP large communities, the first part is the AS number -and the subsequent two integers are up to the interpretation of that network. - -\begin{table}[h] -\centering -\begin{tabular}{@{}l@{\hspace{1.6em}}l@{\hspace{1.6em}}l@{}} -\toprule -Large community & Read by & Effect \\ -\midrule -\texttt{210036:0:0} & \texttt{pr01.rue} & announce nowhere \ldots \\ -\texttt{210036:1:42476} & \texttt{pr01.rue} & \ldots except to AS42476, the SwissIX route servers \\ -\midrule -\texttt{42476:0:0} & SwissIX & send to no member \ldots \\ -\texttt{42476:1:559} & SwissIX & \ldots except AS559, SWITCH, the network ETH is part of \\ -\bottomrule -\end{tabular} -\end{table} - -\task{One exchange only.} -Announce your prefix again with \texttt{210036:0:0} and -\texttt{210036:1:42476}; announcing a prefix you own replaces it. Before you -look: who do you expect to still see it? -\answer[2] -\noindent Wait a moment, then check on \texttt{https://bgp.tools/super-lg} and with \texttt{ping}. -\reach - -\task{To SWITCH only.} -Add \texttt{42476:0:0} and \texttt{42476:1:559} to the two you already have and -announce again. Our router now tells only the route servers, and they tell -only SWITCH. -\reach -\noindent Is your prefix still visible on \texttt{https://bgp.tools/super-lg}? Explain what you see. -\answer[2] - -\task{Use of large communities.} -You steered routing in two networks you do not operate, using nothing but -numbers attached to a route. What stops you from attaching a community that -one of \emph{their} upstreams acts on? -\answer[2] - -\task{Clean up.} Withdraw your prefix and see how long it takes to go away. - -\end{document} diff --git a/docs/lab/prod/bird-ferrix.conf b/docs/lab/prod/bird-ferrix.conf deleted file mode 100644 index 7edb9e6..0000000 --- a/docs/lab/prod/bird-ferrix.conf +++ /dev/null @@ -1,172 +0,0 @@ -# BIRD 2.x / 3.x: peer the production router (AS210036) with ferrix_bgp over -# WireGuard. ferrix is announce-only: we accept the lab /48s from it and send -# it nothing. -# -# Include from bird.conf: include "/etc/bird/bird-ferrix.conf"; -# Check and apply: birdc configure check && birdc configure -# 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} ]; - -filter ferrix_import -{ - # Only the students' /48s, nothing shorter or longer, nothing else. - if !(net ~ LAB_PREFIXES) then reject; - - # ferrix allows at most 16 prepends on top of its own ASN. - if bgp_path.len > 17 then reject; - if bgp_path.first != FERRIX_ASN then reject; - - # Students type communities freely into a web form with no login, so - # only namespaces we have chosen to expose get through. - # - # Standard: just the two well-known ones BIRD itself honours, so - # NO_EXPORT / NO_ADVERTISE visibly work. Notably BLACKHOLE (65535:666), - # which SwissIX acts on, stays scrubbed. - bgp_community.filter([ (65535, 65281), (65535, 65282) ]); - - # Large: the SwissIX route-server action communities, all of which live - # under their route-server ASN 42476 - # (https://www.swissix.ch/resources/action-bgp-communities/): - # 42476:0: do not redistribute to peer 42476:0:0 to nobody - # 42476:1: redistribute to peer 42476:1:0 to all - # 42476:101|102|103: prepend 1x / 2x / 3x - # 42476:901|902: add NO_EXPORT / NO_ADVERTISE - # 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, functions 0 to 3, evaluated by - # lab_export_allowed() below. - bgp_large_community.filter([ (42476, *, *), (MY_ASN, 0..3, *) ]); - - bgp_ext_community.empty; - - # Announce-only: there is nothing behind these prefixes. Do not forward - # their traffic into the tunnel; answer with "unreachable" instead. - dest = RTD_UNREACHABLE; - - accept; -} - -protocol bgp ferrix -{ - description "ferrix_bgp teaching lab (announce only)"; - local 2a0d:d747:1::1 as 210036; - neighbor 2a0d:d747:1:: as FERRIX_ASN; - direct; - - # ferrix dials us. Staying passive avoids log noise from failed attempts - # towards a laptop that is asleep or firewalled. - passive on; - - # ferrix supports none of these; leave them off. - # password "..."; (TCP MD5) - # ttl security on; (GTSM) - # bfd on; - - ipv6 { - import filter ferrix_import; - # 128 students, one /48 each. Extra routes are ignored, the session - # stays up. - import limit 130 action block; - # Send nothing: ferrix keeps received routes in memory and renders - # every one of them on its status page. - export none; - }; -} - -# --------------------------------------------------------------------------- -# Export control for lab routes. -# -# 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: -# -# ipv6 { export where peers_out(42476, AT_SWISSIX); ... }; -# -# Students steer their prefix with large communities, in this precedence: -# -# 1. 210036:0: never announce to that neighbour AS -# 210036:2: never announce at that location -# 2. 210036:1: announce to that neighbour AS -# 210036:3: 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 -# --------------------------------------------------------------------------- -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; -} - -# 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 -# -# 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_route_out(210400, AT_DOWNSTREAM) then accept; -# reject; -# } -# accept; -# }; -# -# The ferrix branch only ever applies to routes learned from ferrix, so these -# 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 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 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 send large communities -# unmodified apart from the delete above; check nothing else clears them. -# --------------------------------------------------------------------------- diff --git a/docs/lab/prod/ferrix.textpb b/docs/lab/prod/ferrix.textpb deleted file mode 100644 index b53f803..0000000 --- a/docs/lab/prod/ferrix.textpb +++ /dev/null @@ -1,20 +0,0 @@ -router_id: "10.255.0.2" # must differ from the production router's -local_asn: 4200000001 # private asn used for internal peering. - -listen_endpoints { address: "2a0d:d747:1::" port: 9179 } - -peers { - name: "pr01.rue.rayhaan.net" - endpoint { address: "2a0d:d747:1::1" port: 179 } - remote_asn: 210036 - hold_time_seconds: 90 -} -connect_retry_seconds: 5 -collision_detect_established_state: true - -local_route_policy { - allowed_prefixes: "2a0d:d741::/40" - max_routes_per_owner: 1 -} - -http_endpoint { address: "127.0.0.1" port: 8080 } diff --git a/docs/lab/prod/on-router/05-lab-lo.network b/docs/lab/prod/on-router/05-lab-lo.network deleted file mode 100644 index 06d7cdd..0000000 --- a/docs/lab/prod/on-router/05-lab-lo.network +++ /dev/null @@ -1,20 +0,0 @@ -# /etc/systemd/network/05-lab-lo.network -# If a .network file already matches "lo" (e.g. the AnyIP one), merge these -# sections into it instead: networkd applies only the first match. -[Match] -Name=lo - -# BIRD and ferrix peer over ::1, which needs no configuration. Should BIRD -# object to identical local and neighbour addresses, give them one each -# instead (and use those in bird.conf and ferrix.textpb): -# -# [Address] -# Address=fd00:179::1/128 -# -# [Address] -# Address=fd00:179::2/128 - -# AnyIP: the router answers ping for every address in the lab space. -[Route] -Destination=2a0d:d741::/40 -Type=local diff --git a/docs/lab/prod/on-router/README.md b/docs/lab/prod/on-router/README.md deleted file mode 100644 index 1f93569..0000000 --- a/docs/lab/prod/on-router/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Running ferrix_bgp on the production router - -ferrix and BIRD run side by side on pr01 and peer over `::1`. Students reach the status page through a reverse proxy that adds -TLS and a password. Nothing here has been run on the router; treat each step -as something to check. - -## 1. Build a static Linux binary (on the Mac) - -The `x86_64-unknown-linux-musl` target and Homebrew's musl cross-linker are -enough; the router needs no Rust toolchain and no `protoc`. - - CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \ - cargo build --release -p bgp --target x86_64-unknown-linux-musl - - scp target/x86_64-unknown-linux-musl/release/bgp pr01:/tmp/ferrix-bgp - -Check the router really is x86-64 first (`uname -m`). - -## 2. Install on the router - - install -m 0755 /tmp/ferrix-bgp /usr/local/bin/ferrix-bgp - install -d /etc/ferrix-bgp - install -m 0644 ferrix.textpb /etc/ferrix-bgp/ferrix.textpb - install -m 0644 ferrix-bgp.service /etc/systemd/system/ferrix-bgp.service - -## 3. Loopback - -Peering over `::1` needs no address setup. `05-lab-lo.network` only carries -the AnyIP route for the lab space; merge it into the networkd file that -matches `lo` if it is not there already, then `networkctl reload`. - -## 4. BIRD - -Replace the `protocol bgp ferrix` stanza with `bird-ferrix-local.conf`, then -`birdc configure check` and `birdc configure timeout 300` / `confirm`. - -## 5. Start - - systemctl daemon-reload - systemctl enable --now ferrix-bgp - journalctl -u ferrix-bgp -f - birdc show protocols all ferrix - -The journal should say `Ping monitor listening for ICMPv6 echo requests`. If -it says the monitor is disabled, the unit is missing `CAP_NET_RAW`; everything -else works without it and the status page says why the table is off. - -To check the monitor before the class, announce a prefix on the page, turn on -auto-refresh and ping an address inside it from somewhere else: - - ping -c 3 2a0d:d741:1::1 - -The ping shows up under "Pings to announced addresses" with its source, the -interface it arrived on and the hop limit it arrived with. Check that -"arrived on" names the real ingress port (the exchange or transit interface) -and not `lo`. - -## 6. Let students in - -The API has no authentication and announces into a real AS, so do not expose -port 8080. Put a reverse proxy in front with TLS and a class password. The -event stream is long-lived, so the proxy must not buffer it. - -Apache (tested against a local instance: 401 without the password, pages, -API and POSTs through the proxy, and events arriving within 50 ms): - - - ServerName lab.example.net - Redirect permanent "/" "https://lab.example.net/" - - - - ServerName lab.example.net - - SSLEngine on - SSLCertificateFile /etc/letsencrypt/live/lab.example.net/fullchain.pem - SSLCertificateKeyFile /etc/letsencrypt/live/lab.example.net/privkey.pem - - # Everything behind one class password. - - AuthType Basic - AuthName "BGP lab" - AuthUserFile /etc/apache2/ferrix.htpasswd - Require valid-user - - - # Use Apache's built-in error messages in this vhost. Servers with the - # multi-language error documents enabled fetch them from /error/, - # which this vhost would proxy to ferrix; a 401 then comes back as - # ferrix's empty 404: a blank page and no password prompt. With the - # built-in ones nothing is fetched, so nothing needs exempting from - # the login. - ErrorDocument 400 default - ErrorDocument 401 default - ErrorDocument 403 default - ErrorDocument 404 default - ErrorDocument 500 default - ErrorDocument 502 default - ErrorDocument 503 default - ErrorDocument 504 default - - ProxyPreserveHost On - ProxyPass "/" "http://127.0.0.1:8080/" - ProxyPassReverse "/" "http://127.0.0.1:8080/" - - # ferrix has no use for the password; do not pass it on. - RequestHeader unset Authorization - - # The status page follows a long-lived event stream. If mod_deflate - # compresses it, events sit in the compressor and never arrive. - SetEnvIf Request_URI "^/api/events" no-gzip=1 - - -Modules: `proxy proxy_http ssl headers setenvif auth_basic authn_file`. -Create the password file with `htpasswd -c -B /etc/apache2/ferrix.htpasswd student`. -To also limit it to known networks, replace `Require valid-user` with - - - Require valid-user - Require ip 192.0.2.0/24 2001:db8::/32 - - -Caddy: - - lab.example.net { - basic_auth { - student - } - reverse_proxy 127.0.0.1:8080 { - flush_interval -1 - } - } - -nginx, inside a TLS `server` block: - - location / { - auth_basic "BGP lab"; - auth_basic_user_file /etc/nginx/lab.htpasswd; - proxy_pass http://127.0.0.1:8080; - proxy_http_version 1.1; - proxy_buffering off; - proxy_read_timeout 1h; - } - -Restricting the proxy to the university's address ranges as well is cheap -and worth doing. Only the Apache example has been tested. - -## 7. Load test before the class - -`tools/loadtest.py` makes N simulated students announce their /48 at the same -instant, then verifies the table, ownership under load, the event stream and -the number of BGP UPDATEs actually sent, and withdraws everything again. -Python 3 standard library only. - - tools/loadtest.py 128 --url https://lab.example.net --user student - -Every test route carries `210036:0:0`, so BIRD accepts it but exports it to no -neighbour (apart from `home_v6`, which is outside the export control). While -it runs, `birdc show route protocol ferrix count` should rise to the number of -students and fall back to what it was. BIRD's `import limit 130` means more -than 130 lab routes at once are blocked there, by design. - -## Things to know - -- The ping monitor only listens. The kernel answers the pings, with or - without ferrix running, because of the AnyIP route. It sees echo requests - to the lab space from anyone on the Internet, so source addresses of - strangers will appear on the page next to the students' own. It remembers - at most 2048 source and destination pairs and publishes at most 50 events a - second, so a ping flood costs little; `ping_monitor_echo_requests_total` and - `ping_monitor_events_suppressed_total` on `/metricz` show what it saw. - -- Announced routes live in memory. A restart of the service forgets them and - students have to announce again. -- Stopping the service sends BIRD a Cease first, so restarts reconnect at - once. -- ferrix's own safety net is `local_route_policy`; BIRD's import filter and - import limit are the second one. Keep both. diff --git a/docs/lab/prod/on-router/bird-ferrix-local.conf b/docs/lab/prod/on-router/bird-ferrix-local.conf deleted file mode 100644 index 45fb647..0000000 --- a/docs/lab/prod/on-router/bird-ferrix-local.conf +++ /dev/null @@ -1,23 +0,0 @@ -# Replaces "protocol bgp ferrix" in bird.conf when ferrix runs on this host. -# Only the addresses and "multihop" differ from the WireGuard version; the -# import filter and everything about export control stay as they are. -# -# Without "multihop" BIRD looks for the neighbour on a directly connected -# network, cannot find ::1 there, and disables the protocol with -# "Error: Invalid next hop". After fixing that, `birdc enable ferrix` if a -# reconfigure does not bring it back by itself. -protocol bgp ferrix -{ - description "ferrix_bgp teaching lab (announce only, same host)"; - local ::1 as 210036; - neighbor ::1 as FERRIX_ASN; - # Both ends are this machine, so there is no directly connected - # neighbour for "direct" (the eBGP default) to find. - multihop; - passive on; - ipv6 { - import filter ferrix_import; - import limit 130 action block; - export none; - }; -} diff --git a/docs/lab/prod/on-router/ferrix-bgp.service b/docs/lab/prod/on-router/ferrix-bgp.service deleted file mode 100644 index 748e851..0000000 --- a/docs/lab/prod/on-router/ferrix-bgp.service +++ /dev/null @@ -1,37 +0,0 @@ -[Unit] -Description=ferrix_bgp teaching BGP speaker -After=network-online.target bird.service -Wants=network-online.target - -[Service] -ExecStart=/usr/local/bin/ferrix-bgp /etc/ferrix-bgp/ferrix.textpb --level info -# SIGTERM makes ferrix send Cease / Administrative Shutdown to BIRD first. -KillSignal=SIGTERM -TimeoutStopSec=10 -Restart=on-failure -RestartSec=5 - -# It needs the network and nothing else: no root, no privileged port, no -# write access anywhere. The one capability, CAP_NET_RAW, is for the ping -# monitor's raw ICMPv6 socket; drop both lines if ping_monitor is not -# configured. -DynamicUser=yes -NoNewPrivileges=yes -ProtectSystem=strict -ProtectHome=yes -PrivateTmp=yes -PrivateDevices=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX -RestrictNamespaces=yes -LockPersonality=yes -MemoryDenyWriteExecute=yes -SystemCallArchitectures=native -AmbientCapabilities=CAP_NET_RAW -CapabilityBoundingSet=CAP_NET_RAW -MemoryMax=256M - -[Install] -WantedBy=multi-user.target diff --git a/docs/lab/prod/on-router/ferrix.textpb b/docs/lab/prod/on-router/ferrix.textpb deleted file mode 100644 index acecdcd..0000000 --- a/docs/lab/prod/on-router/ferrix.textpb +++ /dev/null @@ -1,32 +0,0 @@ -# ferrix_bgp running on the production router itself, next to BIRD. -router_id: "10.255.0.2" -local_asn: 4200000001 - -# Unused in practice (BIRD is passive and we dial it), but a listener is -# required. 179 belongs to BIRD, so take another port on our own address. -listen_endpoints { address: "::1" port: 9179 } - -peers { - name: "pr01-bird" - endpoint { address: "::1" port: 179 } - # BIRD recognises its neighbour by source address, so pin ours. - local_address: "::1" - remote_asn: 210036 - hold_time_seconds: 90 -} -connect_retry_seconds: 5 -collision_detect_established_state: true - -local_route_policy { - allowed_prefixes: "2a0d:d741::/40" - max_routes_per_owner: 1 -} - -# Loopback only. Students reach it through the reverse proxy, which adds TLS -# and a password; the API itself has neither. -http_endpoint { address: "127.0.0.1" port: 8080 } - -# Show who pings the lab addresses. The kernel answers the pings (AnyIP route -# in 05-lab-lo.network); this only listens. Watches allowed_prefixes above. -# Needs CAP_NET_RAW, which the systemd unit grants. -ping_monitor {} diff --git a/docs/lab/prod/pr01/bird.conf b/docs/lab/prod/pr01/bird.conf deleted file mode 100644 index 2f94133..0000000 --- a/docs/lab/prod/pr01/bird.conf +++ /dev/null @@ -1,1495 +0,0 @@ -router id 193.36.104.1; - -log stderr { debug, trace, info, remote, warning, error, auth, fatal, bug }; - -filter rayhaan_net_only { - if (net ~ [2a0d:d740::/29+]) then accept; - if (net ~ [193.36.104.0/22+]) then accept; - reject; -} - -# -# Export control for lab routes (the /48s learned from ferrix). -# -# Every eBGP session states who it talks to and where: its neighbour AS and -# one of the locations below. Students steer their prefix with large -# communities, in this order of precedence: -# -# 1. 210036:0: never announce to that neighbour AS -# 210036:2: never announce at that location -# 2. 210036:1: announce to that neighbour AS -# 210036:3: 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: "to Cloudflare, but -# not at DE-CIX" is 210036:0:0 210036:1:13335 210036:2:4. -# -define AT_SWISSIX = 1; # SwissIX -define AT_NLIX = 2; # NL-ix -define AT_FRANCEIX = 3; # France-IX -define AT_DECIX = 4; # DE-CIX Frankfurt -define AT_FREEIX = 5; # Free-IX Switzerland -define AT_COMMUNITYIX = 6; # Community-IX.ch -define AT_TRANSIT = 100; # transit (CommunityRack) -define AT_DOWNSTREAM = 200; # downstream customers - -function lab_export_allowed(int peer_as; int location) -{ - if (210036, 0, peer_as) ~ bgp_large_community then return false; - if (210036, 2, location) ~ bgp_large_community then return false; - if (210036, 1, peer_as) ~ bgp_large_community then return true; - if (210036, 3, location) ~ bgp_large_community then return true; - if (210036, 0, 0) ~ bgp_large_community then return false; - return true; -} - -# 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([ (210036, 0..3, *) ]); - return true; -} - -protocol kernel kernel4 { - kernel table 200; - scan time 120; - ipv4 { - import all; - export all; - }; -} - -protocol kernel kernel6 { - kernel table 200; - scan time 120; - ipv6 { - import all; - export all; - }; -} - -protocol device { - scan time 10; -} - -protocol direct { - ipv4 { - import filter rayhaan_net_only; - }; - ipv6 { - import filter rayhaan_net_only; - }; -} - -function peers_out(int peer_as; int location) -{ - 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; -} - -function comrack_out(int peer_as; int location) -{ - bgp_med = 500; - 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 proto = "benfiedler_v4" then return true; - if proto = "benfiedler_v6" 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; -} - -filter peers_in { - if (net = 0.0.0.0/0) then { - reject; - } - if (net = ::/0) then { - reject; - } - # Don't accept routes from our own ASN. - if (bgp_path ~ [210036]) then { - reject; - } - accept; -} - - -filter drop_peers_from_transits { - if (bgp_path ~ [15169, 6939]) then { - reject; - } -} - -# CommunityRack V4 -protocol bgp comrack_v4 { - description "CommunityRack IPv4 transit"; - local 185.95.219.37 as 210036; - neighbor 185.95.219.36 as 57118; - ipv4 { - export where comrack_out(57118, AT_TRANSIT); - import filter { - if (net = 0.0.0.0/0) then { - reject; - } - # Don't accept routes from our own ASN. - if (bgp_path ~ [210036]) then { - reject; - } - # Don't accept routes from AS15169 over this link, - # we have multiple other paths via IXes. - if (bgp_path ~ [15169]) then { - reject; - } - accept; - }; - export limit 200; - }; -} - -# CommunityRack V6 -protocol bgp comrack_v6 { - description "CommunityRack IPv6 transit"; - local 2a05:fc87:1:12::b as 210036; - neighbor 2a05:fc87:1:12::a as 57118; - ipv6 { - export where comrack_out(57118, AT_TRANSIT); - import filter { - if (net = ::/0) then { - reject; - } - # Don't accept routes from our own ASN. - if (bgp_path ~ [210036]) then { - reject; - } - # Don't accept routes from AS15169 over this link, - # we have multiple other paths via IXes. - if (bgp_path ~ [15169]) then { - reject; - } - accept; - }; - export limit 200; - }; -} - -# Temporarily disabled until a better route is found. -#protocol bgp bgptools { -# description "BGPTools monitoring session"; -# local 2a05:fc87:1:12::b as 210036; -# neighbor 2a0c:2f07:9459::b14 as 212232; -# multihop; -# ipv6 { -# add paths tx; -# export filter { accept; }; -# import filter { reject; }; -# }; -# ipv4 { -# add paths tx; -# export filter { accept; }; -# import filter { reject; }; -# }; -#} - -# -# SwissIX peers -# - -# RS1 -protocol bgp swissix_rs1_v4 { - description "SwissIX RS1 IPv4"; - local 91.206.52.201 as 210036; - neighbor 91.206.52.252 as 42476; - default bgp_local_pref 500; - ipv4 { - export where peers_out(42476, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp swissix_rs1_v6 { - description "SwissIX RS1 IPv6"; - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::a5ec:a:1 as 42476; - default bgp_local_pref 500; - ipv6 { - export where peers_out(42476, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# RS2 -protocol bgp swissix_rs2_v4 { - description "swissix rs2 ipv4"; - local 91.206.52.201 as 210036; - neighbor 91.206.53.253 as 42476; - default bgp_local_pref 500; - ipv4 { - export where peers_out(42476, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_rs2_v6 { - description "swissix rs2 ipv6"; - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::a5ec:a:2 as 42476; - default bgp_local_pref 500; - ipv6 { - export where peers_out(42476, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Hurricane -protocol bgp swissix_he_v4 { - description "Hurricane Electric @ SwissIX ipv4"; - local 91.206.52.201 as 210036; - neighbor 91.206.52.170 as 6939; - ipv4 { - export where peers_out(6939, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_he_v6 { - description "Hurricane Electric @ SwissIX ipv6"; - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::aa as 6939; - ipv6 { - export where peers_out(6939, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Pim -protocol bgp swissix_pim_8298_v4 { - description "Pim SwissIX IPv4"; - local 91.206.52.201 as 210036; - neighbor 91.206.53.12 as 8298; - ipv4 { - export where peers_out(8298, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_pim_8298_v6 { - description "Pim SwissIX IPv6"; - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::10c as 8298; - ipv6 { - export where peers_out(8298, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Andrew -protocol bgp swissix_andrew_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.243 as 34615; - ipv4 { - export where peers_out(34615, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_andrew_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::f3 as 34615; - ipv6 { - export where peers_out(34615, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Tobias -protocol bgp swissix_tobias_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.53.19 as 212495; - ipv4 { - export where peers_out(212495, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_tobias_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::113 as 212495; - ipv6 { - export where peers_out(212495, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Meerfabrig -protocol bgp swissix_meerfabrig_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.244 as 34549; - ipv4 { - export where peers_out(34549, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_meerfabrig_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::f4 as 34549; - ipv6 { - export where peers_out(34549, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Akamai -protocol bgp swissix_akamai_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.56 as 20940; - ipv4 { - export where peers_out(20940, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_akamai_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::38 as 20940; - ipv6 { - export where peers_out(20940, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# JMP Technologies -protocol bgp swissix_jmp_technologies_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.53.18 as 61423; - ipv4 { - export where peers_out(61423, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_jmp_technologies_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::112 as 61423; - ipv6 { - export where peers_out(61423, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp swissix_google_v4_1 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.74 as 15169; - ipv4 { - export where peers_out(15169, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp swissix_google_v6_1 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::4a as 15169; - ipv6 { - export where peers_out(15169, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp swissix_google_v4_2 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.223 as 15169; - ipv4 { - export where peers_out(15169, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp swissix_google_v6_2 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::df as 15169; - ipv6 { - export where peers_out(15169, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# 31173 Services AB -protocol bgp swissix_31173_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.230 as 39351; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv4 { - export where peers_out(39351, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp swissix_31173_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::e6 as 39351; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv6 { - export where peers_out(39351, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# Cloudflare -protocol bgp swissix_cloudflare_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.52.192 as 13335; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv4 { - export where peers_out(13335, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp swissix_cloudflare_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::c0 as 13335; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv6 { - export where peers_out(13335, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - -# i3d -protocol bgp swissix_i3d_v4 { - local 91.206.52.201 as 210036; - neighbor 91.206.53.28 as 49544; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv4 { - export where peers_out(49544, AT_SWISSIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp swissix_i3d_v6 { - local 2001:7f8:24::c9 as 210036; - neighbor 2001:7f8:24::11c as 49544; - # Higher local pref for Zurich. - default bgp_local_pref 200; - ipv6 { - export where peers_out(49544, AT_SWISSIX); - import filter peers_in; - export limit 200; - }; -} - - - -# -# NL-IX -# - -# RS1 -protocol bgp nl_ix_rs1_v4 { - local 193.239.117.206 as 210036; - neighbor 193.239.116.255 as 34307; - ipv4 { - export where peers_out(34307, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_rs1_v6 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a503:4307:1 as 34307; - ipv6 { - export where peers_out(34307, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -# RS2 -protocol bgp nl_ix_rs2_v4 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.0 as 34307; - ipv4 { - export where peers_out(34307, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_rs2_v6 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a503:4307:2 as 34307; - ipv6 { - export where peers_out(34307, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -# Apple -protocol bgp nl_ix_apple_v4_r1 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.112 as 714; - ipv4 { - export where peers_out(714, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_apple_v6_r1 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a500:714:1 as 714; - ipv6 { - export where peers_out(714, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp nl_ix_apple_v4_r2 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.113 as 714; - ipv4 { - export where peers_out(714, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_apple_v6_r2 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a500:714:2 as 714; - ipv6 { - export where peers_out(714, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - - -# Hurricane -protocol bgp nl_ix_he_v4 { - local 193.239.117.206 as 210036; - neighbor 193.239.116.14 as 6939; - ipv4 { - export where peers_out(6939, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_he_v6 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a500:6939:1 as 6939; - ipv6 { - export where peers_out(6939, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -# NL-IX Cloudflare -protocol bgp nl_ix_cloudflare_1 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a501:3335:1 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; - ipv6 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp nl_ix_cloudflare_2 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a501:3335:2 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; - ipv6 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp nl_ix_cloudflare_3 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a501:3335:3 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; - ipv6 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp nl_ix_cloudflare_4 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a501:3335:4 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_cloudflare_v4_1 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.114 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_cloudflare_v4_2 { - local 193.239.117.206 as 210036; - neighbor 193.239.118.138 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_cloudflare_v4_3 { - local 193.239.117.206 as 210036; - neighbor 193.239.119.101 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp nl_ix_cloudflare_v4_4 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.14 as 13335; - ipv4 { - export where peers_out(13335, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} - -# 31173 Services AB. -protocol bgp nl_ix_31173_v4 { - local 193.239.117.206 as 210036; - neighbor 193.239.116.34 as 39351; - ipv4 { - export where peers_out(39351, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp nl_ix_31173_v6 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a503:9351:1 as 39351; - ipv6 { - export where peers_out(39351, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp nl_ix_i3d_v4 { - local 193.239.117.206 as 210036; - neighbor 193.239.117.76 as 49544; - ipv4 { - export where peers_out(49544, AT_NLIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp nl_ix_i3d_v6 { - local 2001:7f8:13::a521:36:1 as 210036; - neighbor 2001:7f8:13::a504:9544:1 as 49544; - ipv6 { - export where peers_out(49544, AT_NLIX); - import filter peers_in; - export limit 200; - }; -} - -# -# France-IX -# - -protocol bgp france_ix_rs1_v4{ - local 37.49.238.32 as 210036; - neighbor 37.49.236.250 as 51706; - ipv4 { - export where peers_out(51706, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_rs1_v6 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::250 as 51706; - ipv6 { - export where peers_out(51706, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -# Hurricane -protocol bgp france_ix_he_v4{ - local 37.49.238.32 as 210036; - neighbor 37.49.236.10 as 6939; - ipv4 { - export where peers_out(6939, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_he_v6 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::10 as 6939; - ipv6 { - export where peers_out(6939, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -# Transatel -protocol bgp france_ix_transatel_v4{ - local 37.49.238.32 as 210036; - neighbor 37.49.236.140 as 198335; - ipv4 { - export where peers_out(198335, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_transatel_v6 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::140 as 198335; - ipv6 { - export where peers_out(198335, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -# Apple -protocol bgp france_ix_apple_v4_r1 { - local 37.49.238.32 as 210036; - neighbor 37.49.237.176 as 714; - ipv4 { - export where peers_out(714, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_apple_v6_r1 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:176 as 714; - ipv6 { - export where peers_out(714, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_apple_v4_r2 { - local 37.49.238.32 as 210036; - neighbor 37.49.237.179 as 714; - ipv4 { - export where peers_out(714, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_apple_v6_r2 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:179 as 714; - ipv6 { - export where peers_out(714, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_akamai_v4_r1 { - local 37.49.238.32 as 210036; - neighbor 37.49.236.168 as 20940; - ipv4 { - export where peers_out(20940, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_akamai_v6_r1 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::168 as 20940; - ipv6 { - export where peers_out(20940, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_akamai_v4_r2 { - local 37.49.238.32 as 210036; - neighbor 37.49.237.164 as 32787; - ipv4 { - export where peers_out(32787, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_akamai_v6_r2 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:164 as 32787; - ipv6 { - export where peers_out(32787, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_cloudflare_v4_r1 { - local 37.49.238.32 as 210036; - neighbor 37.49.237.49 as 13335; - ipv4 { - export where peers_out(13335, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_cloudflare_v4_r2 { - local 37.49.238.32 as 210036; - neighbor 37.49.238.59 as 13335; - ipv4 { - export where peers_out(13335, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_cloudflare_v6_r1 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:49 as 13335; - ipv6 { - export where peers_out(13335, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp france_ix_cloudflare_v6_r2 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::2:59 as 13335; - ipv6 { - export where peers_out(13335, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_alibaba { - local 37.49.238.32 as 210036; - neighbor 37.49.237.201 as 45102; - ipv4 { - export where peers_out(45102, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; - ipv6 { - export where peers_out(45102, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp france_ix_i3d_v4 { - local 37.49.238.32 as 210036; - neighbor 37.49.237.230 as 49544; - ipv4 { - export where peers_out(49544, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp france_ix_i3d_v6_r1 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:230 as 49544; - ipv6 { - export where peers_out(49544, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -# 31173 services AB. -protocol bgp france_ix_39351_v4{ - local 37.49.238.32 as 210036; - neighbor 37.49.237.171 as 39351; - ipv4 { - export where peers_out(39351, AT_FRANCEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp france_ix_39351_v6 { - local 2001:7f8:54::2:32 as 210036; - neighbor 2001:7f8:54::1:171 as 39351; - ipv6 { - export where peers_out(39351, AT_FRANCEIX); - import filter peers_in; - export limit 200; - }; -} - -# -# DE-CIX -# - -protocol bgp de_cix_rs1_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.192.157 as 6695; - default bgp_local_pref 150; - ipv4 { - export where peers_out(6695, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_rs1_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::1a27:5051:c09d as 6695; - default bgp_local_pref 150; - ipv6 { - export where peers_out(6695, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_rs2_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.193.157 as 6695; - default bgp_local_pref 150; - ipv4 { - export where peers_out(6695, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_rs2_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::1a27:5051:c19d as 6695; - default bgp_local_pref 150; - ipv6 { - export where peers_out(6695, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -# Apple -protocol bgp de_cix_apple_v4_r1 { - local 80.81.196.144 as 210036; - neighbor 80.81.193.202 as 714; - ipv4 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} -protocol bgp de_cix_apple_v6_r1 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::2ca:0:1 as 714; - ipv6 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_apple_v4_r2 { - local 80.81.196.144 as 210036; - neighbor 80.81.193.223 as 714; - ipv4 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_apple_v6_r2 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::2ca:0:2 as 714; - ipv6 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_apple_v4_r3 { - local 80.81.196.144 as 210036; - neighbor 80.81.194.171 as 714; - ipv4 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_apple_v6_r3 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::2ca:0:4 as 714; - ipv6 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_apple_v4_r4 { - local 80.81.196.144 as 210036; - neighbor 80.81.194.161 as 714; - ipv4 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_apple_v6_r4 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::2ca:0:3 as 714; - ipv6 { - export where peers_out(714, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - - -protocol bgp de_cix_cprm_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.192.38 as 8657; - ipv4 { - export where peers_out(8657, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_cprm_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::21d1:0:1 as 8657; - ipv6 { - export where peers_out(8657, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_clouflare_v4_1 { - local 80.81.196.144 as 210036; - neighbor 80.81.194.180 as 13335; - default bgp_local_pref 150; - ipv4 { - export where peers_out(13335, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_clouflare_v4_2 { - local 80.81.196.144 as 210036; - neighbor 80.81.193.129 as 13335; - default bgp_local_pref 150; - ipv4 { - export where peers_out(13335, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_cloudflare_v6_1 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::3417:0:1 as 13335; - default bgp_local_pref 150; - ipv6 { - export where peers_out(13335, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp de_cix_cloudflare_v6_2 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::3417:0:2 as 13335; - default bgp_local_pref 150; - ipv6 { - export where peers_out(13335, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_alibaba { - local 80.81.196.144 as 210036; - neighbor 80.81.195.60 as 45102; - ipv4 { - export where peers_out(45102, AT_DECIX); - import filter peers_in; - export limit 10; - }; - ipv6 { - export where peers_out(45102, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_jmp_technologies_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.195.13 as 61423; - ipv4 { - export where peers_out(61423, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp de_cix_jmp_technologies_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::efef:0:1 as 61423; - ipv6 { - export where peers_out(61423, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -# 31173 services AB. -protocol bgp de_cix_31173_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.195.179 as 39351; - ipv4 { - export where peers_out(39351, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_31173_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::99b7:0:1 as 39351; - ipv6 { - export where peers_out(39351, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp de_cix_alibaba_multi { - local 80.81.196.144 as 210036; - neighbor 80.81.194.33 as 45102; - ipv4 { - export where peers_out(45102, AT_DECIX); - import filter peers_in; - export limit 10; - import limit 100; - }; - ipv6 { - export where peers_out(45102, AT_DECIX); - import filter peers_in; - export limit 10; - import limit 100; - }; -} - -protocol bgp de_cix_i3d_v4 { - local 80.81.196.144 as 210036; - neighbor 80.81.194.107 as 49544; - ipv4 { - export where peers_out(49544, AT_DECIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp de_cix_i3d_v6 { - local 2001:7f8::3:3474:0:1 as 210036; - neighbor 2001:7f8::c188:0:1 as 49544; - ipv6 { - export where peers_out(49544, AT_DECIX); - import filter peers_in; - export limit 200; - }; -} - -# -# FREE-IX -# - -protocol bgp free_ix_rs1_v4 { - local 185.1.205.5 as 210036; - neighbor 185.1.205.254 as 51530; - ipv4 { - export where peers_out(51530, AT_FREEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp free_ix_rs1_v6 { - local 2001:7f8:111:42:0:3:3474:1 as 210036; - neighbor 2001:7f8:111:42::c94a:1 as 51530; - ipv6 { - export where peers_out(51530, AT_FREEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp free_ix_rs2_v4 { - local 185.1.205.5 as 210036; - neighbor 185.1.205.253 as 51530; - ipv4 { - export where peers_out(51530, AT_FREEIX); - import filter peers_in; - export limit 10; - }; -} -protocol bgp free_ix_rs2_v6 { - local 2001:7f8:111:42:0:3:3474:1 as 210036; - neighbor 2001:7f8:111:42::c94a:2 as 51530; - ipv6 { - export where peers_out(51530, AT_FREEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp free_ix_otg_v4 { - local 185.1.205.5 as 210036; - neighbor 185.1.205.12 as 35202; - ipv4 { - export where peers_out(35202, AT_FREEIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp free_ix_otg_v6 { - local 2001:7f8:111:42:0:3:3474:1 as 210036; - neighbor 2001:7f8:111:42::8982:1 as 35202; - ipv6 { - export where peers_out(35202, AT_FREEIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp free_ix_remote_v4 { - local 185.1.205.5 as 210036; - neighbor 185.1.205.7 as 50869; - ipv4 { - export where peers_out(50869, AT_FREEIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp free_ix_remote_v6 { - local 2001:7f8:111:42:0:3:3474:1 as 210036; - neighbor 2001:7f8:111:42::c6b5:1 as 50869; - ipv6 { - export where peers_out(50869, AT_FREEIX); - import filter peers_in; - export limit 200; - }; -} - -# -# CommunityIX -# - -protocol bgp com_ix_otg_v4 { - local 185.1.105.7 as 210036; - neighbor 185.1.105.3 as 35202; - ipv4 { - export where peers_out(35202, AT_COMMUNITYIX); - import filter peers_in; - export limit 10; - }; -} - -protocol bgp com_ix_otg_v6 { - local 2001:7f8:bf:1::7 as 210036; - neighbor 2001:7f8:bf:1::3 as 35202; - ipv6 { - export where peers_out(35202, AT_COMMUNITYIX); - import filter peers_in; - export limit 200; - }; -} - -protocol bgp delroth_v6 { - local fd00:2::1 as 210036; - neighbor fd00:2::2 as 210400; - # Transit customers get local_pref higher than any other route - # to ensure we propagate their best routes outwards. - default bgp_local_pref 1000; - ipv6 { - export filter { - if proto = "ferrix" then { - if lab_route_out(210400, AT_DOWNSTREAM) then accept; - reject; - } - accept; # Announce all routes to downstream. - }; - import filter { - if (net ~ [2a0d:d742:40::/44+]) then { - accept; - } - reject; - }; - }; -} - -protocol bgp pkern_v6 { - local fd00:3::1 as 210036; - neighbor fd00:3::2 as 210617; - # Transit customers get local_pref higher than any other route - # to ensure we propagate their best routes outwards. - default bgp_local_pref 1000; - ipv4 { - extended next hop on; - export filter { - accept; # Announce all routes downstream. - }; - import filter { - if (net ~ [193.36.107.0/24]) then { - accept; - } - reject; - }; - }; - ipv6 { - export filter { - if proto = "ferrix" then { - if lab_route_out(210617, AT_DOWNSTREAM) then accept; - reject; - } - accept; # Announce all routes to downstream. - }; - import filter { - if (net ~ [2a0e:97c0:5a0::/48, 2a0e:97c0:5a1::/48, 2a0d:d740:107::/48]) then { - accept; - } - reject; - }; - }; -} - -protocol bgp home_v6 { - local 2a0d:d747:100:: as 210036; - neighbor 2a0d:d747:100::1 as 210036; - allow local as; - ipv6 { - next hop self; - export filter { - # Don't advertise stuff from comrack to home. - if (proto = "comrack_v6") then { - reject; - } - accept; - }; - import filter { - if (net ~ [2a0d:d740:105::/48]) then { - bgp_path.empty; - accept; - } - reject; - }; - }; -} - -protocol bgp home_v4 { - local 10.2.0.2 as 210036; - neighbor 10.2.0.1 as 210036; - allow local as; - direct; - ipv4 { - next hop self; - gateway direct; - import where net ~ [ 193.36.105.0/24 ]; - export where source = RTS_BGP; - }; -} - -define FERRIX_ASN = 4200000001; -define LAB_PREFIXES = [ 2a0d:d741::/40{48,48} ]; - -filter ferrix_import -{ - if !(net ~ LAB_PREFIXES) then reject; - if bgp_path.len > 17 then reject; - if bgp_path.first != FERRIX_ASN then reject; - bgp_community.filter([ (65535, 65281), (65535, 65282) ]); - bgp_large_community.filter([ (42476, *, *), (210036, 0..3, *) ]); - bgp_ext_community.empty; - dest = RTD_UNREACHABLE; - accept; -} - -protocol bgp ferrix -{ - description "ferrix_bgp teaching lab (announce only, same host)"; - local ::1 as 210036; - neighbor ::1 as FERRIX_ASN; - # Both ends are this machine, so there is no directly connected - # neighbour for "direct" (the eBGP default) to find. - multihop; - passive on; - ipv6 { - import filter ferrix_import; - import limit 130 action block; - export none; - }; -} - - diff --git a/docs/lab/r1/daemons b/docs/lab/r1/daemons deleted file mode 100644 index b89af75..0000000 --- a/docs/lab/r1/daemons +++ /dev/null @@ -1,4 +0,0 @@ -bgpd=yes -zebra=yes -staticd=yes -vtysh_enable=yes diff --git a/docs/lab/r1/frr.conf b/docs/lab/r1/frr.conf deleted file mode 100644 index 1e3c3f2..0000000 --- a/docs/lab/r1/frr.conf +++ /dev/null @@ -1,44 +0,0 @@ -! r1: AS65001. Peers with ferrix (AS65000) and r2 (AS65002). -! Applies policy on routes from ferrix based on the communities students set: -! NO_EXPORT (65535:65281) standard: not sent to r2 (FRR honours it) -! 65001:100 r1 prepends its own ASN three times -! BLACKHOLE (65535:666) dropped at r1 -frr defaults traditional -hostname r1 -log stdout -! -ip route 192.0.2.0/24 Null0 -! -bgp community-list standard PREPEND-ME permit 65001:100 -bgp community-list standard BLACKHOLE permit 65535:666 -! -route-map FROM-FERRIX deny 10 - match community BLACKHOLE -! -route-map FROM-FERRIX permit 20 - match community PREPEND-ME - set as-path prepend 65001 65001 65001 -! -route-map FROM-FERRIX permit 30 -! -route-map PERMIT-ALL permit 10 -! -router bgp 65001 - bgp router-id 10.200.0.2 - no bgp ebgp-requires-policy - no bgp network import-check - neighbor 10.200.0.1 remote-as 65000 - neighbor 10.200.0.1 description ferrix - neighbor 10.200.0.1 timers 10 30 - neighbor 10.200.0.3 remote-as 65002 - neighbor 10.200.0.3 description r2 - neighbor 10.200.0.3 timers 10 30 - ! - address-family ipv4 unicast - network 192.0.2.0/24 - neighbor 10.200.0.1 route-map FROM-FERRIX in - neighbor 10.200.0.1 route-map PERMIT-ALL out - neighbor 10.200.0.3 route-map PERMIT-ALL in - neighbor 10.200.0.3 route-map PERMIT-ALL out - exit-address-family -! diff --git a/docs/lab/r1/vtysh.conf b/docs/lab/r1/vtysh.conf deleted file mode 100644 index e0ab9cb..0000000 --- a/docs/lab/r1/vtysh.conf +++ /dev/null @@ -1 +0,0 @@ -service integrated-vtysh-config diff --git a/docs/lab/r2/daemons b/docs/lab/r2/daemons deleted file mode 100644 index b89af75..0000000 --- a/docs/lab/r2/daemons +++ /dev/null @@ -1,4 +0,0 @@ -bgpd=yes -zebra=yes -staticd=yes -vtysh_enable=yes diff --git a/docs/lab/r2/frr.conf b/docs/lab/r2/frr.conf deleted file mode 100644 index f8f8430..0000000 --- a/docs/lab/r2/frr.conf +++ /dev/null @@ -1,33 +0,0 @@ -! r2: AS65002. Peers with r1 (AS65001) and ferrix (AS65000). -! Accepts nothing directly from ferrix so every student prefix reaches it -! only via r1, then re-advertises everything to ferrix. That closes the loop: -! ferrix sees its own prefixes come back with AS path 65002 65001 65000. -frr defaults traditional -hostname r2 -log stdout -! -ip route 198.51.100.0/24 Null0 -! -route-map PERMIT-ALL permit 10 -! -route-map DENY-ALL deny 10 -! -router bgp 65002 - bgp router-id 10.200.0.3 - no bgp ebgp-requires-policy - no bgp network import-check - neighbor 10.200.0.2 remote-as 65001 - neighbor 10.200.0.2 description r1 - neighbor 10.200.0.2 timers 10 30 - neighbor 10.200.0.1 remote-as 65000 - neighbor 10.200.0.1 description ferrix - neighbor 10.200.0.1 timers 10 30 - ! - address-family ipv4 unicast - network 198.51.100.0/24 - neighbor 10.200.0.2 route-map PERMIT-ALL in - neighbor 10.200.0.2 route-map PERMIT-ALL out - neighbor 10.200.0.1 route-map DENY-ALL in - neighbor 10.200.0.1 route-map PERMIT-ALL out - exit-address-family -! diff --git a/docs/lab/r2/vtysh.conf b/docs/lab/r2/vtysh.conf deleted file mode 100644 index e0ab9cb..0000000 --- a/docs/lab/r2/vtysh.conf +++ /dev/null @@ -1 +0,0 @@ -service integrated-vtysh-config diff --git a/docs/lab/ripe/lab-objects.rpsl b/docs/lab/ripe/lab-objects.rpsl deleted file mode 100644 index 739e68c..0000000 --- a/docs/lab/ripe/lab-objects.rpsl +++ /dev/null @@ -1,779 +0,0 @@ -inet6num: 2a0d:d741::/40 -netname: CH-RAY-ETHZ-NETSEC-LAB-2026 -descr: ETHZ Netsec 2026 BGP hands-on lab, one /48 per student -country: CH -org: ORG-RAJ2-RIPE -admin-c: RJ4890-RIPE -tech-c: RJ4890-RIPE -status: AGGREGATED-BY-LIR -assignment-size: 48 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1::/48 -descr: ETHZ Netsec 2026 BGP lab - student 1 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2::/48 -descr: ETHZ Netsec 2026 BGP lab - student 2 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3::/48 -descr: ETHZ Netsec 2026 BGP lab - student 3 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4::/48 -descr: ETHZ Netsec 2026 BGP lab - student 4 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5::/48 -descr: ETHZ Netsec 2026 BGP lab - student 5 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6::/48 -descr: ETHZ Netsec 2026 BGP lab - student 6 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7::/48 -descr: ETHZ Netsec 2026 BGP lab - student 7 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:8::/48 -descr: ETHZ Netsec 2026 BGP lab - student 8 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:9::/48 -descr: ETHZ Netsec 2026 BGP lab - student 9 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 10 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 11 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 12 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 13 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 14 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 15 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:10::/48 -descr: ETHZ Netsec 2026 BGP lab - student 16 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:11::/48 -descr: ETHZ Netsec 2026 BGP lab - student 17 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:12::/48 -descr: ETHZ Netsec 2026 BGP lab - student 18 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:13::/48 -descr: ETHZ Netsec 2026 BGP lab - student 19 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:14::/48 -descr: ETHZ Netsec 2026 BGP lab - student 20 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:15::/48 -descr: ETHZ Netsec 2026 BGP lab - student 21 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:16::/48 -descr: ETHZ Netsec 2026 BGP lab - student 22 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:17::/48 -descr: ETHZ Netsec 2026 BGP lab - student 23 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:18::/48 -descr: ETHZ Netsec 2026 BGP lab - student 24 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:19::/48 -descr: ETHZ Netsec 2026 BGP lab - student 25 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 26 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 27 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 28 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 29 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 30 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:1f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 31 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:20::/48 -descr: ETHZ Netsec 2026 BGP lab - student 32 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:21::/48 -descr: ETHZ Netsec 2026 BGP lab - student 33 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:22::/48 -descr: ETHZ Netsec 2026 BGP lab - student 34 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:23::/48 -descr: ETHZ Netsec 2026 BGP lab - student 35 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:24::/48 -descr: ETHZ Netsec 2026 BGP lab - student 36 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:25::/48 -descr: ETHZ Netsec 2026 BGP lab - student 37 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:26::/48 -descr: ETHZ Netsec 2026 BGP lab - student 38 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:27::/48 -descr: ETHZ Netsec 2026 BGP lab - student 39 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:28::/48 -descr: ETHZ Netsec 2026 BGP lab - student 40 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:29::/48 -descr: ETHZ Netsec 2026 BGP lab - student 41 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 42 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 43 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 44 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 45 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 46 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:2f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 47 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:30::/48 -descr: ETHZ Netsec 2026 BGP lab - student 48 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:31::/48 -descr: ETHZ Netsec 2026 BGP lab - student 49 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:32::/48 -descr: ETHZ Netsec 2026 BGP lab - student 50 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:33::/48 -descr: ETHZ Netsec 2026 BGP lab - student 51 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:34::/48 -descr: ETHZ Netsec 2026 BGP lab - student 52 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:35::/48 -descr: ETHZ Netsec 2026 BGP lab - student 53 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:36::/48 -descr: ETHZ Netsec 2026 BGP lab - student 54 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:37::/48 -descr: ETHZ Netsec 2026 BGP lab - student 55 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:38::/48 -descr: ETHZ Netsec 2026 BGP lab - student 56 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:39::/48 -descr: ETHZ Netsec 2026 BGP lab - student 57 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 58 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 59 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 60 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 61 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 62 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:3f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 63 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:40::/48 -descr: ETHZ Netsec 2026 BGP lab - student 64 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:41::/48 -descr: ETHZ Netsec 2026 BGP lab - student 65 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:42::/48 -descr: ETHZ Netsec 2026 BGP lab - student 66 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:43::/48 -descr: ETHZ Netsec 2026 BGP lab - student 67 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:44::/48 -descr: ETHZ Netsec 2026 BGP lab - student 68 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:45::/48 -descr: ETHZ Netsec 2026 BGP lab - student 69 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:46::/48 -descr: ETHZ Netsec 2026 BGP lab - student 70 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:47::/48 -descr: ETHZ Netsec 2026 BGP lab - student 71 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:48::/48 -descr: ETHZ Netsec 2026 BGP lab - student 72 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:49::/48 -descr: ETHZ Netsec 2026 BGP lab - student 73 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 74 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 75 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 76 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 77 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 78 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:4f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 79 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:50::/48 -descr: ETHZ Netsec 2026 BGP lab - student 80 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:51::/48 -descr: ETHZ Netsec 2026 BGP lab - student 81 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:52::/48 -descr: ETHZ Netsec 2026 BGP lab - student 82 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:53::/48 -descr: ETHZ Netsec 2026 BGP lab - student 83 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:54::/48 -descr: ETHZ Netsec 2026 BGP lab - student 84 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:55::/48 -descr: ETHZ Netsec 2026 BGP lab - student 85 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:56::/48 -descr: ETHZ Netsec 2026 BGP lab - student 86 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:57::/48 -descr: ETHZ Netsec 2026 BGP lab - student 87 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:58::/48 -descr: ETHZ Netsec 2026 BGP lab - student 88 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:59::/48 -descr: ETHZ Netsec 2026 BGP lab - student 89 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 90 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 91 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 92 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 93 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 94 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:5f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 95 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:60::/48 -descr: ETHZ Netsec 2026 BGP lab - student 96 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:61::/48 -descr: ETHZ Netsec 2026 BGP lab - student 97 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:62::/48 -descr: ETHZ Netsec 2026 BGP lab - student 98 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:63::/48 -descr: ETHZ Netsec 2026 BGP lab - student 99 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:64::/48 -descr: ETHZ Netsec 2026 BGP lab - student 100 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:65::/48 -descr: ETHZ Netsec 2026 BGP lab - student 101 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:66::/48 -descr: ETHZ Netsec 2026 BGP lab - student 102 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:67::/48 -descr: ETHZ Netsec 2026 BGP lab - student 103 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:68::/48 -descr: ETHZ Netsec 2026 BGP lab - student 104 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:69::/48 -descr: ETHZ Netsec 2026 BGP lab - student 105 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 106 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 107 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 108 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 109 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 110 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:6f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 111 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:70::/48 -descr: ETHZ Netsec 2026 BGP lab - student 112 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:71::/48 -descr: ETHZ Netsec 2026 BGP lab - student 113 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:72::/48 -descr: ETHZ Netsec 2026 BGP lab - student 114 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:73::/48 -descr: ETHZ Netsec 2026 BGP lab - student 115 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:74::/48 -descr: ETHZ Netsec 2026 BGP lab - student 116 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:75::/48 -descr: ETHZ Netsec 2026 BGP lab - student 117 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:76::/48 -descr: ETHZ Netsec 2026 BGP lab - student 118 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:77::/48 -descr: ETHZ Netsec 2026 BGP lab - student 119 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:78::/48 -descr: ETHZ Netsec 2026 BGP lab - student 120 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:79::/48 -descr: ETHZ Netsec 2026 BGP lab - student 121 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7a::/48 -descr: ETHZ Netsec 2026 BGP lab - student 122 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7b::/48 -descr: ETHZ Netsec 2026 BGP lab - student 123 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7c::/48 -descr: ETHZ Netsec 2026 BGP lab - student 124 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7d::/48 -descr: ETHZ Netsec 2026 BGP lab - student 125 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7e::/48 -descr: ETHZ Netsec 2026 BGP lab - student 126 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:7f::/48 -descr: ETHZ Netsec 2026 BGP lab - student 127 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE - -route6: 2a0d:d741:80::/48 -descr: ETHZ Netsec 2026 BGP lab - student 128 -origin: AS210036 -mnt-by: mnt-ch-ray-1 -source: RIPE diff --git a/slides/README.md b/slides/README.md deleted file mode 100644 index 24d84e9..0000000 --- a/slides/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Slides - -A reveal.js deck structured like Matt Godbolt's newer talks -([what-happens-beneath-accu-2026](https://github.com/mattgodbolt/what-happens-beneath-accu-2026)): -no stock theme but a custom dark one, every slide on a rounded panel, IBM Plex -Sans and Mono, monokai code with glowing stepped lines, 16:9 with fade -transitions, and the content in one Markdown file per section. - -What is ours: the palette, and the corner motif. His ring-and-thread became a -chain of nodes and links, an AS path. - -## The talk - -| Section | File | -|---|---| -| About the speaker | `sections/00-intro.md` | -| Internet numbering resources: addresses, prefixes, registries, AS numbers | `sections/01-resources.md` | -| BGP and the bytes of an UPDATE | `sections/03-bgp-update.md` | -| IP transit and Internet exchanges | `sections/04-transit-and-ix.md` | -| The lab: the lab speaker, pr01.rue and its exchanges | `sections/05-lab.md` | - -The UPDATE in section 3 is `2a0d:d740::/48` as AS210036 announces it to the -SwissIX route servers: next hop `2001:7f8:24::c9` (pr01's port on the peering -LAN), MED 0 and the `210036:10:2` tag, which is what pr01's export filter puts -on routes to peers. It was produced with the lab daemon's own encoder and parsed back -to the same message, not captured off the wire; a capture from pr01 may differ -in detail (BIRD can append a link-local next hop). The byte slides step through -it one part at a time (press down), and every number on them was checked -against the bytes when the slides were generated. To change the example, -regenerate rather than edit the hex by hand. - -The deck deliberately has no fragments or stepped code: a slide shows -everything at once. Two code blocks keep a static highlight on one line. - -| File | Holds | -|---|---| -| `theme.css` | the look: palette, type, panels, motif, title slide | -| `style.css` | components used by slides: columns, boxes, bytes, notes | -| `index.html` | section list, reveal.js loader and options, `bird` highlighter | -| `sections/*.md` | the slides | - -## Make it yours - -Everything brand-like is a variable at the top of `theme.css`. To compare -accents without editing, open `?accent=green`, `?accent=amber` or -`?accent=magenta` (blue is the default); copy the one you like into `:root`. -The wordmark bottom-left is the `
` in `index.html`. -For a full-bleed title image, empty the title `
` and give it -`data-background="images/title.png"`. - -## Show it - -The sections are loaded over HTTP, so open the deck through a server, not by -double-clicking `index.html`. Opened from disk, browsers block reveal.js and -the section files, and you would see a title and empty panels; the page now -says so instead: - - python3 slides/serve.py 8000 - -(`python3 -m http.server` works too, but it lets the browser cache the -section files and the stylesheet, so edits may not show until a hard reload, -Cmd+Shift+R. `serve.py` is the same server with caching turned off.) - -Then . Keys: arrows to move, `S` speaker view with -notes and a timer, `O` overview, `F` fullscreen, `B` blank the screen. - -## Offline - - slides/vendor-reveal.sh - -copies reveal.js into `slides/reveal.js/` (ignored by git). The deck uses that -copy when present and the CDN otherwise. Do this before presenting anywhere -with uncertain Wi-Fi. IBM Plex still comes from Google Fonts; without a -network the deck falls back to Helvetica/Arial and the system monospace, so -install IBM Plex on the presenting machine if that matters. - -## Write slides - -Edit the files in `sections/`; add a section by adding a file and a -`
` line in `index.html`. - - ----- next slide - --- next vertical slide (detail you can skip) - Notes: speaker notes from here to the end of the slide - -Apply a class to the element above with ``. - -| Class | Effect | -|---|---| -| `big` | one large statement on the slide | -| `note` | quiet italic aside | -| `attribution` | small credit under an image or quote | -| `borders` | table with visible borders | -| `r-stretch` | image or iframe fills the remaining height | -| `blur-edges` | soft edge for photos | - -Blocks: `
` two columns, `
` labelled cards -(`us`, `ok`, `bad`), `
` BGP bytes in the colours of the lab
-page (add `focus`, and `on` to the spans to keep lit, to dim the rest),
-`
` for the explanation under them, -`` for diagrams drawn in the theme's colours (`hot`, -`fabric`, `link`, `bgp`, `traffic`). Code fences take a language and optional line steps, e.g. -` ```bird [1-3|5-9] `; `bird` is a small highlighter defined in `index.html`. - -`` drops the panel, the motif and the wordmark -for one slide, e.g. a full-bleed image. The title slide does so by itself. - -## Publish as one file - - slides/vendor-reveal.sh # once, if slides/reveal.js is missing - python3 slides/build.py # writes slides/dist/hands-on-bgp.html - -`build.py` inlines everything the page would otherwise fetch: reveal.js and -its plugins, the stylesheets, the favicon, our scripts and the Markdown -sections. The one file (about 1.3 MB) can be uploaded anywhere and also -opens straight from disk. It still uses the network for two things: the IBM -Plex fonts from Google Fonts (without them the browser's fallback fonts are -used) and the live DNS and RIPE lookups in section 1. Speaker view (`S`) -works from the bundle too. `slides/dist/` is not committed; rebuild after -editing. - -## PDF - -Open in Chrome and print to PDF with -background graphics on. diff --git a/slides/build.py b/slides/build.py deleted file mode 100755 index 665cc33..0000000 --- a/slides/build.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -"""Bundle the deck into one self-contained HTML file for uploading. - - python3 slides/build.py # writes slides/dist/hands-on-bgp.html - -Everything the page fetches at runtime is inlined: reveal.js and its plugins -(from the vendored copy, so run vendor-reveal.sh first), the stylesheets, the -favicon, our scripts and the Markdown sections. The result opens from any web -server and from disk. Two things still need a network: the IBM Plex fonts -(Google Fonts) and the live DNS and RIPE lookups. -""" -import base64 -import os -import re -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -REVEAL = os.path.join(HERE, "reveal.js", "dist") -OUT = os.path.join(HERE, "dist", "hands-on-bgp.html") - - -def read(*parts): - with open(os.path.join(HERE, *parts), encoding="utf-8") as f: - return f.read() - - -def main(): - if not os.path.isdir(REVEAL): - sys.exit("reveal.js is not vendored; run slides/vendor-reveal.sh first") - html = read("index.html") - - # Stylesheets: reveal's first, then ours, as the loader orders them. - css = "\n".join(read("reveal.js", "dist", p) for p in ["reset.css", "reveal.css", "plugin/highlight/monokai.css"]) - css += "\n" + read("theme.css") + "\n" + read("style.css") - html = re.sub(r'\s*', "", html) - html = re.sub(r'\s*', f"\n ", html, count=1) - - icon = "data:image/svg+xml;base64," + base64.b64encode(read("images", "favicon.svg").encode()).decode() - html = html.replace('href="images/favicon.svg"', f'href="{icon}"') - - # Sections: the Markdown goes inline in a text/template script. - def inline_section(m): - attrs, path = m.group(1), m.group(2) - text = read(path) - if "\n\n' - html, n = re.subn(r'', - lambda m: inline_section(type("M", (), {"group": lambda self, i: (None, m.group(2), m.group(1))[i]})()), html) - if n == 0: - sys.exit("no data-markdown sections found in index.html") - - # Our scripts. - for name in ["dns-lookup.js", "prefix-explorer.js"]: - html, k = re.subn(rf'', lambda m: f"", html) - if k != 1: - sys.exit(f"expected one ', "", html, count=1, flags=re.S) - html = re.sub(r'\s* - - - - - - -
-
- -
-

Hands-on BGP

-

Insights into real world operations

- -
- - -
-
-
-
-
-
-
- -
AS210036 · netsec 2026
- - - - - - - - - - - diff --git a/slides/prefix-explorer.js b/slides/prefix-explorer.js deleted file mode 100644 index 86eb74d..0000000 --- a/slides/prefix-explorer.js +++ /dev/null @@ -1,123 +0,0 @@ -// A live prefix calculator on a slide. Any -// with and gets its -// .px-bits, .px-range and .px-facts filled in as the reader types. -(function () { - const ALL = (1n << 128n) - 1n; - const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); - - // "2001:db8::1" -> BigInt, or null. - function parse(text) { - const s = text.trim().toLowerCase(); - if (!/^[0-9a-f:]+$/.test(s) || s.split("::").length > 2) return null; - const [left, right = ""] = s.split("::"); - const l = left ? left.split(":") : [], r = right ? right.split(":") : []; - if (s.includes("::") ? l.length + r.length > 7 : l.length !== 8) return null; - const groups = [...l, ...Array(8 - l.length - r.length).fill("0"), ...r]; - let v = 0n; - for (const g of groups) { - if (!/^[0-9a-f]{1,4}$/.test(g)) return null; - v = (v << 16n) | BigInt(parseInt(g, 16)); - } - return v; - } - - function groupsOf(v) { - const g = []; - for (let i = 7; i >= 0; i--) g.push(Number((v >> BigInt(i * 16)) & 0xffffn)); - return g; - } - - // Shortest form, RFC 5952 style. - function format(v) { - const g = groupsOf(v).map((x) => x.toString(16)); - let best = -1, bestLen = 0; - for (let i = 0; i < 8; i++) { - let n = 0; - while (i + n < 8 && g[i + n] === "0") n++; - if (n > bestLen && n > 1) { best = i; bestLen = n; } - } - if (best < 0) return g.join(":"); - return g.slice(0, best).join(":") + "::" + g.slice(best + bestLen).join(":"); - } - - const sup = (k) => String(k).replace(/\d/g, (d) => "⁰¹²³⁴⁵⁶⁷⁸⁹"[d]); - // 2^k as a number people can read: exact up to 2^50, else a power. - const pow2 = (k) => (k <= 50 ? Number(1n << BigInt(k)).toLocaleString("en") : `2${sup(k)}`); - - function render(form) { - const input = form.elements.prefix, slider = form.elements.len; - const bits = form.querySelector(".px-bits"), range = form.querySelector(".px-range"), facts = form.querySelector(".px-facts"); - let [addrText, lenText] = input.value.split("/"); - let len = lenText === undefined ? Number(slider.value) : parseInt(lenText, 10); - const addr = parse(addrText || ""); - if (addr === null || !(len >= 0 && len <= 128)) { - bits.innerHTML = 'not an IPv6 prefix: write address/length, e.g. 2001:db8::/32'; - range.innerHTML = ""; facts.textContent = ""; - return; - } - slider.value = len; - const host = 128 - len; - const first = addr & ((ALL << BigInt(host)) & ALL); - const last = first | ((1n << BigInt(host)) - 1n); - - // The 32 hex digits with the fixed part in the accent colour; a nibble - // that straddles the boundary is shown in binary. - const hex = groupsOf(first).map((x) => x.toString(16).padStart(4, "0")).join(""); - const fullNibbles = Math.floor(len / 4), partBits = len % 4; - const withColons = (s, offset) => s.replace(/./g, (c, i) => ((offset + i) % 4 === 3 && offset + i < 31 ? c + ":" : c)); - let html = `${withColons(hex.slice(0, fullNibbles), 0)}`; - let rest = fullNibbles; - if (partBits) { - const nib = parseInt(hex[fullNibbles], 16).toString(2).padStart(4, "0"); - // One hex digit, four bits, with the boundary inside it. - html += `${nib.slice(0, partBits)}${"x".repeat(4 - partBits)}`; - rest++; - } - html += `${withColons("x".repeat(32 - rest), rest)}`; - bits.innerHTML = html; - - // Every shorter prefix contains this one; show three of them, at byte - // boundaries, with the range's position and share marked in each, so - // that no single one looks like "the" parent. Then the range itself. - const levels = []; - for (let pl = len === 0 ? -1 : 8 * Math.floor((len - 1) / 8); pl >= 0 && levels.length < 3; pl -= 8) { - const slots = 1n << BigInt(len - pl); - const index = (first >> BigInt(host)) & (slots - 1n); - const enclosing = first & ((ALL << BigInt(128 - pl)) & ALL); - // Position and width as fractions; the slice is kept visible. - const left = Number((index * 100000n) / slots) / 1000, width = Math.max(100 / Number(slots), 0.3); - levels.push(`
${esc(format(enclosing))}/${pl} - - 1 of ${pow2(len - pl)}
`); - } - range.innerHTML = ` -
${levels.length ? `
every shorter prefix contains it; three of them:
${levels.join("")}` : '
nothing is shorter than /0: this is every address there is
'}
-
${esc(format(first))}${pow2(host)} addresses${esc(format(last))}
`; - - const parts = [`The first ${len} bits are fixed; the other ${host} are for use within the network.`]; - if (len < 64) parts.push(`That is ${pow2(64 - len)} /64s, one per LAN.`); - else if (len === 64) parts.push(`Exactly one LAN.`); - else parts.push(`Smaller than a LAN: ${host === 0 ? "a single host" : `part of one /64`}.`); - if (len < 48) parts.push(`Or ${pow2(48 - len)} /48s, one per site.`); - facts.innerHTML = parts.join(" "); - } - - document.addEventListener("input", (e) => { - const form = e.target.closest && e.target.closest("form.prefix-explorer"); - if (!form) return; - if (e.target.name === "len") { - // The slider rewrites the length in the text field. - const addr = form.elements.prefix.value.split("/")[0]; - form.elements.prefix.value = `${addr}/${e.target.value}`; - } - render(form); - if (window.deck) window.deck.layout(); - }); - document.addEventListener("submit", (e) => { - if (e.target.closest && e.target.closest("form.prefix-explorer")) e.preventDefault(); - }); - // Fill in the initial values once the slides exist. - const init = () => document.querySelectorAll("form.prefix-explorer").forEach(render); - window.addEventListener("deck-ready", init); - if (document.readyState === "complete") setTimeout(init, 500); -})(); diff --git a/slides/sections/00-intro.md b/slides/sections/00-intro.md deleted file mode 100644 index 56522ba..0000000 --- a/slides/sections/00-intro.md +++ /dev/null @@ -1,15 +0,0 @@ -## About the speaker - -**Rayhaan Jaufeerally** - -- Software engineer at a hyperscaler, working on security and reliability. -- Studied at ETH from 2016 to 2019, and took the network security course. -- Has been running **AS210036** since 2018, for personal use and research - -

Today we'll get to use some connectivity through this network.

- -Notes: - -- Working at a large company with some offices near the main train station -- Former ETH masters student and took the network security course -- After my degree I signed up to be a LIR at RIPE, and got my own Autonomous System and started peering, for the fun of running my own network. diff --git a/slides/sections/01-resources.md b/slides/sections/01-resources.md deleted file mode 100644 index 9a140f4..0000000 --- a/slides/sections/01-resources.md +++ /dev/null @@ -1,170 +0,0 @@ -# Internet Numbering Resources - -Notes: -Before we look at what's going on in the internet, let's refresh on how the Internet is even addressed. - ---- - -## Packets are sent to IP addresses - - - -
Ask a resolver which IPv6 address this name stands for.
- - -IP packets are sent to IP addresses, not domain names. Every time you use a domain name, it is resolved into an IP address via DNS. - - - -Notes: -Live: type a few names the students use. ethz.ch has an AAAA record; -github.com has none (only A), which shows that the two address families -are separate. The answer comes from Google Public DNS over HTTPS -(dns.google), so it needs the lecture-hall network. - ---- - -## Who does the address belong to? - - - -
Paste the address from the last slide.
- - -The **inet6num** says who was assigned the block; the **route6** says which AS is allowed to originate it. - - - -Notes: -Paste the AAAA answer from the previous slide. ETH's /48 is ASSIGNED PI -(provider independent, sponsored by SWITCH) and its route6 names AS559, -SWITCH. Try 2a0d:d741:1::1 as well: the lab prefix, origin AS210036. The -box takes AS numbers too (AS559). Same source as whois -h whois.ripe.net, -over the database's REST API. - ---- - -## An IPv6 address is 128 bits - -```text -2001:0db8:0001:0000:0000:0000:0000:0001 eight groups of 16 bits, in hex -2001:db8:1:0:0:0:0:1 drop leading zeros in a group -2001:db8:1::1 one run of zero groups becomes :: -``` - -`::` may appear once, or the address would be ambiguous. `2001:db8::/32` is reserved for documentation, so it never appears on the real Internet. - - - -Notes: -Ask: why only once? Because 2001::1::1 could put the zeros in several places. -2001:db8::/32 is RFC 3849: set aside for examples, never routed. - ---- - -## A prefix is a range of addresses - -In BGP, the control plane for the internet, it's not viable to talk about single addresses, so we use prefix ranges. - - - -

-
-

- - -Notes: -Live. Drag the slider: the fixed part grows and the range shrinks by half per -bit. Try /47 or /50 to see a hex digit split in two. The three rows above the -bar are three of the shorter prefixes that contain it, with its position and -share in each: there is no such thing as "the" parent prefix, only whatever -someone chose to allocate from. -2^80 addresses in one /48, about 1.2 million billion billion. The /n is a -count of leading bits, nothing more. - ---- - -## The global IPv6 address space - - - -::/0every IPv6 address there is: 2¹²⁸ of them - -2000::/3global unicast: the eighth of the space that is routed on the Internet - -2001:db8::/32one provider's allocation (this one is reserved for documentation) - -2001:db8:1:2::/64one LAN: 2⁶⁴ addresses for its hosts -::1 -::2 -::3 -… -::ffff:ffff:ffff:ffff -not to scale - - -Notes: -Not to scale: a /64 is 2^-64 of the space. Only the first ring matters for -routing; the others are how the space is handed down, which is next. - ---- - -## Who hands out the addresses - - -IANA -the root: one pool for the whole world - -a /12 to each region -AFRINIC -APNIC -ARIN -LACNIC -RIPE NCC -RIRs: five regional registries, one per part of the world - -/29 and up to a member -SWITCH -an ISP -LIRs: members of an RIR, usually providers - -a /48 to a site -ETH Zürich -its customers -end users: companies, universities, you - - -Notes: - -- The Internet Assigned Numbers Authority comes up with allocations of IP - addresses from the global space -- Blocks are assigned to Regional Internet Registries (RIRs), which allocate - them to Local Internet Registries (LIRs), who assign to end users -- The RIPE lookup two slides back showed exactly this chain: ETH's /48 is - ASSIGNED PI, sponsored by SWITCH (the LIR), registered at RIPE NCC (the - RIR). Nobody owns addresses; they are registered to you. - ---- - -## AS numbers - -- Used to identify networks under a single oeprator, and required for exchanging BGP traffic -- Managed by IANA and the RIRs -- 16 bits at first: `1` to `65535` -- 32 bits since RFC 6793: up to `4294967295` -- In order to have a smooth transition, `AS23456` is used as a placeholder when the AS number is > 16 bits -- 32 bit AS numbers are then exchanged using a BGP capability message to be backwards compatible -- Private use: `64512–65534` and `4200000000–4294967294` - -AS numbers come down the same chain: IANA to the RIRs, an RIR to whoever runs a network. The lab daemon is `AS4200000001`, a private one, stripped before the Internet sees your route. - - - -Notes: -Nobody "owns" numbers or addresses; they are registered to you. RFC 6996 -reserves the private ranges. The upstream router removes the private AS from -the path, so the origin everybody else sees is AS210036. - ---- - -## Handout Part 1 diff --git a/slides/sections/03-bgp-update.md b/slides/sections/03-bgp-update.md deleted file mode 100644 index fd70726..0000000 --- a/slides/sections/03-bgp-update.md +++ /dev/null @@ -1,329 +0,0 @@ -# BGP in detail - -Notes: -We're going to look at some implementation details of the protocol - ---- - -## - -- Runs over TCP, port `179`, and is exchanged typically between peering routers -- Each side tells the other its best route per prefix -- Only _changes_ are sent: no periodic refresh -- If the session drops, after a timeout the routes learned over that session are dropped - -Notes: -BGP is a very old protocol, and a lot of it's design predates the internet being something with adversarial participants. -Security and extensions have had to be bolted on, in perhaps strange ways. - ---- - -## The four message types - -| Type | Name | Says | -| ---- | ------------ | ------------------------------------------------------- | -| 1 | OPEN | AS number, router ID, capabilities | -| 2 | **UPDATE** | these prefixes are reachable, those no longer are | -| 3 | NOTIFICATION | errors and informational messages: e.g. rebooting | -| 4 | KEEPALIVE | Heartbeat to keep the session up when there's no update | - -OPEN, OPEN, KEEPALIVE, KEEPALIVE, and then a stream of UPDATEs. - - - -Notes: -There is actually a fifth type, ROUTE-REFRESH: "send me everything again, my policy changed". - ---- - -## OPEN: a handshake between peers. - - -header, the same on every message: 19 bytes - -marker -16 bytes, all ff - -length -2 bytes - -type -1 byte -OPEN: the fixed part - -version -1 byte: 4 - -my AS -2 bytes, or 23456 - -hold time -2 bytes, seconds - -BGP identifier -4 bytes, the router ID - -opt. length -1 byte - - -optional parameters: today always one, of type 2 = capabilities, each a (code, length, value) - -type 2 -capabilities - -length -1 byte - -IPv6 unicast -code 1: AFI 2, SAFI 1 - -4-byte AS -code 65: 210036 - -route refresh -code 2, empty - -… -more capabilities - - -Notes: -Sent once, by each side, as soon as the TCP connection is up. The two sides -must agree: the AS must be the one configured for the neighbour, the hold -time becomes the smaller of the two, and only capabilities both announced are -used. A 4-byte AS does not fit in "my AS", so that field carries 23456 and the -real number rides in a capability, which is how our AS210036 arrives. - ---- - -## BGP Open message - -
-
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 37 01 04 5b a0 00 f0 0a ff 00 01 1a 02 18 01 04 00 02 00 01 02 00 06 00 40 02 00 00 41 04 00 03 34 74 46 00 47 00
-
-
BGP header: 19 bytes, on every message
-
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ffmarkersixteen bytes of ff.
-
00 37length0x0037 = 55 bytes, header included. At most 4096, or 65535 with the extended-message capability
-
01type1 = OPEN (2 UPDATE, 3 NOTIFICATION, 4 KEEPALIVE, 5 ROUTE-REFRESH)
-
OPEN: the fixed part, 10 bytes
-
04versionBGP version 4
-
5b a0my AS0x5ba0 = 23456, AS_TRANS: the ASN is longer than 16 bits so is encoded in a capability instead
-
00 f0hold time0x00f0 = 240 seconds. both peers use the minimum announced; a KEEPALIVE typically is sent every 1/3rd of this duration
-
0a ff 00 01BGP identifier0a ff 00 01 = 10.255.0.1, the router ID. Looks like an IPv4 address, just needs to be a unique identifier.
-
1aoptional parameters length0x1a = 26 bytes follow
-
Optional parameter: 2 + 24 bytes
-
02parameter type2 = capabilities
-
18parameter length0x18 = 24 bytes of capabilities follow, each as type, length, value
-
Capability 1: multiprotocol (RFC 4760)
-
01code1 = multiprotocol extensions
-
04length4
-
00 02AFI0x0002 = IPv6. Address Family Identifier
-
00reserved0
-
01SAFI1 = unicast. Subsequent Address Family Identifier.
-
Capability 2: route refresh (RFC 2918)
-
02code2 = route refresh: “you may ask me to resend everything”
-
00length0
-
Capability 6: extended message (RFC 8654)
-
06code6 = messages up to 65535 bytes are supported
-
00length0
-
Capability 64: graceful restart (RFC 4724)
-
40code0x40 = 64, graceful restart
-
02length2
-
00 00flags and restart time0x0000: not restarting, restart time 0, and no address families listed.
-
Capability 65: four-octet AS (RFC 6793)
-
41code0x41 = 65, four-octet AS number
-
04length4
-
00 03 34 74AS number00 03 34 74 = 210036: the real AS. Included here since it doesn't fit in the header
-
Capability 70: enhanced route refresh (RFC 7313)
-
46code0x46 = 70
-
00length0
-
Capability 71: long-lived graceful restart
-
47code0x47 = 71
-
00length0, no address families listed
-
-
- -Notes: -What BIRD on our upstream router sends the lab daemon, byte for byte. Hold -time 240 is BIRD's default; keepalives go every third of it. - ---- - -## Update message format - - -header: 19 bytes - -marker -16 bytes, all ff - -length -2 bytes - -type -1 byte -UPDATE body: two lengths, three variable parts - -withdrawn length -2 bytes - -withdrawn routes -IPv4 prefixes gone - -attributes length -2 bytes - -path attributes -everything about the route - -NLRI -IPv4 prefixes, new - - -one path attribute; repeated for multiple - -flags -1 byte - -type -1 byte: ORIGIN = 1, AS_PATH = 2 … - -length -1 or 2 bytes - -value -as many bytes as it said - -one prefix, in NLRI and in withdrawn routes: a bit count, then only the bytes that count needs - -length -1 byte: 24 for a /24 - -prefix -3 bytes for a /24, 6 for a /48 … - - -Notes: -One UPDATE carries one set of attributes, and every prefix that shares them. -The withdrawn and NLRI fields can only hold IPv4; the next slides show where -IPv6 goes. Lengths everywhere: a receiver can walk the message without -understanding any of it, which is what lets unknown attributes pass through. - ---- - -## An example BGP update - -
headerlengthspath attributesNLRI
- -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -`2a0d:d740::/48`, as AS210036 announces it to the SwissIX route servers. - - - -Notes: -Built with the lab daemon's own encoder and checked by parsing it back, with -the attributes pr01 puts on routes it exports to peers. Press down to walk -through it part by part. Note there is nothing yellow: the NLRI field is -empty. Hold that thought. - ---- - -## Header - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
ff × 16Start of message marker.
00 59length of the whole message: 89 bytes (maximum 4096)
02type 2 = UPDATE
- ---- - -## Two lengths - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
00 000 bytes of withdrawn routes: nothing is being taken back
00 4266 bytes of path attributes follow
89 − 19 − 2 − 2 − 66 = 0 bytes left over for NLRI
- ---- - -## ORIGIN - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
40flags: well-known, transitive
01type 1 = ORIGIN
011 byte of value
00IGP: the route was originated inside this AS
- -Every attribute is _flags, type, length, value_. - - - ---- - -## AS_PATH - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
40 02 06well-known, transitive; type 2; 6 bytes
02an AS_SEQUENCE, order matters
01of 1 AS number
00 03 34 74210036, the origin. Four bytes each (RFC 6793)
- -Every BGP router prepends its own AS number before announcing a route to an external peer, creating a map of which networks the announcement travelled through. This allows selecting the shortest path. - - - -Notes: -One entry, because SwissIX is one hop from the origin and the route server -does not add itself. Seen from further away it reads e.g. 6939 210036. - ---- - -## MULTI_EXIT_DISC - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
80 04 04optional, non-transitive; type 4; 4 bytes
00 00 00 00MED 0. Lower is preferred, among routes from the same neighbour AS
- -A hint to a neighbour about which path you prefer traffic to come in on. Non-transitive: it goes one hop and no further. - - - ---- - -## MP_REACH_NLRI: announcing paths for non-IPv4 protocols - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
80 0e 1coptional, non-transitive; type 14; 28 bytes
00 02 · 01address family IPv6, unicast
10 · 2001 07f8 0024 … 00c9next hop, 16 bytes: 2001:7f8:24::c9, our port on the SwissIX peering LAN
00reserved
30 · 2a 0d d7 40 00 0048 bits of prefix: 2a0d:d740::/48
- -Notes: -BGP-4 was built for IPv4: its NLRI field cannot carry anything else. RFC 4760 -bolts other address families on as an attribute, each with its own next hop. -A prefix is sent as a bit count plus only as many bytes as the count needs. -The next hop is an address on the exchange's shared LAN: whoever receives this -route sends packets straight to that address across the switch. Remember ::c9 -for the exchange diagram in the next section. -Routers may also append their link-local address here, making it 32 bytes. - ---- - -## LARGE_COMMUNITIES - -
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 59 02 00 00 00 42 40 01 01 00 40 02 06 02 01 00 03 34 74 80 04 04 00 00 00 00 80 0e 1c 00 02 01 10 20 01 07 f8 00 24 00 00 00 00 00 00 00 00 00 c9 00 30 2a 0d d7 40 00 00 c0 20 0c 00 03 34 74 00 00 00 0a 00 00 00 02
- -
c0 20 0coptional, transitive; type 32; 12 bytes = one community
00033474 · 0000000a · 00000002210036:10:2, a tag our router puts on everything it exports to peers
- -Three 32-bit numbers. They mean whatever the network named in the first one says they mean. - - - ---- - -## The flags byte - -| Bit | Set means | Clear means | -| ------ | ------------------------------------------------- | ------------------------------------------------ | -| `0x80` | optional | well-known: every BGP speaker must understand it | -| `0x40` | transitive: pass it on, even if not understood | drop it at the next hop | -| `0x20` | partial: someone on the way did not understand it | | -| `0x10` | length field is 2 bytes | 1 byte | - -Path attribute transitivity allows adding instructions for networks far away from direct neighbours. - - diff --git a/slides/sections/04-transit-and-ix.md b/slides/sections/04-transit-and-ix.md deleted file mode 100644 index fdc4820..0000000 --- a/slides/sections/04-transit-and-ix.md +++ /dev/null @@ -1,105 +0,0 @@ -# Transit and exchanges - -Notes: -Section 4. Two very different ways of getting routes, and what an Internet -exchange physically is. - ---- - -## Two ways to reach other networks - -
-
- -### IP transit - -- You **pay** a provider -- They give you routes to _everyone_ -- They announce you to _everyone_ - -
-
- -### Peering - -- Usually **free**, by mutual benefit -- You exchange _your own_ routes, and your customers' -- Traffic between the locally connected parties only - -
-
- -Notes: -Transit is a customer-provider relationship; peering is between equals. A peer -does not carry your traffic to third parties. Most networks have both: transit -as the fallback, peering to keep traffic cheap and close. - ---- - -## An exchange is a big Ethernet switch - - - -peering LAN: one Ethernet segment, one subnet -SwissIX 2001:7f8:24::/64 - -AS210036::c9 -AS559 SWITCH::35 -AS2906 Netflix::55 -AS13335 Cloudflare::c0 - - -route serversAS42476 ::a5ec:a:1 - - - - -traffic -BGP - - -Notes: -Every member plugs one router port into the same layer-2 network and gets one -address in a shared subnet. No routing inside the exchange: just Ethernet. -Dashed blue is a BGP session, green is where the packets actually go. - ---- - -## What that implies - -- Peering participants in an exchange are all one hop away, found by neighbour discovery -- A route's next hop is the peer's LAN address, so traffic goes **directly** member to member -- The exchange is a simple layer 2 switch, allowing for a large capacity at low cost -- Strict(ish) rules on what traffic is allowed on the peering lan (e.g. no DHCP or broadcast packets) - -Notes: - -- IPv6 neighbour discovery and ARP are used to discover the MAC addresses of peers -- after which traffic can flow directly - ---- - -## Route servers - -Are a piece of shared infrastructure that simplifies peering. - -- Most participants peer with the route server and annouce their prefixes there -- The route server can then distribute these routes to every other participant -- Simplifes from `n²` to `n` sessions -- Not in the traffic path and does not add its own AS into the path -- Allows controlling behaviour with communities, e.g. `42476:0:` "not to ``", `42476:1:` "only to ``" - -Notes: -A route server is a BGP speaker that only redistributes. Because it is -transparent, the AS path looks exactly as if you peered with each member -directly. We will use those communities in the lab. - ---- - -## Looking glasses - -Are a useful tool to inspect the state of another network or internet exchange - -- SwissIX https://lg.swissix.ch/ -- NL-ix https://lg.nl-ix.net/ -- BGP tools https://bgp.tools/ diff --git a/slides/sections/05-lab.md b/slides/sections/05-lab.md deleted file mode 100644 index 49561ef..0000000 --- a/slides/sections/05-lab.md +++ /dev/null @@ -1,90 +0,0 @@ -# The lab - -### bgplab.rayhaan.net - -Notes: -Section 5. What the students are about to touch, and what stands between a -typo and the Internet. - ---- - -## What you are connected to - - -youa browser -lab speakerAS4200000001 -pr01.rue (BIRD)AS210036 -HTTPS -BGP -web page + BGP speakerfilters, then decides where -SwissIX -NL-ix -France-IX -DE-CIX -transit - - - - - -Notes: -The lab speaker is a small BGP daemon written for this course; it runs on -the same machine as the router. Both dashed hops are real BGP sessions. Everything to -the right of pr01 is the actual Internet. - ---- - -## Your prefix - -`2a0d:d741:::/48` - -- `` is your number **in hex**: student 10 is `2a0d:d741:a::/48` -- Put your name in _owner_: only the owner can withdraw a route -- One prefix each - -Notes: -Put the URL and the class password on the board. The page is a snapshot by -default; turn on auto-refresh at the top right to watch things change. - ---- - -## What happens when you press _Announce_ - -1. The lab speaker builds an UPDATE. Click _bytes_ to see it, hover to decode it -2. pr01 checks it against its import filter -3. pr01 decides, per neighbour, whether to pass it on -4. Your announcement propagates throughout the whole internet! -5. `ping 2a0d:d741:::1` answers, from anywhere - -Notes: -Step 5: pr01 answers for every address in the lab space, so reachability of -your prefix depends only on whether your announcement got out. - ---- - -## Steering your route - -| You add | Who acts | Effect | -| ----------------------------- | -------- | --------------------------------- | -| nothing | | announced everywhere | -| `210036:0:0` | pr01 | announced nowhere | -| `210036:0:0` `210036:1:42476` | pr01 | only to the SwissIX route servers | -| + `42476:0:0` `42476:1:559` | SwissIX | …and from there only to SWITCH | -| `NO_EXPORT` | pr01 | accepted, never passed on | - -Same idea at two hops: _never there_, _only there_, _nowhere_. - - - -Notes: -210036:0: never to that AS, 210036:1: to that AS, 210036:2/3: the -same by exchange (1 SwissIX, 2 NL-ix, 3 France-IX, 4 DE-CIX). A "never" always -wins. The preset menu next to the field has all of these. - ---- - -## Handout Part 2 & 3 - -Notes: -Looking glasses: bgp.tools, RIPEstat, and the exchange's own. Global -visibility takes a minute or two to settle after each change. diff --git a/slides/serve.py b/slides/serve.py deleted file mode 100644 index f19ea63..0000000 --- a/slides/serve.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -"""Serve the deck for editing: like `python3 -m http.server`, but every -response says Cache-Control: no-store, so a plain reload always shows the -files as they are on disk. (The stock server sends Last-Modified and nothing -else, and browsers then keep the section files and the CSS for a while.) - - python3 slides/serve.py # http://localhost:8765 - python3 slides/serve.py 8000 -""" -import os -import sys -from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer - - -class NoCacheHandler(SimpleHTTPRequestHandler): - def end_headers(self): - self.send_header("Cache-Control", "no-store") - super().end_headers() - - def log_message(self, fmt, *args): - pass # quiet; errors still go through log_error - - -if __name__ == "__main__": - port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765 - os.chdir(os.path.dirname(os.path.abspath(__file__))) - print(f"serving {os.getcwd()} on http://localhost:{port}/") - ThreadingHTTPServer(("", port), NoCacheHandler).serve_forever() diff --git a/slides/style.css b/slides/style.css deleted file mode 100644 index cc2e9be..0000000 --- a/slides/style.css +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Components and helpers used by the slides. The look (palette, type, panels, - * motif) is in theme.css; nothing here sets a brand colour directly. - */ - -.hidden { - display: none; -} - -/* ---- text -------------------------------------------------------------------- */ - -/* A quiet aside under the main point. */ -.reveal .note { - font-style: italic; - font-size: 75%; - color: var(--muted); -} - -/* Image and quote credits: */ -.reveal .attribution { - font-size: 45%; - font-style: italic; - color: var(--muted); -} - -/* One big statement on an otherwise empty slide. */ -.reveal .big { - font-size: 1.9em; - font-weight: 300; - line-height: 1.25; - color: var(--ink-strong); -} - -/* ---- layout ------------------------------------------------------------------ */ - -.container { - display: flex; - justify-content: center; - align-items: center; - gap: 1em; -} - -.flex-around { - display: flex; - justify-content: space-around; - align-items: flex-start; - gap: 1em; -} - -/* Two equal columns:
…
…
*/ -.cols { - display: flex; - gap: 1.2em; - align-items: center; - text-align: left; -} - -.cols > * { - flex: 1 1 0; - min-width: 0; -} - -.float-left { - float: left; -} - -.float-right { - float: right; -} - -.no-border { - background: none !important; - border: none !important; - box-shadow: none !important; -} - -.reveal img.blur-edges { - border-radius: 12px; - box-shadow: 0 0 25px 8px rgba(20, 30, 60, 0.2); -} - -.reveal table.borders td, -.reveal table.borders th { - border: 1px solid var(--rule); -} - -/* ---- boxes: small labelled cards, e.g. one per AS or per community ----------- */ - -.boxes { - display: flex; - flex-wrap: wrap; - justify-content: center; - font-size: 70%; - margin: 0.6em 0; -} - -.boxes > div { - padding: 0.35em 0.8em; - margin: 0.3em 0.5em; - border: 1px solid var(--rule); - border-radius: 10px; - background: var(--ground); - line-height: 1.35; -} - -.boxes > div.us { - border-color: var(--accent); - box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.35); - color: var(--ink-strong); -} - -.boxes > div.ok { - border-color: #1a8a4f; -} - -.boxes > div.bad { - border-color: #d64545; -} - -/* ---- BGP bytes, matching the four groups of the /statusz bytes view ---------- */ - -.reveal pre.bytes { - font-size: 0.5em; - line-height: 1.9; - white-space: pre-wrap; - word-break: break-all; - background: var(--code-ground); - border-radius: 6px; - padding: 15px 20px; - box-sizing: border-box; - color: var(--code-ink); -} - -.bytes span, -.legend span { - border-radius: 3px; - padding: 1px 3px; - color: var(--code-ink); /* the fills are dark on any ground */ -} - -.bytes .hh, -.legend .hh { background: #34406e; } -.bytes .hl, -.legend .hl { background: #6e3440; } -.bytes .ha, -.legend .ha { background: #245c3a; } -.bytes .ha2 { background: #2f7a4b; } -.bytes .hn, -.legend .hn { background: #6b5a1f; } - -.legend { - font-size: 50%; - font-family: var(--r-code-font); -} - -.legend span { - padding: 1px 0.6em; - margin: 0 0.2em; -} - -/* ---- live DNS and RIPE database lookups ------------------------------------ */ - -.reveal form.dns-lookup, -.reveal form.ripe-lookup { - margin: 0.4em auto 0; - max-width: 34em; - text-align: left; - font-family: var(--r-code-font); -} - -.reveal .dns-lookup input, -.reveal .ripe-lookup input { - width: 62%; - padding: 0.3em 0.5em; - font: inherit; - font-size: 0.8em; - color: var(--r-main-color); - background: var(--panel); - border: 1px solid var(--rule); - border-radius: 6px; - outline: none; -} - -.reveal .dns-lookup input:focus, -.reveal .ripe-lookup input:focus { - border-color: var(--accent); -} - -.reveal .dns-lookup button, -.reveal .ripe-lookup button { - margin-left: 0.4em; - padding: 0.3em 0.9em; - font: inherit; - font-size: 0.8em; - color: #fff; - background: var(--accent); - border: 0; - border-radius: 6px; - cursor: pointer; -} - -.reveal .dns-result { - min-height: 3.2em; - max-height: 13em; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--rule) transparent; - margin-top: 0.5em; - padding: 0.5em 0.7em; - font-size: 0.7em; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-all; - background: var(--panel); - border: 1px solid var(--rule); - border-radius: 6px; -} - -.reveal .dns-result .ttl, -.reveal .dns-result .type, -.reveal .dns-result .dim { color: var(--muted); } -.reveal .dns-result .addr { color: var(--accent); font-weight: 500; } -.reveal .dns-result .bad { color: #c62828; } - -/* RIPE database objects, laid out like apps.db.ripe.net: a light page with - one card per object, one row per attribute, references in blue, rows the - RIPE NCC manages shaded. */ -.reveal .ripe-result { - /* A fixed height, not a maximum: the box must not grow when the answer - arrives, or the slide overflows (reveal centres a slide only when it - is shown). */ - height: 13.5em; - margin-top: 0.5em; - padding: 0.5em; - overflow-y: auto; - scrollbar-width: thin; - font-size: 0.62em; - text-align: left; - background: #f4f5f7; - border-radius: 6px; -} - -.reveal .ripe-card { - margin-bottom: 0.7em; - padding: 0.2em 0.5em; - background: #fff; - border-radius: 6px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); -} - -.reveal .ripe-card:last-child { margin-bottom: 0; } - -.reveal .ripe-card table { - width: 100%; - margin: 0; - border-collapse: collapse; - font-family: var(--r-code-font); - font-size: inherit; - color: #1c1c28; -} - -.reveal .ripe-card th, -.reveal .ripe-card td { - padding: 0.28em 0.6em; - border: 0; - font-weight: normal; - text-align: left; - vertical-align: top; - background: transparent; - line-height: 1.4; -} - -.reveal .ripe-card table th { - width: 8.5em; - color: #1c1c28; - white-space: nowrap; - text-decoration: underline dotted #9a9aa6; - text-underline-offset: 0.2em; -} - -.reveal .ripe-card td { word-break: break-all; } -.reveal .ripe-card tr.managed th, -.reveal .ripe-card tr.managed td { background: #dde5f0; } -.reveal .ripe-card .ref { color: #2f6fe4; } -.reveal .ripe-card .comment { margin-left: 0.4em; color: #6b6b78; } - -.reveal .ripe-msg { - padding: 0.6em; - font-family: var(--r-code-font); - color: #1c1c28; -} - -/* A framed live page on a slide. */ -.reveal iframe.live { - border: 1px solid var(--rule); - border-radius: 8px; - background: #fff; -} - -/* ---- bytes with one part in focus: everything not .on is dimmed --------------- */ - -.bytes.focus span:not(.on) { - opacity: 0.22; -} - -.bytes.focus span.on { - box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.9); -} - -/* Packet layouts: field boxes in the colours of the bytes view. */ -.diagram .pkt rect { stroke: none; } -.diagram .pkt.hh rect { fill: #34406e; } -.diagram .pkt.hl rect { fill: #6e3440; } -.diagram .pkt.ha rect { fill: #245c3a; } -.diagram .pkt.ha2 rect { fill: #2f7a4b; } -.diagram .pkt.hn rect { fill: #6b5a1f; } -.diagram text.pkt-name { font-size: 18px; font-weight: 500; fill: #fff; } -.diagram text.pkt-size { font-size: 13px; font-family: "IBM Plex Mono", monospace; fill: #fff; opacity: 0.8; } -.diagram text.pkt-title { paint-order: stroke; stroke: var(--panel); stroke-width: 8px; stroke-linejoin: round; } -.diagram .pkt-expand { stroke: var(--muted); stroke-width: 1.5; stroke-dasharray: 4 5; fill: none; } - -/* ---- byte walk: a hex dump and one row per field, in step on hover --------- */ - -.reveal .bytewalk { text-align: left; } -.reveal .bytewalk .bw-dump { margin-bottom: 8px; } -/* The lit bytes invert: a white block on the dark dump, visible from the back row. */ -.reveal .bw-dump span.on { background: #fff; color: var(--code-ground); box-shadow: 0 0 0 3px var(--accent); } - -.reveal .bw-rows { - position: relative; /* rows measure their offsetTop against the box */ - height: 19em; - overflow-y: auto; - scrollbar-width: thin; - font-size: 46%; - line-height: 1.45; - border: 1px solid var(--rule); - border-radius: 6px; - background: rgba(var(--accent-rgb), 0.04); -} - -.reveal .bw-group { - padding: 0.45em 0.8em 0.2em; - color: var(--accent); - font-weight: 700; -} - -.reveal .bw-row { - display: grid; - grid-template-columns: 11em 12em 1fr; - gap: 0 1em; - padding: 0.18em 0.8em; - cursor: default; - outline: none; -} - -.reveal .bw-row:hover, -.reveal .bw-row:focus, -.reveal .bw-row.on { background: rgba(var(--accent-rgb), 0.14); } -.reveal .bw-dump span[data-i] { cursor: pointer; } -.reveal .bw-row code.hh { background: rgba(52, 64, 110, 0.16); } -.reveal .bw-row code.hl { background: rgba(110, 52, 64, 0.16); } -.reveal .bw-row code.ha { background: rgba(36, 92, 58, 0.16); } -.reveal .bw-row code.ha2 { background: rgba(47, 122, 75, 0.16); } -.reveal .bw-row code.hn { background: rgba(107, 90, 31, 0.16); } -.reveal .bw-row code { display: block; color: var(--ink-strong); min-width: 0; max-width: 100%; justify-self: start; align-self: start; font-family: var(--r-code-font); border-radius: 3px; padding: 0 4px; white-space: normal; word-break: normal; } -.reveal .bw-row b { color: var(--ink-strong); font-weight: 500; } -.reveal .bw-row span { color: var(--ink); } - -/* Small two-column explanation under a bytes block. */ -.reveal table.decode { - font-size: 58%; - margin-top: 10px; -} - -.reveal table.decode td:first-child { - font-family: var(--r-code-font); - color: var(--accent-soft); - white-space: nowrap; -} - -/* ---- prefix explorer: type a prefix, see its range ------------------------ */ - -.reveal form.prefix-explorer { - margin: 0 auto; - max-width: 36em; - text-align: center; -} - -.reveal .prefix-explorer input[name="prefix"] { - width: 15em; - padding: 0.25em 0.5em; - font: inherit; - font-family: var(--r-code-font); - font-size: 0.8em; - color: var(--r-main-color); - background: var(--panel); - border: 1px solid var(--rule); - border-radius: 6px; - outline: none; - text-align: center; -} - -.reveal .prefix-explorer input[name="prefix"]:focus { border-color: var(--accent); } - -.reveal .prefix-explorer input[name="len"] { - width: 12em; - margin-left: 1em; - vertical-align: middle; - accent-color: var(--accent); -} - -.reveal .prefix-explorer .px-bits { margin: 0.5em 0 0.4em; min-height: 1.6em; white-space: nowrap; } -.reveal .prefix-explorer .px-bad { color: #c62828; font-size: 70%; } - -.reveal .px-range { font-family: var(--r-code-font); font-size: 50%; min-height: 10.5em; } - -.reveal .px-levels { text-align: left; margin-bottom: 1em; } -.reveal .px-levels-title { color: var(--muted); margin-bottom: 0.35em; } -.reveal .px-level { display: flex; align-items: center; gap: 0.7em; margin-bottom: 0.3em; } -.reveal .px-level-name { width: 13em; text-align: right; color: var(--accent-soft); white-space: nowrap; } -.reveal .px-level-count { width: 8em; color: var(--muted); white-space: nowrap; } -.reveal .px-track { flex: 1; position: relative; height: 0.9em; background: var(--ground); border: 1px solid var(--rule); border-radius: 2px; } -.reveal .px-track i { position: absolute; top: 0; bottom: 0; min-width: 2px; background: var(--accent); } - -.reveal .px-bar { - position: relative; - height: 2.6em; - background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.45), rgba(var(--accent-rgb), 0.12)); - border: 1px solid var(--accent); - border-radius: 6px; -} -.reveal .px-bar .px-mid { - position: absolute; left: 0; right: 0; top: 0.6em; - text-align: center; color: var(--ink-strong); font-weight: 500; -} -.reveal .px-bar .px-end { position: absolute; top: 3em; color: var(--accent-soft); } -.reveal .px-bar .px-first { left: 0; } -.reveal .px-bar .px-last { right: 0; } - -.reveal .prefix-explorer .px-facts { margin-top: 2.6em; } - -/* ---- bits of an address: fixed part vs. free part ------------------------------ */ - -.bits { - font-family: var(--r-code-font); - font-size: 80%; - letter-spacing: 0.02em; -} - -.bits .fixed { - color: #fff; - background: var(--accent); - border-radius: 4px 0 0 4px; - padding: 2px 6px; -} - -.bits .part { - color: var(--accent-soft); - background: var(--ground); - border: 1px solid var(--accent); - padding: 1px 4px; - margin: 0 2px; - font-size: 85%; - letter-spacing: 0.12em; -} - -.bits .part::before { content: "₂"; color: var(--muted); letter-spacing: 0; margin-right: 2px; } - -.bits .part i { font-style: normal; color: var(--muted); } - -.bits .free { - color: var(--ink); - background: var(--ground); - border: 1px solid var(--rule); - border-left: none; - border-radius: 0 4px 4px 0; - padding: 1px 6px; -} - -/* ---- diagrams: inline SVG drawn with the theme's colours ------------------------ */ - -.reveal svg.diagram { - width: 100%; - height: auto; - max-height: 470px; - display: block; - margin: 0 auto; -} - -.diagram rect, -.diagram ellipse { - fill: var(--ground); - stroke: var(--rule); - stroke-width: 2; -} - -.diagram .hot rect { - stroke: var(--accent); -} - -.diagram .fabric rect { - fill: rgba(var(--accent-rgb), 0.18); - stroke: var(--accent); -} - -.diagram text { - fill: var(--ink); - font-family: "IBM Plex Sans", sans-serif; - font-size: 19px; - text-anchor: middle; -} - -.diagram text.small { - font-size: 14px; - fill: var(--muted); - font-family: "IBM Plex Mono", monospace; -} - -.diagram text.accent { - fill: var(--accent); -} - -.diagram .link { - stroke: var(--muted); - stroke-width: 2.5; - fill: none; -} - -.diagram .bgp { - stroke: var(--accent); - stroke-width: 2.5; - stroke-dasharray: 7 6; - fill: none; -} - -/* Nested address ranges: each level a little brighter than the one around it. */ -.diagram .nest rect { - stroke: var(--accent); - rx: 14; -} -.diagram .nest.l0 rect { fill: rgba(var(--accent-rgb), 0.04); stroke-opacity: 0.35; } -.diagram .nest.l1 rect { fill: rgba(var(--accent-rgb), 0.09); stroke-opacity: 0.55; } -.diagram .nest.l2 rect { fill: rgba(var(--accent-rgb), 0.16); stroke-opacity: 0.8; } -.diagram .nest.l3 rect { fill: rgba(var(--accent-rgb), 0.28); stroke-opacity: 1; } -.diagram text.left { text-anchor: start; } -.diagram text.mono { font-family: "IBM Plex Mono", monospace; } -.diagram .nest-label { font-family: "IBM Plex Mono", monospace; font-size: 22px; font-weight: 500; } -.diagram .nest-desc { font-size: 17px; fill: var(--ink); opacity: 0.85; } -.diagram text.host-addr { font-size: 15px; fill: var(--accent-soft); font-family: "IBM Plex Mono", monospace; } -.diagram .host { fill: var(--accent-soft); stroke: none; } - -.diagram .traffic { - stroke: #1a8a4f; - stroke-width: 4; - fill: none; -} diff --git a/slides/theme.css b/slides/theme.css deleted file mode 100644 index c7d41c3..0000000 --- a/slides/theme.css +++ /dev/null @@ -1,430 +0,0 @@ -/* - * The look of the deck. Structure borrowed from Matt Godbolt's newer talks - * (hrt-theme.css in github.com/mattgodbolt/what-happens-beneath-accu-2026): - * every slide on a rounded panel, IBM Plex Sans with thin h1 and bold accent - * h2, IBM Plex Mono code, accent-coloured list markers, links and progress - * bar, glowing highlighted code lines. - * - * Made our own: a light palette (his is near-black; change it in :root - * below), and his "thread" corner motif becomes a chain of nodes and links, - * an AS path. Code blocks and byte dumps stay dark on the light page so the - * monokai colours and the byte palette keep working. - * - * Try accents without editing: index.html?accent=green (blue, green, - * amber, magenta). Make one permanent by copying its values into :root. - */ -@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400&display=swap"); - -:root { - --ground: #e9edf5; /* behind the panels */ - --panel: #ffffff; /* the slide itself */ - --panel-edge: rgba(31, 111, 235, 0.22); - --ink: #1f2333; /* body text */ - --ink-strong: #0b0d17; /* h1, h3 */ - --muted: #6b7088; /* h4, captions */ - --rule: #c3c8d8; /* table rules, borders */ - --accent: #1f6feb; /* h2, markers, links, motif */ - --accent-soft: #0b4ea2; /* inline code, emphasis */ - --accent-rgb: 31, 111, 235; - /* Code blocks and byte dumps keep a dark ground of their own. */ - --code-ground: #1b1e33; - --code-ink: #eceff9; - --r-code-font: "IBM Plex Mono", "SF Mono", Menlo, Consolas, monospace; - --r-heading-text-shadow: none; -} - -:root[data-accent="green"] { - --accent: #1a8a4f; - --accent-soft: #0f6238; - --accent-rgb: 26, 138, 79; - --panel-edge: rgba(26, 138, 79, 0.22); -} - -:root[data-accent="amber"] { - --accent: #b86e00; - --accent-soft: #7a4a00; - --accent-rgb: 184, 110, 0; - --panel-edge: rgba(184, 110, 0, 0.22); -} - -:root[data-accent="magenta"] { - --accent: #c2277f; - --accent-soft: #8a1a5a; - --accent-rgb: 194, 39, 127; - --panel-edge: rgba(194, 39, 127, 0.22); -} - -/* ---- ground and panels ---------------------------------------------------- */ - -.reveal-viewport { - background: var(--ground); -} - -.reveal { - font-family: "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif; - font-size: 40px; - font-weight: 400; - color: var(--ink); -} - -.reveal .slides section:not(.stack):not(.title-slide):not(.bare) { - background-color: var(--panel); - border-radius: 12px; - box-shadow: 0 4px 30px rgba(20, 30, 60, 0.1); - border: 1px solid var(--panel-edge); - padding: 1em 0.8em; - box-sizing: border-box; -} - -/* ---- type ------------------------------------------------------------------ */ - -.reveal h1, -.reveal h2, -.reveal h3, -.reveal h4 { - font-family: inherit; - text-transform: none; - text-shadow: none; - margin: 0 0 20px 0; - line-height: 1.25; -} - -.reveal h1 { - font-weight: 300; - color: var(--ink-strong); - font-size: 2.2em; - line-height: 1.15; -} - -.reveal h2 { - font-weight: 700; - color: var(--accent); - font-size: 1.5em; -} - -.reveal h3 { - font-weight: 500; - color: var(--ink-strong); - font-size: 1.2em; -} - -.reveal h4 { - font-weight: 500; - color: var(--muted); - font-size: 1em; -} - -.reveal p { - margin: 20px 0; - line-height: 1.5; -} - -.reveal strong, -.reveal b { - font-weight: 700; - color: var(--ink-strong); -} - -.reveal em { - font-style: italic; - color: var(--accent-soft); -} - -.reveal a { - color: var(--accent); - text-decoration: none; - transition: color 0.15s ease; -} - -.reveal a:hover { - color: var(--accent-soft); -} - -.reveal ul, -.reveal ol { - display: inline-block; - text-align: left; - margin: 0 0 0 1em; -} - -.reveal ul { - list-style-type: square; -} - -.reveal ul li, -.reveal ol li { - margin-bottom: 0.4em; -} - -.reveal li::marker { - color: var(--accent); -} - -.reveal blockquote { - background: var(--ground); - border-left: 4px solid var(--accent); - padding: 20px 30px; - font-style: italic; - width: 85%; - margin: 20px auto; - text-align: left; -} - -.reveal img, -.reveal video, -.reveal iframe { - max-width: 95%; - max-height: 95%; -} - -.reveal kbd { - font-family: var(--r-code-font); - font-size: 80%; - border: 1px solid var(--rule); - border-bottom-width: 3px; - border-radius: 5px; - padding: 0 0.35em; -} - -/* ---- code ------------------------------------------------------------------ */ - -.reveal pre { - width: 100%; - margin: 20px auto; - box-shadow: none; - text-align: left; - position: relative; - font-size: 0.55em; - line-height: 1.4; -} - -.reveal code { - font-family: var(--r-code-font); - color: var(--accent-soft); -} - -.reveal :not(pre) > code { - white-space: nowrap; - font-size: 92%; -} - -.reveal pre code { - display: block; - color: var(--code-ink); - background: var(--code-ground); - border-radius: 6px; - padding: 15px 20px; - max-height: 560px; - overflow: auto; -} - -/* The lines being talked about glow; the rest dim (reveal handles the dim). */ -.reveal .hljs.has-highlights tr.highlight-line { - text-shadow: - 0 0 6px currentColor, - 0 0 12px currentColor; - transition: text-shadow 0.4s ease; -} - -/* ---- tables ---------------------------------------------------------------- */ - -.reveal table { - border-collapse: collapse; - border-spacing: 0; - margin: 20px auto; - font-size: 75%; -} - -.reveal table th { - font-weight: 700; - color: var(--ink-strong); -} - -.reveal table th, -.reveal table td { - text-align: left; - border-bottom: 1px solid var(--rule); - padding: 0.25em 0.6em; -} - -.reveal table tr:last-child td { - border-bottom: none; -} - -/* ---- reveal chrome ---------------------------------------------------------- */ - -.reveal .controls { - color: var(--accent); - bottom: 40px; -} - -.reveal .progress { - background: var(--panel); - height: 4px; -} - -.reveal .progress span { - background: var(--accent); - transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -} - -.reveal .slide-number { - color: #fff; - font-family: var(--r-code-font); - font-size: 18px; - font-weight: 500; - background: rgba(var(--accent-rgb), 0.9); - padding: 3px 9px; - border-radius: 4px; - right: 16px; - bottom: 16px; -} - -::selection { - color: #fff; - background: var(--accent); -} - -/* ---- the path motif --------------------------------------------------------- - * Matt's decks carry a ring with a line running off the edge. Ours is a short - * AS path: nodes joined by links, heading off the top-right corner, mirrored - * running up from the bottom-left. Drawn in the accent colour via a mask, so - * it follows --accent. Hidden on the title slide and on "bare" slides. - */ -.reveal::before, -.reveal-viewport::before { - content: ""; - position: fixed; - background: var(--accent); - z-index: 0; - pointer-events: none; - opacity: 0.9; -} - -.reveal::before { - top: 26px; - right: 0; - width: 38vw; - height: 22px; - -webkit-mask: - url("data:image/svg+xml;utf8,") - left center / 190px 22px no-repeat, - linear-gradient(black, black) 189px center / calc(100% - 189px) 3px no-repeat; - mask: - url("data:image/svg+xml;utf8,") - left center / 190px 22px no-repeat, - linear-gradient(black, black) 189px center / calc(100% - 189px) 3px no-repeat; -} - -.reveal-viewport::before { - bottom: 64px; - left: 40px; - width: 22px; - height: 36vh; - -webkit-mask: - url("data:image/svg+xml;utf8,") - center bottom / 22px 126px no-repeat, - linear-gradient(black, black) center top / 3px calc(100% - 125px) no-repeat; - mask: - url("data:image/svg+xml;utf8,") - center bottom / 22px 126px no-repeat, - linear-gradient(black, black) center top / 3px calc(100% - 125px) no-repeat; -} - -.reveal:has(> .slides > section.title-slide.present)::before, -.reveal:has(> .slides section.bare.present)::before { - display: none; -} - -.reveal-viewport:has(.reveal > .slides > section.title-slide.present)::before, -.reveal-viewport:has(.reveal > .slides section.bare.present)::before { - display: none; -} - -.reveal:has(section.title-slide.present) .controls { - display: none !important; -} - -/* ---- wordmark, where his logo sits ------------------------------------------ */ - -.wordmark { - position: fixed; - left: 22px; - bottom: 18px; - z-index: 10; - font-family: var(--r-code-font); - font-size: 15px; - letter-spacing: 0.04em; - color: var(--muted); - pointer-events: none; -} - -.wordmark b { - color: var(--accent); - font-weight: 500; -} - -.reveal:has(section.title-slide.present) ~ .wordmark, -.reveal:has(section.bare.present) ~ .wordmark { - display: none; -} - -/* ---- title slide -------------------------------------------------------------- */ - -.reveal section.title-slide { - text-align: left; - padding: 0 1.5em; -} - -.reveal section.title-slide h1 { - font-size: 2.6em; - margin-bottom: 0.15em; -} - -.reveal section.title-slide h3 { - font-weight: 400; - color: var(--accent); - margin-bottom: 2.2em; -} - -.reveal section.title-slide .byline { - font-size: 60%; - color: var(--muted); - margin: 0; -} - -/* ---- shown by index.html when the deck cannot start --------------------------- */ - -#deck-help { - position: fixed; - inset: 0; - z-index: 100; - padding: 8vh 10vw; - background: var(--ground); - color: var(--ink); - font: 20px/1.5 "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif; -} - -#deck-help h2 { - color: var(--accent); - font-weight: 700; -} - -#deck-help pre, -#deck-help code { - font-family: var(--r-code-font); - color: var(--accent-soft); -} - -#deck-help pre { - background: var(--panel); - padding: 12px 16px; - border-radius: 6px; -} - -/* ---- print ------------------------------------------------------------------ */ - -html.print-pdf .wordmark, -html.reveal-print .wordmark { - display: none; -} diff --git a/slides/vendor-reveal.sh b/slides/vendor-reveal.sh deleted file mode 100755 index c746ea9..0000000 --- a/slides/vendor-reveal.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# Copies reveal.js into ./reveal.js so the deck works with no network. -# index.html uses this copy when it exists and the CDN otherwise. Keep VERSION -# in step with the one in index.html. -set -eu -VERSION=6.0.2 -cd "$(dirname "$0")" -tmp=$(mktemp -d) -trap 'rm -rf "$tmp"' EXIT -npm pack "reveal.js@$VERSION" --pack-destination "$tmp" --silent >/dev/null 2>&1 -tar -xzf "$tmp/reveal.js-$VERSION.tgz" -C "$tmp" -rm -rf reveal.js -mkdir -p reveal.js -cp -R "$tmp/package/dist" reveal.js/dist -cp "$tmp/package/LICENSE" reveal.js/LICENSE -echo "reveal.js $VERSION vendored into $(pwd)/reveal.js ($(du -sh reveal.js | cut -f1))" diff --git a/tools/loadtest.py b/tools/loadtest.py deleted file mode 100755 index 94caf6e..0000000 --- a/tools/loadtest.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env python3 -"""Load test for the ferrix_bgp web API: N students announce at the same moment. - -Each simulated student owns one prefix (student n gets the n-th /48, numbered -in hex like the lab's route6 objects) and announces it under its own owner -name. All students are released together by a barrier, so the announcements -really are concurrent. The script then checks that the daemon did the right -thing, not merely that it answered: - - 1. every announcement was accepted, with latency percentiles - 2. the local table holds exactly these routes, under the right owners - 3. every route was turned into an UPDATE for each established session - 4. the event stream delivered one "announced" event per route, none lost - 5. ownership holds under load: nobody could replace another's prefix - 6. concurrent withdrawal leaves nothing behind - 7. the daemon's own counters show the matching number of BGP UPDATEs sent, - for the announcements and again for the withdrawals - -SAFETY. Pointed at a router that peers with the real Internet, this announces -real prefixes. By default every route therefore carries the large community -210036:0:0 ("announce nowhere"), so the production router accepts the routes -but exports them to nobody, and the script withdraws everything it announced -even when it fails half-way. Pass --announce-for-real only if you mean it. - -Standard library only. Examples: - - tools/loadtest.py 128 - tools/loadtest.py 128 --url https://lab.example.net --user student - tools/loadtest.py 64 --rounds 5 --keep -""" - -import argparse -import base64 -import getpass -import json -import os -import ssl -import statistics -import sys -import threading -import time -import urllib.error -import urllib.request - -QUIET_COMMUNITY = "210036:0:0" -OWNER_PREFIX = "loadtest-" - - -class Api: - def __init__(self, base, user=None, password=None, insecure=False, timeout=15): - self.base = base.rstrip("/") - self.timeout = timeout - self.headers = {"content-type": "application/json"} - if user: - token = base64.b64encode(f"{user}:{password}".encode()).decode() - self.headers["authorization"] = f"Basic {token}" - self.ctx = ssl._create_unverified_context() if insecure else None - - def call(self, method, path, body=None): - """Returns (status, parsed JSON or text, seconds).""" - data = json.dumps(body).encode() if body is not None else None - req = urllib.request.Request(self.base + path, data=data, method=method, headers=self.headers) - start = time.perf_counter() - try: - with urllib.request.urlopen(req, timeout=self.timeout, context=self.ctx) as resp: - status, raw = resp.status, resp.read() - except urllib.error.HTTPError as e: - status, raw = e.code, e.read() - except Exception as e: # connection refused, timeout, TLS failure, ... - return 0, f"{type(e).__name__}: {e}", time.perf_counter() - start - elapsed = time.perf_counter() - start - try: - return status, json.loads(raw) if raw else None, elapsed - except ValueError: - return status, raw.decode(errors="replace"), elapsed - - def open_stream(self, path): - req = urllib.request.Request(self.base + path, headers={**self.headers, "accept": "text/event-stream"}) - return urllib.request.urlopen(req, timeout=self.timeout, context=self.ctx) - - -def updates_sent(api): - """BGP UPDATE messages the daemon has sent so far, from /metricz; None if unavailable.""" - status, text, _ = api.call("GET", "/metricz") - if status != 200 or not isinstance(text, str): - return None - for line in text.splitlines(): - if line.startswith('bgp_messages_sent_total{type="update"}'): - return int(float(line.rsplit(" ", 1)[1])) - return 0 # the counter has no sample until the first UPDATE - - -def established(api): - status, peers, _ = api.call("GET", "/api/peers") - return [p for p in peers if p.get("state") == "Established"] if status == 200 else [] - - -def check_wire(api, report, before, routes, sessions_before, what): - """The advertiser works asynchronously, so give the counter a moment to settle.""" - if before is None: - print(f" [skip] /metricz unavailable, cannot count UPDATEs for the {what}") - return - expected = routes * len(sessions_before) - deadline = time.time() + 5 - while time.time() < deadline and (updates_sent(api) or 0) - before < expected: - time.sleep(0.1) - delta = (updates_sent(api) or 0) - before - if len(established(api)) != len(sessions_before): - print(f" [skip] sessions changed during the test; {delta} UPDATEs sent for the {what}") - else: - report.check(delta == expected, f"{delta} BGP UPDATEs sent for the {what} " - f"({routes} routes x {len(sessions_before)} session(s))", [f"expected {expected}"]) - - -class EventCounter(threading.Thread): - """Follows /api/events and records what it sees until stopped.""" - - def __init__(self, api): - super().__init__(daemon=True) - self.api, self.events, self.error = api, [], None - self.connected, self._stop = threading.Event(), threading.Event() - - def run(self): - try: - stream = self.api.open_stream("/api/events") - except Exception as e: - self.error = f"{type(e).__name__}: {e}" - self.connected.set() - return - self.connected.set() - try: - for raw in stream: - if self._stop.is_set(): - break - line = raw.decode(errors="replace").strip() - if line.startswith("data:"): - try: - self.events.append(json.loads(line[5:])) - except ValueError: - pass - except Exception as e: - if not self._stop.is_set(): - self.error = f"{type(e).__name__}: {e}" - - def stop(self): - self._stop.set() - - -def percentile(values, p): - ordered = sorted(values) - return ordered[min(len(ordered) - 1, int(round(p / 100 * (len(ordered) - 1))))] - - -def latency_line(latencies): - if not latencies: - return "no samples" - ms = [v * 1000 for v in latencies] - return (f"min {min(ms):.0f} ms, median {statistics.median(ms):.0f} ms, " - f"p95 {percentile(ms, 95):.0f} ms, max {max(ms):.0f} ms") - - -def run_concurrently(jobs, worker): - """Runs worker(job) for every job at the same instant; returns results in order.""" - results = [None] * len(jobs) - barrier = threading.Barrier(len(jobs)) - - def target(i, job): - try: - barrier.wait(timeout=30) - except threading.BrokenBarrierError: - results[i] = (0, "barrier broken", 0.0) - return - results[i] = worker(job) - - threads = [threading.Thread(target=target, args=(i, job)) for i, job in enumerate(jobs)] - wall = time.perf_counter() - for t in threads: - t.start() - for t in threads: - t.join() - return results, time.perf_counter() - wall - - -class Report: - def __init__(self): - self.failures = 0 - - def check(self, ok, text, detail=None): - print(f" [{'ok' if ok else 'FAIL'}] {text}") - if not ok: - self.failures += 1 - for line in (detail or [])[:8]: - print(f" {line}") - if detail and len(detail) > 8: - print(f" ... and {len(detail) - 8} more") - return ok - - -def one_round(api, args, students, report, round_no): - body_for = lambda s: { - "prefix": s["prefix"], "owner": s["owner"], - "communities": args.community, "large_communities": s["large"], - } - - events = EventCounter(api) - if not args.no_events: - events.start() - events.connected.wait(timeout=10) - time.sleep(0.3) # let the subscription settle before the burst - - # ---- 1. everybody announces at once ----------------------------------- - print(f"\nRound {round_no}: {len(students)} students announce concurrently") - sent_before, sessions_before = updates_sent(api), established(api) - results, wall = run_concurrently(students, lambda s: api.call("POST", "/api/routes/local", body_for(s))) - accepted = [s for s, r in zip(students, results) if r[0] == 200] - rejected = [f"{s['owner']} {s['prefix']}: HTTP {r[0]} {str(r[1])[:160]}" for s, r in zip(students, results) if r[0] != 200] - report.check(not rejected, f"all {len(students)} announcements accepted in {wall:.2f} s " - f"({len(students) / wall:.0f}/s)", rejected) - print(f" latency: {latency_line([r[2] for r in results if r[0] == 200])}") - if not accepted: - print(" [skip] nothing was accepted, so there is nothing further to verify") - if not args.no_events: - events.stop() - return accepted - - # ---- 2. the table holds exactly what was announced -------------------- - status, table, _ = api.call("GET", "/api/routes/local") - table = table if status == 200 and isinstance(table, list) else [] - by_prefix = {r["prefix"]: r for r in table} - missing = [f"{s['prefix']} missing" for s in accepted if s["prefix"] not in by_prefix] - wrong_owner = [f"{s['prefix']} owned by {by_prefix[s['prefix']]['owner']!r}, expected {s['owner']!r}" - for s in accepted if s["prefix"] in by_prefix and by_prefix[s["prefix"]]["owner"] != s["owner"]] - wrong_attrs = [f"{s['prefix']} large communities {by_prefix[s['prefix']]['large_communities']}" - for s in accepted if s["prefix"] in by_prefix - and sorted(by_prefix[s["prefix"]]["large_communities"]) != sorted(s["large"])] - report.check(not (missing or wrong_owner or wrong_attrs), - "local table holds every route with the right owner and communities", - missing + wrong_owner + wrong_attrs) - - # ---- 3. each route became an UPDATE on every session ------------------- - up = established(api) - if not up: - print(" [skip] no established BGP session, so nothing was put on the wire") - else: - sample = accepted if len(accepted) <= 32 else accepted[:: max(1, len(accepted) // 32)] - bad = [] - for s in sample: - code, wire, _ = api.call("GET", "/api/routes/local/wire?prefix=" + urllib.request.quote(s["prefix"], safe="")) - if code != 200 or not wire: - bad.append(f"{s['prefix']}: HTTP {code}, {len(wire) if isinstance(wire, list) else 0} session(s)") - elif not all(w["hex"].startswith("ff" * 16) for w in wire): - bad.append(f"{s['prefix']}: malformed UPDATE") - report.check(not bad, f"UPDATE built for {len(up)} established session(s) " - f"(checked {len(sample)} of {len(accepted)} routes)", bad) - check_wire(api, report, sent_before, len(accepted), sessions_before, "announcements") - - # ---- 4. the event stream lost nothing ---------------------------------- - if not args.no_events: - deadline = time.time() + 5 - want = {s["prefix"] for s in accepted} - seen = lambda: {e["route"]["prefix"] for e in events.events if e.get("type") == "local_announced"} - while time.time() < deadline and not want <= seen(): - time.sleep(0.1) - lagged = [e for e in events.events if e.get("type") == "lagged"] - lost = sorted(want - seen()) - detail = [f"no event for {p}" for p in lost] + [f"stream lagged: missed {e['missed']} on {e['stream']}" for e in lagged] - if events.error: - detail.insert(0, f"event stream: {events.error}") - report.check(not detail, f"event stream delivered all {len(want)} announcements", detail) - - # ---- 5. ownership holds under load ------------------------------------- - if len(accepted) >= 2 and not args.no_policy: - # Everyone tries, at once, to take over their neighbour's prefix. - thieves = [{**accepted[i], "prefix": accepted[(i + 1) % len(accepted)]["prefix"]} for i in range(len(accepted))] - results, _ = run_concurrently(thieves, lambda s: api.call("POST", "/api/routes/local", body_for(s))) - leaked = [f"{t['owner']} took {t['prefix']}: HTTP {r[0]}" for t, r in zip(thieves, results) if r[0] != 409] - status, table, _ = api.call("GET", "/api/routes/local") - owners = {r["prefix"]: r["owner"] for r in table} if status == 200 else {} - leaked += [f"{s['prefix']} now owned by {owners.get(s['prefix'])!r}" for s in accepted if owners.get(s["prefix"]) != s["owner"]] - report.check(not leaked, f"{len(thieves)} concurrent takeover attempts all refused (409), owners unchanged", leaked) - - if not args.no_events: - events.stop() - return accepted - - -def withdraw_all(api, students, report, verify=True): - if not students: - return - sent_before, sessions_before = (updates_sent(api), established(api)) if verify else (None, []) - results, wall = run_concurrently( - students, lambda s: api.call("DELETE", "/api/routes/local", {"prefix": s["prefix"], "owner": s["owner"]})) - failed = [f"{s['prefix']}: HTTP {r[0]} {r[1]}" for s, r in zip(students, results) if r[0] != 200] - if not verify: - return - report.check(not failed, f"all {len(students)} withdrawals accepted in {wall:.2f} s", failed) - status, table, _ = api.call("GET", "/api/routes/local") - left = [r["prefix"] for r in table if r["owner"].startswith(OWNER_PREFIX)] if status == 200 else ["(could not read table)"] - report.check(not left, "nothing left behind by the load test", left) - if sessions_before: - check_wire(api, report, sent_before, len(students) - len(failed), sessions_before, "withdrawals") - - -def main(): - ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("students", type=int, help="number of simulated students") - ap.add_argument("--url", default="http://127.0.0.1:8080", help="base URL of the status page (default %(default)s)") - ap.add_argument("--user", help="HTTP basic auth user; the password is read from $FERRIX_PASSWORD or prompted for") - ap.add_argument("--insecure", action="store_true", help="do not verify the TLS certificate") - ap.add_argument("--prefix-template", default="2a0d:d741:{n:x}::/48", - help="prefix of student n, a Python format string (default %(default)s)") - ap.add_argument("--first", type=int, default=1, help="number of the first student (default 1)") - ap.add_argument("--rounds", type=int, default=1, help="repeat announce/verify/withdraw this many times") - ap.add_argument("--community", action="append", default=[], help="standard community to attach (repeatable)") - ap.add_argument("--large-community", action="append", default=[], help="extra large community to attach (repeatable)") - ap.add_argument("--announce-for-real", action="store_true", - help=f"do NOT add {QUIET_COMMUNITY}; routes may be exported to the Internet") - ap.add_argument("--keep", action="store_true", help="leave the routes announced at the end") - ap.add_argument("--no-events", action="store_true", help="skip the event stream check") - ap.add_argument("--no-policy", action="store_true", help="skip the concurrent takeover check") - args = ap.parse_args() - if args.students < 1: - ap.error("need at least one student") - - password = None - if args.user: - password = os.environ.get("FERRIX_PASSWORD") or getpass.getpass(f"Password for {args.user}: ") - api = Api(args.url, args.user, password, args.insecure) - - large = list(args.large_community) - if not args.announce_for_real and QUIET_COMMUNITY not in large: - large.insert(0, QUIET_COMMUNITY) - students = [{"owner": f"{OWNER_PREFIX}{n}", "prefix": args.prefix_template.format(n=n), "large": large} - for n in range(args.first, args.first + args.students)] - - # ---- preflight ---------------------------------------------------------- - status, info, _ = api.call("GET", "/api/status") - if status == 401: - sys.exit(f"{args.url} wants a password: HTTP 401. Pass --user, and check the password.") - if status != 200: - sys.exit(f"Cannot reach {args.url}/api/status: HTTP {status} {str(info)[:200]}") - print(f"Target: {args.url} router-id {info['router_id']} AS{info['local_asn']} version {info['version']} " - f"on {info.get('hostname', '?')}") - print(f" {info['sessions']} established session(s), {info['local_routes']} local route(s) already present") - print(f"Prefixes {students[0]['prefix']} .. {students[-1]['prefix']}, large communities {large or 'none'}") - if args.announce_for_real: - print("WARNING: --announce-for-real: these routes may be exported to the Internet.") - - status, table, _ = api.call("GET", "/api/routes/local") - stale = [r for r in table if r["owner"].startswith(OWNER_PREFIX)] if status == 200 else [] - if stale: - print(f"Removing {len(stale)} route(s) left over from an earlier load test") - withdraw_all(api, [{"owner": r["owner"], "prefix": r["prefix"]} for r in stale], Report(), verify=False) - taken = {r["prefix"]: r["owner"] for r in table if not r["owner"].startswith(OWNER_PREFIX)} if status == 200 else {} - clash = [s for s in students if s["prefix"] in taken] - if clash: - print(f"Skipping {len(clash)} prefix(es) currently announced by real owners: " - + ", ".join(f"{s['prefix']} ({taken[s['prefix']]})" for s in clash[:5]) + (" ..." if len(clash) > 5 else "")) - students = [s for s in students if s["prefix"] not in taken] - if not students: - sys.exit("Nothing left to test.") - - report, announced = Report(), [] - try: - for round_no in range(1, args.rounds + 1): - announced = one_round(api, args, students, report, round_no) - if round_no < args.rounds or not args.keep: - withdraw_all(api, announced, report) - announced = [] - except KeyboardInterrupt: - print("\nInterrupted.") - report.failures += 1 - finally: - if announced and not args.keep: - withdraw_all(api, announced, Report(), verify=False) - - status, info, _ = api.call("GET", "/api/status") - if status == 200: - print(f"\nAfterwards: {info['sessions']} established session(s), {info['local_routes']} local route(s)") - print("RESULT: " + ("PASS" if report.failures == 0 else f"FAIL ({report.failures} check(s) failed)")) - sys.exit(0 if report.failures == 0 else 1) - - -if __name__ == "__main__": - main()