Skip to main content

rustbgpd_wire/
notification.rs

1/// RFC 4271 §4.5 — NOTIFICATION error codes.
2///
3/// Known codes (1–6) have named variants. Unknown codes from the wire are
4/// preserved via `Unknown(u8)` so the original byte is never lost.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum NotificationCode {
7    /// Error in the message header (code 1).
8    MessageHeader,
9    /// Error in the OPEN message (code 2).
10    OpenMessage,
11    /// Error in the UPDATE message (code 3).
12    UpdateMessage,
13    /// Hold timer expired without receiving a KEEPALIVE or UPDATE (code 4).
14    HoldTimerExpired,
15    /// Finite state machine error (code 5).
16    FsmError,
17    /// Administrative or resource-related session termination (code 6).
18    Cease,
19    /// A code value not defined in RFC 4271. The raw byte is preserved
20    /// for logging and re-encoding.
21    Unknown(u8),
22}
23
24impl NotificationCode {
25    /// Create from a raw code byte, mapping known values to named variants.
26    #[must_use]
27    pub fn from_u8(value: u8) -> Self {
28        match value {
29            1 => Self::MessageHeader,
30            2 => Self::OpenMessage,
31            3 => Self::UpdateMessage,
32            4 => Self::HoldTimerExpired,
33            5 => Self::FsmError,
34            6 => Self::Cease,
35            other => Self::Unknown(other),
36        }
37    }
38
39    /// Return the raw byte value for this error code.
40    #[must_use]
41    pub fn as_u8(self) -> u8 {
42        match self {
43            Self::MessageHeader => 1,
44            Self::OpenMessage => 2,
45            Self::UpdateMessage => 3,
46            Self::HoldTimerExpired => 4,
47            Self::FsmError => 5,
48            Self::Cease => 6,
49            Self::Unknown(v) => v,
50        }
51    }
52}
53
54impl std::fmt::Display for NotificationCode {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::MessageHeader => write!(f, "Message Header Error"),
58            Self::OpenMessage => write!(f, "OPEN Message Error"),
59            Self::UpdateMessage => write!(f, "UPDATE Message Error"),
60            Self::HoldTimerExpired => write!(f, "Hold Timer Expired"),
61            Self::FsmError => write!(f, "Finite State Machine Error"),
62            Self::Cease => write!(f, "Cease"),
63            Self::Unknown(code) => write!(f, "Unknown({code})"),
64        }
65    }
66}
67
68/// Message Header Error subcodes (code 1).
69pub mod header_subcode {
70    /// Subcode 1: Connection Not Synchronized.
71    pub const CONNECTION_NOT_SYNCHRONIZED: u8 = 1;
72    /// Subcode 2: Bad Message Length.
73    pub const BAD_MESSAGE_LENGTH: u8 = 2;
74    /// Subcode 3: Bad Message Type.
75    pub const BAD_MESSAGE_TYPE: u8 = 3;
76}
77
78/// OPEN Message Error subcodes (code 2).
79pub mod open_subcode {
80    /// Subcode 1: Unsupported Version Number.
81    pub const UNSUPPORTED_VERSION: u8 = 1;
82    /// Subcode 2: Bad Peer AS.
83    pub const BAD_PEER_AS: u8 = 2;
84    /// Subcode 3: Bad BGP Identifier.
85    pub const BAD_BGP_IDENTIFIER: u8 = 3;
86    /// Subcode 4: Unsupported Optional Parameter.
87    pub const UNSUPPORTED_OPTIONAL_PARAMETER: u8 = 4;
88    // subcode 5 deprecated (Authentication Failure)
89    /// Subcode 6: Unacceptable Hold Time.
90    pub const UNACCEPTABLE_HOLD_TIME: u8 = 6;
91    /// Subcode 7: Unsupported Capability (RFC 5492).
92    pub const UNSUPPORTED_CAPABILITY: u8 = 7;
93}
94
95/// UPDATE Message Error subcodes (code 3).
96pub mod update_subcode {
97    /// Subcode 1: Malformed Attribute List.
98    pub const MALFORMED_ATTRIBUTE_LIST: u8 = 1;
99    /// Subcode 2: Unrecognized Well-known Attribute.
100    pub const UNRECOGNIZED_WELLKNOWN: u8 = 2;
101    /// Subcode 3: Missing Well-known Attribute.
102    pub const MISSING_WELLKNOWN: u8 = 3;
103    /// Subcode 4: Attribute Flags Error.
104    pub const ATTRIBUTE_FLAGS_ERROR: u8 = 4;
105    /// Subcode 5: Attribute Length Error.
106    pub const ATTRIBUTE_LENGTH_ERROR: u8 = 5;
107    /// Subcode 6: Invalid `ORIGIN` Attribute.
108    pub const INVALID_ORIGIN: u8 = 6;
109    // subcode 7 deprecated (AS Routing Loop)
110    /// Subcode 8: Invalid `NEXT_HOP` Attribute.
111    pub const INVALID_NEXT_HOP: u8 = 8;
112    /// Subcode 9: Optional Attribute Error.
113    pub const OPTIONAL_ATTRIBUTE_ERROR: u8 = 9;
114    /// Subcode 10: Invalid Network Field.
115    pub const INVALID_NETWORK_FIELD: u8 = 10;
116    /// Subcode 11: Malformed `AS_PATH`.
117    pub const MALFORMED_AS_PATH: u8 = 11;
118}
119
120/// Cease subcodes (code 6, RFC 4486).
121pub mod cease_subcode {
122    /// Subcode 1: Maximum Number of Prefixes Reached.
123    pub const MAX_PREFIXES: u8 = 1;
124    /// Subcode 2: Administrative Shutdown (RFC 8203).
125    pub const ADMINISTRATIVE_SHUTDOWN: u8 = 2;
126    /// Subcode 3: Peer De-configured.
127    pub const PEER_DECONFIGURED: u8 = 3;
128    /// Subcode 4: Administrative Reset (RFC 8203).
129    pub const ADMINISTRATIVE_RESET: u8 = 4;
130    /// Subcode 8: Out of Resources.
131    pub const OUT_OF_RESOURCES: u8 = 8;
132    /// RFC 4271 §6.8
133    pub const CONNECTION_COLLISION_RESOLUTION: u8 = 7;
134    /// RFC 8538
135    pub const HARD_RESET: u8 = 9;
136}
137
138/// Encode a shutdown communication reason string (RFC 8203).
139///
140/// The format is: 1-byte length prefix + UTF-8 string, max 128 bytes.
141/// If the reason exceeds 128 bytes, it is truncated at a char boundary.
142/// An empty reason encodes as a zero-length field (`[0]`).
143#[must_use]
144pub fn encode_shutdown_communication(reason: &str) -> bytes::Bytes {
145    // Truncate to at most 128 bytes at a char boundary
146    let mut end = reason.len().min(128);
147    while end > 0 && !reason.is_char_boundary(end) {
148        end -= 1;
149    }
150    let truncated = &reason[..end];
151    // Safe: end ≤ 128, which always fits in u8.
152    #[expect(clippy::cast_possible_truncation)]
153    let len = truncated.len() as u8;
154    let mut buf = Vec::with_capacity(1 + truncated.len());
155    buf.push(len);
156    buf.extend_from_slice(truncated.as_bytes());
157    bytes::Bytes::from(buf)
158}
159
160/// Decode a shutdown communication reason string from NOTIFICATION data (RFC 8203).
161///
162/// Returns `None` if the data is empty or the length prefix is inconsistent.
163/// Extra trailing bytes after the declared shutdown-communication string are ignored.
164/// Invalid UTF-8 is replaced with the Unicode replacement character.
165#[must_use]
166pub fn decode_shutdown_communication(data: &[u8]) -> Option<String> {
167    if data.is_empty() {
168        return None;
169    }
170    let len = data[0] as usize;
171    if data.len() < 1 + len {
172        return None;
173    }
174    let raw = &data[1..=len];
175    Some(String::from_utf8_lossy(raw).into_owned())
176}
177
178/// Human-readable description for a NOTIFICATION code/subcode pair.
179#[must_use]
180pub fn description(code: NotificationCode, subcode: u8) -> &'static str {
181    match (code, subcode) {
182        // Message Header Error
183        (NotificationCode::MessageHeader, 1) => "Connection Not Synchronized",
184        (NotificationCode::MessageHeader, 2) => "Bad Message Length",
185        (NotificationCode::MessageHeader, 3) => "Bad Message Type",
186        // OPEN Message Error
187        (NotificationCode::OpenMessage, 1) => "Unsupported Version Number",
188        (NotificationCode::OpenMessage, 2) => "Bad Peer AS",
189        (NotificationCode::OpenMessage, 3) => "Bad BGP Identifier",
190        (NotificationCode::OpenMessage, 4) => "Unsupported Optional Parameter",
191        (NotificationCode::OpenMessage, 6) => "Unacceptable Hold Time",
192        (NotificationCode::OpenMessage, 7) => "Unsupported Capability",
193        // UPDATE Message Error
194        (NotificationCode::UpdateMessage, 1) => "Malformed Attribute List",
195        (NotificationCode::UpdateMessage, 2) => "Unrecognized Well-known Attribute",
196        (NotificationCode::UpdateMessage, 3) => "Missing Well-known Attribute",
197        (NotificationCode::UpdateMessage, 4) => "Attribute Flags Error",
198        (NotificationCode::UpdateMessage, 5) => "Attribute Length Error",
199        (NotificationCode::UpdateMessage, 6) => "Invalid ORIGIN Attribute",
200        (NotificationCode::UpdateMessage, 8) => "Invalid NEXT_HOP Attribute",
201        (NotificationCode::UpdateMessage, 9) => "Optional Attribute Error",
202        (NotificationCode::UpdateMessage, 10) => "Invalid Network Field",
203        (NotificationCode::UpdateMessage, 11) => "Malformed AS_PATH",
204        // Hold Timer Expired
205        (NotificationCode::HoldTimerExpired, _) => "Hold Timer Expired",
206        // FSM Error
207        (NotificationCode::FsmError, _) => "Finite State Machine Error",
208        // Cease
209        (NotificationCode::Cease, 1) => "Maximum Number of Prefixes Reached",
210        (NotificationCode::Cease, 2) => "Administrative Shutdown",
211        (NotificationCode::Cease, 3) => "Peer De-configured",
212        (NotificationCode::Cease, 4) => "Administrative Reset",
213        (NotificationCode::Cease, 8) => "Out of Resources",
214        (NotificationCode::Cease, 7) => "Connection Collision Resolution",
215        (NotificationCode::Cease, 9) => "Hard Reset",
216        // Unknown code
217        (NotificationCode::Unknown(_), _) => "Unknown Error Code",
218        // Fallback for known code with unknown subcode
219        (_, _) => "Unknown",
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn from_u8_roundtrip() {
229        for code_val in 1..=6u8 {
230            let code = NotificationCode::from_u8(code_val);
231            assert_eq!(code.as_u8(), code_val);
232            assert!(!matches!(code, NotificationCode::Unknown(_)));
233        }
234    }
235
236    #[test]
237    fn from_u8_unknown_preserved() {
238        assert_eq!(NotificationCode::from_u8(0), NotificationCode::Unknown(0));
239        assert_eq!(NotificationCode::from_u8(7), NotificationCode::Unknown(7));
240        assert_eq!(
241            NotificationCode::from_u8(255),
242            NotificationCode::Unknown(255)
243        );
244        // Raw byte survives roundtrip
245        assert_eq!(NotificationCode::from_u8(42).as_u8(), 42);
246    }
247
248    #[test]
249    fn description_returns_nonempty_for_known_pairs() {
250        let pairs = [
251            (NotificationCode::MessageHeader, 1),
252            (NotificationCode::MessageHeader, 2),
253            (NotificationCode::MessageHeader, 3),
254            (NotificationCode::OpenMessage, 1),
255            (NotificationCode::OpenMessage, 6),
256            (NotificationCode::UpdateMessage, 1),
257            (NotificationCode::UpdateMessage, 11),
258            (NotificationCode::Cease, 2),
259            (NotificationCode::Cease, 4),
260        ];
261        for (code, subcode) in pairs {
262            let desc = description(code, subcode);
263            assert!(
264                !desc.is_empty(),
265                "empty description for ({code}, {subcode})"
266            );
267            assert_ne!(desc, "Unknown", "got Unknown for ({code}, {subcode})");
268        }
269    }
270
271    #[test]
272    fn shutdown_communication_roundtrip() {
273        let reason = "maintenance window";
274        let encoded = encode_shutdown_communication(reason);
275        assert_eq!(encoded[0] as usize, reason.len());
276        let decoded = decode_shutdown_communication(&encoded).unwrap();
277        assert_eq!(decoded, reason);
278    }
279
280    #[test]
281    fn shutdown_communication_empty() {
282        let encoded = encode_shutdown_communication("");
283        assert_eq!(encoded.as_ref(), &[0]);
284        assert_eq!(decode_shutdown_communication(&encoded).as_deref(), Some(""));
285        assert_eq!(decode_shutdown_communication(&[]), None);
286    }
287
288    #[test]
289    fn shutdown_communication_truncates_at_128() {
290        let long = "a".repeat(200);
291        let encoded = encode_shutdown_communication(&long);
292        assert_eq!(encoded[0], 128);
293        assert_eq!(encoded.len(), 129);
294        let decoded = decode_shutdown_communication(&encoded).unwrap();
295        assert_eq!(decoded.len(), 128);
296    }
297
298    #[test]
299    fn shutdown_communication_truncates_at_char_boundary() {
300        // 'é' is 2 bytes in UTF-8. Fill 127 bytes + 'é' = 129 bytes total → truncate
301        let reason = format!("{}é", "x".repeat(127));
302        assert_eq!(reason.len(), 129);
303        let encoded = encode_shutdown_communication(&reason);
304        // Should truncate to 127 bytes (before the multi-byte char)
305        assert_eq!(encoded[0], 127);
306        let decoded = decode_shutdown_communication(&encoded).unwrap();
307        assert_eq!(decoded, "x".repeat(127));
308    }
309
310    #[test]
311    fn shutdown_communication_invalid_utf8() {
312        // Length 3 + 3 bytes of invalid UTF-8
313        let data = [3, 0xff, 0xfe, 0xfd];
314        let decoded = decode_shutdown_communication(&data).unwrap();
315        assert!(decoded.contains('\u{FFFD}')); // replacement char
316    }
317
318    #[test]
319    fn shutdown_communication_ignores_trailing_bytes() {
320        let data = [3, b'f', b'o', b'o', b'x'];
321        assert_eq!(decode_shutdown_communication(&data).as_deref(), Some("foo"));
322    }
323}