Skip to main content

netget/server/bgp/
wire.rs

1//! BGP-4 wire format: encoding, decoding, and the model-facing JSON projection.
2//!
3//! # Why netgauze rather than a hand-rolled codec
4//!
5//! Everything on this path is attacker-controlled. Capability TLVs, path-attribute flags,
6//! extended attribute lengths, the two-byte/four-byte AS_PATH ambiguity of RFC 6793 and the
7//! multiprotocol attributes of RFC 4760 are all places where a plausible-looking hand-rolled
8//! parser silently disagrees with a real router. `netgauze-bgp-pkt` was already a declared
9//! dependency of the `bgp` feature and was not used by a single line of code; it is a
10//! maintained implementation of RFC 4271 and the surrounding RFCs, so all parsing and the
11//! structurally interesting encodings (OPEN, UPDATE) now go through it.
12//!
13//! Access is via [`netgauze_bgp_pkt::codec::BgpCodec`], which implements `tokio_util`'s
14//! `Encoder`/`Decoder`. That matters for a mundane reason: netgauze's more direct
15//! `WritablePdu`/`ReadablePduWithOneInput` traits live in `netgauze-parse-utils`, which is not
16//! a declared dependency of this crate, and neither is `ipnet`. The codec route needs only
17//! `netgauze-bgp-pkt`, `tokio-util` and `bytes`, all of which are already present.
18//!
19//! Two consequences of that constraint are visible below and both would disappear if
20//! `netgauze-parse-utils` and `ipnet` were added to `Cargo.toml`:
21//!
22//! * [`ipv4_unicast`] builds an [`Ipv4Unicast`] through serde instead of `Ipv4Net`.
23//! * A per-message codec is constructed rather than reusing one.
24//!
25//! # What is deliberately *not* netgauze
26//!
27//! KEEPALIVE and NOTIFICATION are hand-encoded. KEEPALIVE is a bare header. NOTIFICATION is a
28//! header plus two octets, and netgauze models it as a closed enum of the (code, subcode) pairs
29//! it knows, which cannot represent an arbitrary subcode. Since `send_bgp_notification` is an
30//! LLM-facing action taking numeric codes, hand-encoding is both simpler and strictly more
31//! expressive. Neither message has any structure to get wrong.
32
33use anyhow::{anyhow, bail, Context, Result};
34use bytes::BytesMut;
35use std::net::Ipv4Addr;
36use tokio_util::codec::{Decoder, Encoder};
37
38use netgauze_bgp_pkt::{
39    capabilities::{BgpCapability, FourOctetAsCapability},
40    codec::BgpCodec,
41    nlri::{Ipv4Unicast, Ipv4UnicastAddress},
42    open::{BgpOpenMessage, BgpOpenMessageParameter},
43    path_attribute::{
44        As2PathSegment, As4Path, As4PathSegment, AsPath, AsPathSegmentType, LocalPreference,
45        MultiExitDiscriminator, NextHop, Origin, PathAttribute, PathAttributeValue,
46    },
47    update::BgpUpdateMessage,
48    BgpMessage,
49};
50
51/// Every BGP message begins with 16 octets of ones (RFC 4271 section 4.1).
52pub const BGP_MARKER: [u8; 16] = [0xff; 16];
53
54/// Marker (16) + length (2) + type (1).
55pub const BGP_HEADER_LEN: usize = 19;
56
57/// RFC 4271 section 4: no message may exceed 4096 octets. RFC 8654 raises this for peers that
58/// negotiate the extended-message capability; NetGet does not advertise it, so the hard limit
59/// applies in both directions and is enforced on input before a single body byte is read.
60pub const BGP_MAX_MESSAGE_LEN: usize = 4096;
61
62/// RFC 6793: the reserved two-octet ASN a four-octet speaker puts in the OPEN `My Autonomous
63/// System` field when its real ASN does not fit.
64pub const AS_TRANS: u16 = 23456;
65
66/// BGP message type octets (RFC 4271 section 4.1).
67pub const MSG_OPEN: u8 = 1;
68pub const MSG_UPDATE: u8 = 2;
69pub const MSG_NOTIFICATION: u8 = 3;
70pub const MSG_KEEPALIVE: u8 = 4;
71pub const MSG_ROUTE_REFRESH: u8 = 5;
72
73/// A NOTIFICATION (error code, error subcode) pair.
74pub type NotifyCode = (u8, u8);
75
76// Message Header Error (RFC 4271 section 6.1)
77pub const ERR_HEADER: u8 = 1;
78pub const SUB_CONNECTION_NOT_SYNCHRONIZED: u8 = 1;
79pub const SUB_BAD_MESSAGE_LENGTH: u8 = 2;
80pub const SUB_BAD_MESSAGE_TYPE: u8 = 3;
81// OPEN Message Error (RFC 4271 section 6.2)
82pub const ERR_OPEN: u8 = 2;
83pub const SUB_UNSUPPORTED_VERSION: u8 = 1;
84pub const SUB_BAD_PEER_AS: u8 = 2;
85pub const SUB_BAD_BGP_IDENTIFIER: u8 = 3;
86pub const SUB_UNACCEPTABLE_HOLD_TIME: u8 = 6;
87// UPDATE Message Error (RFC 4271 section 6.3)
88pub const ERR_UPDATE: u8 = 3;
89// Hold Timer Expired (RFC 4271 section 6.5)
90pub const ERR_HOLD_TIMER_EXPIRED: u8 = 4;
91// Finite State Machine Error (RFC 4271 section 6.6)
92pub const ERR_FSM: u8 = 5;
93pub const SUB_FSM_OPENSENT: u8 = 1;
94pub const SUB_FSM_OPENCONFIRM: u8 = 2;
95pub const SUB_FSM_ESTABLISHED: u8 = 3;
96// Cease (RFC 4271 section 6.7; subcodes from RFC 4486 section 3)
97pub const ERR_CEASE: u8 = 6;
98pub const SUB_CEASE_ADMIN_SHUTDOWN: u8 = 2;
99/// RFC 4486 section 4: the speaker is rejecting this connection. Permanent as far as the peer
100/// is concerned — it should not treat the rejection as a transient resource condition.
101pub const SUB_CEASE_CONNECTION_REJECTED: u8 = 5;
102/// RFC 4486 section 8: the speaker cannot carry the peering right now for want of resources.
103/// A conforming implementation applies a damping/backoff to this subcode and retries, which is
104/// exactly the behaviour a saturated backend wants.
105pub const SUB_CEASE_OUT_OF_RESOURCES: u8 = 8;
106
107/// Why a 19-octet header was rejected, and the NOTIFICATION it earns.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum HeaderError {
110    /// Marker was not all ones — the stream is out of sync and cannot be resynchronised.
111    BadMarker,
112    /// Length outside \[19, 4096\], or below the minimum for the declared type.
113    BadLength(u16),
114}
115
116impl HeaderError {
117    pub fn notify_code(&self) -> NotifyCode {
118        match self {
119            Self::BadMarker => (ERR_HEADER, SUB_CONNECTION_NOT_SYNCHRONIZED),
120            Self::BadLength(_) => (ERR_HEADER, SUB_BAD_MESSAGE_LENGTH),
121        }
122    }
123
124    /// RFC 4271 section 6.1 says the Data field carries the erroneous length.
125    pub fn notify_data(&self) -> Vec<u8> {
126        match self {
127            Self::BadMarker => Vec::new(),
128            Self::BadLength(len) => len.to_be_bytes().to_vec(),
129        }
130    }
131}
132
133/// Validate a BGP message header without touching the body.
134///
135/// Returns `(total_message_length, message_type)`. The length is the *whole* message including
136/// these 19 octets, exactly as it appears on the wire, and is guaranteed on return to be in
137/// `[BGP_HEADER_LEN, BGP_MAX_MESSAGE_LEN]` — so `len - BGP_HEADER_LEN` cannot underflow and
138/// allocating `len` bytes is bounded.
139///
140/// The per-type minimums come from RFC 4271 section 4.1: OPEN is at least 29, UPDATE at least
141/// 23, NOTIFICATION at least 21, KEEPALIVE exactly 19.
142pub fn parse_header(header: &[u8; BGP_HEADER_LEN]) -> Result<(usize, u8), HeaderError> {
143    if header[..16] != BGP_MARKER {
144        return Err(HeaderError::BadMarker);
145    }
146    let len = u16::from_be_bytes([header[16], header[17]]);
147    let msg_type = header[18];
148
149    if (len as usize) < BGP_HEADER_LEN || (len as usize) > BGP_MAX_MESSAGE_LEN {
150        return Err(HeaderError::BadLength(len));
151    }
152    let min = match msg_type {
153        MSG_OPEN => 29,
154        MSG_UPDATE => 23,
155        MSG_NOTIFICATION => 21,
156        MSG_KEEPALIVE => 19,
157        // ROUTE-REFRESH is 23; an unknown type is not a length error, it is a type error, and
158        // is reported as such after the body has been consumed so the stream stays in sync.
159        MSG_ROUTE_REFRESH => 23,
160        _ => BGP_HEADER_LEN as u16,
161    };
162    if len < min {
163        return Err(HeaderError::BadLength(len));
164    }
165    Ok((len as usize, msg_type))
166}
167
168/// Decode one complete BGP message (header included).
169///
170/// `asn4` must be the *negotiated* four-octet-AS state: it decides whether AS_PATH in an UPDATE
171/// is read as two-octet or four-octet ASNs. Getting it wrong does not fail loudly, it silently
172/// yields the wrong AS path, which is why it is threaded through from the session rather than
173/// defaulted.
174///
175/// The error carries the NOTIFICATION that RFC 4271 section 6 prescribes for that failure.
176pub fn decode(bytes: &[u8], asn4: bool) -> std::result::Result<BgpMessage, DecodeError> {
177    let mut codec = BgpCodec::new(asn4);
178    let mut buf = BytesMut::from(bytes);
179    match codec.decode(&mut buf) {
180        Ok(Some((msg, _ignored))) => Ok(msg),
181        // `decode` returns Ok(None) when it wants more bytes. The caller framed the message
182        // from its own length field, so this means the length field disagreed with the body.
183        Ok(None) => Err(DecodeError {
184            notify: (ERR_HEADER, SUB_BAD_MESSAGE_LENGTH),
185            detail: "message body shorter than its length field".to_string(),
186        }),
187        Err(e) => Err(DecodeError::from_codec(e)),
188    }
189}
190
191/// A message that could not be parsed, together with the NOTIFICATION it earns.
192#[derive(Debug, Clone)]
193pub struct DecodeError {
194    pub notify: NotifyCode,
195    pub detail: String,
196}
197
198impl std::fmt::Display for DecodeError {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(
201            f,
202            "{} (NOTIFICATION {}/{})",
203            self.detail, self.notify.0, self.notify.1
204        )
205    }
206}
207
208impl DecodeError {
209    fn from_codec(e: netgauze_bgp_pkt::codec::BgpCodecDecoderError) -> Self {
210        use netgauze_bgp_pkt::codec::BgpCodecDecoderError as E;
211        use netgauze_bgp_pkt::wire::deserializer::open::BgpOpenMessageParsingError as OpenErr;
212        use netgauze_bgp_pkt::wire::deserializer::BgpMessageParsingError as MsgErr;
213
214        let detail = format!("{e:?}");
215        let notify = match &e {
216            E::IoError(_) => (ERR_CEASE, 0),
217            E::Incomplete(_) => (ERR_HEADER, SUB_BAD_MESSAGE_LENGTH),
218            E::BgpMessageParsingError(inner) => match inner {
219                MsgErr::ConnectionNotSynchronized(_) => {
220                    (ERR_HEADER, SUB_CONNECTION_NOT_SYNCHRONIZED)
221                }
222                MsgErr::BadMessageLength(_) => (ERR_HEADER, SUB_BAD_MESSAGE_LENGTH),
223                MsgErr::UndefinedBgpMessageType(_) => (ERR_HEADER, SUB_BAD_MESSAGE_TYPE),
224                MsgErr::BgpOpenMessageParsingError(open) => match open {
225                    OpenErr::UnsupportedVersionNumber(_) => (ERR_OPEN, SUB_UNSUPPORTED_VERSION),
226                    OpenErr::UnacceptableHoldTime(_) => (ERR_OPEN, SUB_UNACCEPTABLE_HOLD_TIME),
227                    OpenErr::InvalidBgpId(_) => (ERR_OPEN, SUB_BAD_BGP_IDENTIFIER),
228                    _ => (ERR_OPEN, 0),
229                },
230                MsgErr::BgpUpdateMessageParsingError(_) => (ERR_UPDATE, 0),
231                // A malformed NOTIFICATION, or anything else, is not worth a reply beyond
232                // closing: RFC 4271 forbids answering a NOTIFICATION with a NOTIFICATION.
233                _ => (ERR_CEASE, 0),
234            },
235        };
236        Self { notify, detail }
237    }
238}
239
240/// Encode a message netgauze can represent (OPEN, UPDATE, KEEPALIVE, ROUTE-REFRESH).
241///
242/// The encoder is a pure function of the message value — in particular the codec's `asn4` flag
243/// does *not* influence the output, so the two/four-octet AS_PATH choice must be made when the
244/// [`AsPath`] is built. See [`build_update`].
245pub fn encode(msg: BgpMessage) -> Result<Vec<u8>> {
246    let mut codec = BgpCodec::default();
247    let mut out = BytesMut::new();
248    codec
249        .encode(msg, &mut out)
250        .map_err(|e| anyhow!("BGP encoding failed: {e:?}"))?;
251    if out.len() > BGP_MAX_MESSAGE_LEN {
252        bail!(
253            "BGP message is {} bytes, over the RFC 4271 maximum of {}",
254            out.len(),
255            BGP_MAX_MESSAGE_LEN
256        );
257    }
258    Ok(out.to_vec())
259}
260
261/// A bare 19-octet KEEPALIVE (RFC 4271 section 4.4).
262pub fn encode_keepalive() -> Vec<u8> {
263    let mut msg = Vec::with_capacity(BGP_HEADER_LEN);
264    msg.extend_from_slice(&BGP_MARKER);
265    msg.extend_from_slice(&(BGP_HEADER_LEN as u16).to_be_bytes());
266    msg.push(MSG_KEEPALIVE);
267    msg
268}
269
270/// A NOTIFICATION (RFC 4271 section 4.5): header, error code, error subcode, opaque data.
271///
272/// Hand-encoded on purpose — see the module comment.
273pub fn encode_notification(error_code: u8, error_subcode: u8, data: &[u8]) -> Result<Vec<u8>> {
274    let total = BGP_HEADER_LEN + 2 + data.len();
275    if total > BGP_MAX_MESSAGE_LEN {
276        bail!(
277            "BGP NOTIFICATION with {} data bytes exceeds the {}-byte maximum",
278            data.len(),
279            BGP_MAX_MESSAGE_LEN
280        );
281    }
282    let mut msg = Vec::with_capacity(total);
283    msg.extend_from_slice(&BGP_MARKER);
284    msg.extend_from_slice(&(total as u16).to_be_bytes());
285    msg.push(MSG_NOTIFICATION);
286    msg.push(error_code);
287    msg.push(error_subcode);
288    msg.extend_from_slice(data);
289    Ok(msg)
290}
291
292/// Build our OPEN.
293///
294/// RFC 6793: the two-octet `My Autonomous System` field carries [`AS_TRANS`] when the real ASN
295/// does not fit, and the real value always travels in the four-octet-AS capability. NetGet
296/// advertises that capability unconditionally, which is both what every current implementation
297/// does and what lets the read side decode AS_PATH correctly whenever the peer agrees.
298///
299/// The previous implementation wrote `local_as as u16` with no capability, so any ASN above
300/// 65535 was silently truncated to a different, valid-looking ASN.
301pub fn build_open(local_as: u32, hold_time: u16, router_id: Ipv4Addr) -> BgpMessage {
302    let my_as = u16::try_from(local_as).unwrap_or(AS_TRANS);
303    BgpMessage::Open(BgpOpenMessage::new(
304        my_as,
305        hold_time,
306        router_id,
307        vec![BgpOpenMessageParameter::Capabilities(vec![
308            BgpCapability::FourOctetAs(FourOctetAsCapability::new(local_as)),
309        ])],
310    ))
311}
312
313/// Parse `a.b.c.d/len` into a netgauze NLRI prefix, masking host bits.
314///
315/// Host bits matter: a prefix is written as `len` followed by `ceil(len/8)` octets, so
316/// `10.0.0.17/28` would otherwise put the `17` on the wire inside the host part.
317///
318/// The `Ipv4Unicast` newtype wraps `ipnet::Ipv4Net`, which this crate does not depend on
319/// directly; serde is the available route to a value. It is a plain string round-trip and it
320/// bypasses netgauze's own unicast check, so the multicast/broadcast rejection is done here.
321pub fn ipv4_unicast(prefix: &str) -> Result<Ipv4Unicast> {
322    let (addr_str, len_str) = prefix
323        .split_once('/')
324        .with_context(|| format!("BGP prefix {prefix:?} must be in CIDR form, e.g. 10.0.0.0/24"))?;
325    let addr: Ipv4Addr = addr_str
326        .parse()
327        .with_context(|| format!("BGP prefix {prefix:?} has an invalid IPv4 address"))?;
328    let len: u8 = len_str
329        .parse()
330        .with_context(|| format!("BGP prefix {prefix:?} has an invalid prefix length"))?;
331    if len > 32 {
332        bail!("BGP prefix {prefix:?} has prefix length {len}, which exceeds 32");
333    }
334    if addr.is_multicast() || addr.is_broadcast() {
335        bail!("BGP prefix {prefix:?} is not a unicast address");
336    }
337
338    let mask: u32 = if len == 0 {
339        0
340    } else {
341        u32::MAX << (32 - u32::from(len))
342    };
343    let network = Ipv4Addr::from(u32::from(addr) & mask);
344
345    serde_json::from_value::<Ipv4Unicast>(serde_json::Value::String(format!("{network}/{len}")))
346        .with_context(|| format!("BGP prefix {prefix:?} is not a valid IPv4 network"))
347}
348
349/// Everything `send_bgp_update` needs, already validated.
350#[derive(Debug, Clone)]
351pub struct UpdateIntent {
352    pub withdrawn: Vec<String>,
353    pub nlri: Vec<String>,
354    pub next_hop: Option<Ipv4Addr>,
355    pub as_path: Vec<u32>,
356    pub origin: Origin,
357    pub med: Option<u32>,
358    pub local_pref: Option<u32>,
359}
360
361/// Build an UPDATE.
362///
363/// `peer_asn4` is the negotiated four-octet-AS state and it changes the bytes on the wire:
364///
365/// * negotiated — AS_PATH carries four-octet ASNs (RFC 6793 section 4).
366/// * not negotiated — AS_PATH must carry two-octet ASNs. Any ASN that does not fit is replaced
367///   by [`AS_TRANS`] and the true path is added as the optional-transitive AS4_PATH attribute.
368///   Sending a four-octet AS_PATH to a peer that did not negotiate it is a protocol violation
369///   that a real router answers with NOTIFICATION 3/11 (Malformed AS_PATH).
370///
371/// RFC 4271 section 9.1 makes ORIGIN, AS_PATH and NEXT_HOP mandatory whenever NLRI is present;
372/// a withdrawal-only UPDATE carries no attributes at all.
373pub fn build_update(intent: &UpdateIntent, peer_asn4: bool) -> Result<BgpMessage> {
374    let withdrawn = intent
375        .withdrawn
376        .iter()
377        .map(|p| ipv4_unicast(p).map(Ipv4UnicastAddress::new_no_path_id))
378        .collect::<Result<Vec<_>>>()?;
379    let nlri = intent
380        .nlri
381        .iter()
382        .map(|p| ipv4_unicast(p).map(Ipv4UnicastAddress::new_no_path_id))
383        .collect::<Result<Vec<_>>>()?;
384
385    if withdrawn.is_empty() && nlri.is_empty() {
386        bail!("send_bgp_update needs at least one prefix in nlri or withdrawn_routes");
387    }
388
389    let mut attrs: Vec<PathAttribute> = Vec::new();
390
391    if !nlri.is_empty() {
392        let next_hop = intent
393            .next_hop
394            .context("send_bgp_update with nlri requires next_hop (RFC 4271 makes NEXT_HOP mandatory for an announcement)")?;
395
396        attrs.push(well_known(PathAttributeValue::Origin(intent.origin))?);
397
398        if peer_asn4 {
399            let segments = if intent.as_path.is_empty() {
400                vec![]
401            } else {
402                vec![As4PathSegment::new(
403                    AsPathSegmentType::AsSequence,
404                    intent.as_path.clone(),
405                )]
406            };
407            attrs.push(well_known(PathAttributeValue::AsPath(
408                AsPath::As4PathSegments(segments),
409            ))?);
410        } else {
411            let needs_as4 = intent.as_path.iter().any(|&asn| asn > u32::from(u16::MAX));
412            let segments = if intent.as_path.is_empty() {
413                vec![]
414            } else {
415                vec![As2PathSegment::new(
416                    AsPathSegmentType::AsSequence,
417                    intent
418                        .as_path
419                        .iter()
420                        .map(|&asn| u16::try_from(asn).unwrap_or(AS_TRANS))
421                        .collect(),
422                )]
423            };
424            attrs.push(well_known(PathAttributeValue::AsPath(
425                AsPath::As2PathSegments(segments),
426            ))?);
427            if needs_as4 {
428                // Optional (true), transitive (true), not partial.
429                attrs.push(
430                    PathAttribute::from(
431                        true,
432                        true,
433                        false,
434                        false,
435                        PathAttributeValue::As4Path(As4Path::new(vec![As4PathSegment::new(
436                            AsPathSegmentType::AsSequence,
437                            intent.as_path.clone(),
438                        )])),
439                    )
440                    .map_err(|(_, e)| anyhow!("invalid AS4_PATH attribute: {e:?}"))?,
441                );
442            }
443        }
444
445        attrs.push(well_known(PathAttributeValue::NextHop(NextHop::new(
446            next_hop,
447        )))?);
448
449        if let Some(local_pref) = intent.local_pref {
450            attrs.push(well_known(PathAttributeValue::LocalPreference(
451                LocalPreference::new(local_pref),
452            ))?);
453        }
454        if let Some(med) = intent.med {
455            // MULTI_EXIT_DISC is optional non-transitive.
456            attrs.push(
457                PathAttribute::from(
458                    true,
459                    false,
460                    false,
461                    false,
462                    PathAttributeValue::MultiExitDiscriminator(MultiExitDiscriminator::new(med)),
463                )
464                .map_err(|(_, e)| anyhow!("invalid MULTI_EXIT_DISC attribute: {e:?}"))?,
465            );
466        }
467    }
468
469    Ok(BgpMessage::Update(BgpUpdateMessage::new(
470        withdrawn, attrs, nlri,
471    )))
472}
473
474/// Well-known mandatory: optional=false, transitive=true, partial=false, one-octet length.
475fn well_known(value: PathAttributeValue) -> Result<PathAttribute> {
476    PathAttribute::from(false, true, false, false, value)
477        .map_err(|(_, e)| anyhow!("invalid well-known path attribute: {e:?}"))
478}
479
480/// Render a decoded UPDATE as the structured JSON handed to the model.
481///
482/// Deliberately field-per-concept rather than a hex blob: the root CLAUDE.md forbids putting
483/// raw bytes in event data, and this body used to be delivered as `hex::encode(body)`, which no
484/// model can act on.
485pub fn update_to_json(update: &BgpUpdateMessage) -> serde_json::Value {
486    let withdrawn: Vec<String> = update
487        .withdraw_routes()
488        .iter()
489        .map(|r| r.network().to_string())
490        .collect();
491    let nlri: Vec<String> = update
492        .nlri()
493        .iter()
494        .map(|r| r.network().to_string())
495        .collect();
496
497    let attributes: Vec<serde_json::Value> = update
498        .path_attributes()
499        .iter()
500        .map(path_attribute_to_json)
501        .collect();
502
503    // Convenience projections so a handler does not have to walk the attribute list for the
504    // three fields a routing decision almost always turns on.
505    let mut origin = serde_json::Value::Null;
506    let mut next_hop = serde_json::Value::Null;
507    let mut as_path = serde_json::Value::Null;
508    for attr in update.path_attributes() {
509        match attr.value() {
510            PathAttributeValue::Origin(o) => origin = serde_json::json!(format!("{o:?}")),
511            PathAttributeValue::NextHop(n) => {
512                next_hop = serde_json::json!(n.next_hop().to_string())
513            }
514            PathAttributeValue::AsPath(p) => {
515                as_path = serde_json::json!(Vec::<u32>::from(p.clone()))
516            }
517            _ => {}
518        }
519    }
520
521    serde_json::json!({
522        "withdrawn_routes": withdrawn,
523        "nlri": nlri,
524        "origin": origin,
525        "next_hop": next_hop,
526        "as_path": as_path,
527        "path_attributes": attributes,
528        "end_of_rib": update.end_of_rib().is_some(),
529    })
530}
531
532fn path_attribute_to_json(attr: &PathAttribute) -> serde_json::Value {
533    let type_name = match attr.path_attribute_type() {
534        Ok(t) => format!("{t:?}"),
535        Err(code) => format!("UNKNOWN({code})"),
536    };
537    let mut out = serde_json::json!({
538        "type_name": type_name,
539        "optional": attr.optional(),
540        "transitive": attr.transitive(),
541    });
542
543    match attr.value() {
544        PathAttributeValue::Origin(o) => out["origin"] = serde_json::json!(format!("{o:?}")),
545        PathAttributeValue::AsPath(p) => {
546            out["as_path"] = serde_json::json!(Vec::<u32>::from(p.clone()));
547            out["four_octet"] = serde_json::json!(matches!(p, AsPath::As4PathSegments(_)));
548        }
549        PathAttributeValue::As4Path(p) => {
550            let asns: Vec<u32> = p
551                .segments()
552                .iter()
553                .flat_map(|s| s.as_numbers().iter().copied())
554                .collect();
555            out["as4_path"] = serde_json::json!(asns);
556        }
557        PathAttributeValue::NextHop(n) => {
558            out["next_hop"] = serde_json::json!(n.next_hop().to_string())
559        }
560        PathAttributeValue::MultiExitDiscriminator(m) => out["med"] = serde_json::json!(m.metric()),
561        PathAttributeValue::LocalPreference(l) => out["local_pref"] = serde_json::json!(l.metric()),
562        PathAttributeValue::AtomicAggregate(_) => out["atomic_aggregate"] = serde_json::json!(true),
563        PathAttributeValue::Communities(c) => {
564            let communities: Vec<String> = c
565                .communities()
566                .iter()
567                .map(|c| format!("{}:{}", c.collection_asn(), c.collection_value()))
568                .collect();
569            out["communities"] = serde_json::json!(communities);
570        }
571        // Everything else (MP_REACH/MP_UNREACH, aggregator, large/extended communities,
572        // BGP-LS, ...) is reported by name only. It is parsed correctly by netgauze; it is
573        // simply not projected into a shape a model can usefully reason about yet.
574        _ => {}
575    }
576    out
577}
578
579/// Describe our own capabilities and the peer's for the `bgp_open` event.
580pub fn capabilities_to_json(open: &BgpOpenMessage) -> serde_json::Value {
581    let names: Vec<String> = open
582        .capabilities()
583        .into_iter()
584        .map(|c| match c {
585            BgpCapability::FourOctetAs(a) => format!("four_octet_as({})", a.asn4()),
586            other => format!("{other:?}")
587                .split_whitespace()
588                .next()
589                .unwrap_or("unknown")
590                .trim_end_matches('(')
591                .to_string(),
592        })
593        .collect();
594    serde_json::json!(names)
595}
596
597/// Human-readable RFC 4271 section 6 error names, for logs and for the model.
598pub fn error_name(code: u8) -> &'static str {
599    match code {
600        1 => "Message Header Error",
601        2 => "OPEN Message Error",
602        3 => "UPDATE Message Error",
603        4 => "Hold Timer Expired",
604        5 => "Finite State Machine Error",
605        6 => "Cease",
606        _ => "Unknown",
607    }
608}
609
610pub fn error_subcode_name(code: u8, subcode: u8) -> &'static str {
611    match (code, subcode) {
612        (1, 1) => "Connection Not Synchronized",
613        (1, 2) => "Bad Message Length",
614        (1, 3) => "Bad Message Type",
615        (2, 1) => "Unsupported Version Number",
616        (2, 2) => "Bad Peer AS",
617        (2, 3) => "Bad BGP Identifier",
618        (2, 4) => "Unsupported Optional Parameter",
619        (2, 6) => "Unacceptable Hold Time",
620        (2, 7) => "Unsupported Capability",
621        (3, 1) => "Malformed Attribute List",
622        (3, 2) => "Unrecognized Well-known Attribute",
623        (3, 3) => "Missing Well-known Attribute",
624        (3, 4) => "Attribute Flags Error",
625        (3, 5) => "Attribute Length Error",
626        (3, 6) => "Invalid ORIGIN Attribute",
627        (3, 8) => "Invalid NEXT_HOP Attribute",
628        (3, 9) => "Optional Attribute Error",
629        (3, 10) => "Invalid Network Field",
630        (3, 11) => "Malformed AS_PATH",
631        (5, 1) => "Unexpected Message in OpenSent State",
632        (5, 2) => "Unexpected Message in OpenConfirm State",
633        (5, 3) => "Unexpected Message in Established State",
634        (6, 1) => "Maximum Number of Prefixes Reached",
635        (6, 2) => "Administrative Shutdown",
636        (6, 3) => "Peer De-configured",
637        (6, 4) => "Administrative Reset",
638        (6, 5) => "Connection Rejected",
639        (6, 6) => "Other Configuration Change",
640        (6, 7) => "Connection Collision Resolution",
641        (6, 8) => "Out of Resources",
642        _ => "Unspecified",
643    }
644}
645
646/// Turn the validated intent produced by a `send_bgp_*` action into wire bytes.
647///
648/// The action executor cannot do this itself: `Protocol::execute_action` is a pure function of
649/// the action JSON with no access to the session, and the correct encoding of an UPDATE depends
650/// on whether four-octet AS was negotiated with *this* peer. So the executor validates and
651/// normalises, returning `ActionResult::Custom`, and the session calls this with the negotiated
652/// state.
653pub fn encode_intent(intent: &serde_json::Value, peer_asn4: bool) -> Result<Vec<u8>> {
654    let kind = intent
655        .get("kind")
656        .and_then(|v| v.as_str())
657        .context("BGP action intent is missing its 'kind'")?;
658
659    match kind {
660        "open" => {
661            let my_as = intent
662                .get("my_as")
663                .and_then(|v| v.as_u64())
664                .context("BGP open intent is missing my_as")? as u32;
665            let hold_time = intent
666                .get("hold_time")
667                .and_then(|v| v.as_u64())
668                .context("BGP open intent is missing hold_time")?
669                as u16;
670            let router_id: Ipv4Addr = intent
671                .get("router_id")
672                .and_then(|v| v.as_str())
673                .context("BGP open intent is missing router_id")?
674                .parse()
675                .context("BGP open intent has an invalid router_id")?;
676            encode(build_open(my_as, hold_time, router_id))
677        }
678        "keepalive" => Ok(encode_keepalive()),
679        "notification" => {
680            let code = intent
681                .get("error_code")
682                .and_then(|v| v.as_u64())
683                .context("BGP notification intent is missing error_code")?
684                as u8;
685            let subcode = intent
686                .get("error_subcode")
687                .and_then(|v| v.as_u64())
688                .unwrap_or(0) as u8;
689            let data = match intent.get("data").and_then(|v| v.as_str()) {
690                Some(hex_str) if !hex_str.is_empty() => {
691                    hex::decode(hex_str).context("BGP notification intent has non-hex data")?
692                }
693                _ => Vec::new(),
694            };
695            encode_notification(code, subcode, &data)
696        }
697        "update" => {
698            let strings = |key: &str| -> Vec<String> {
699                intent
700                    .get(key)
701                    .and_then(|v| v.as_array())
702                    .map(|a| {
703                        a.iter()
704                            .filter_map(|v| v.as_str().map(str::to_string))
705                            .collect()
706                    })
707                    .unwrap_or_default()
708            };
709            let next_hop = match intent.get("next_hop").and_then(|v| v.as_str()) {
710                Some(s) => Some(
711                    s.parse::<Ipv4Addr>()
712                        .context("BGP update intent has an invalid next_hop")?,
713                ),
714                None => None,
715            };
716            let origin = match intent.get("origin").and_then(|v| v.as_str()) {
717                Some("EGP") => Origin::EGP,
718                Some("INCOMPLETE") => Origin::Incomplete,
719                _ => Origin::IGP,
720            };
721            let intent = UpdateIntent {
722                withdrawn: strings("withdrawn_routes"),
723                nlri: strings("nlri"),
724                next_hop,
725                as_path: intent
726                    .get("as_path")
727                    .and_then(|v| v.as_array())
728                    .map(|a| {
729                        a.iter()
730                            .filter_map(|v| v.as_u64())
731                            .map(|n| n as u32)
732                            .collect()
733                    })
734                    .unwrap_or_default(),
735                origin,
736                med: intent.get("med").and_then(|v| v.as_u64()).map(|n| n as u32),
737                local_pref: intent
738                    .get("local_pref")
739                    .and_then(|v| v.as_u64())
740                    .map(|n| n as u32),
741            };
742            encode(build_update(&intent, peer_asn4)?)
743        }
744        other => bail!("unknown BGP action intent kind {other:?}"),
745    }
746}