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    /// Subcode 11: Role Mismatch (RFC 9234 §4.2).
94    pub const ROLE_MISMATCH: u8 = 11;
95}
96
97/// UPDATE Message Error subcodes (code 3).
98pub mod update_subcode {
99    /// Subcode 1: Malformed Attribute List.
100    pub const MALFORMED_ATTRIBUTE_LIST: u8 = 1;
101    /// Subcode 2: Unrecognized Well-known Attribute.
102    pub const UNRECOGNIZED_WELLKNOWN: u8 = 2;
103    /// Subcode 3: Missing Well-known Attribute.
104    pub const MISSING_WELLKNOWN: u8 = 3;
105    /// Subcode 4: Attribute Flags Error.
106    pub const ATTRIBUTE_FLAGS_ERROR: u8 = 4;
107    /// Subcode 5: Attribute Length Error.
108    pub const ATTRIBUTE_LENGTH_ERROR: u8 = 5;
109    /// Subcode 6: Invalid `ORIGIN` Attribute.
110    pub const INVALID_ORIGIN: u8 = 6;
111    // subcode 7 deprecated (AS Routing Loop)
112    /// Subcode 8: Invalid `NEXT_HOP` Attribute.
113    pub const INVALID_NEXT_HOP: u8 = 8;
114    /// Subcode 9: Optional Attribute Error.
115    pub const OPTIONAL_ATTRIBUTE_ERROR: u8 = 9;
116    /// Subcode 10: Invalid Network Field.
117    pub const INVALID_NETWORK_FIELD: u8 = 10;
118    /// Subcode 11: Malformed `AS_PATH`.
119    pub const MALFORMED_AS_PATH: u8 = 11;
120}
121
122/// Cease subcodes (code 6, RFC 4486).
123pub mod cease_subcode {
124    /// Subcode 1: Maximum Number of Prefixes Reached.
125    pub const MAX_PREFIXES: u8 = 1;
126    /// Subcode 2: Administrative Shutdown (RFC 8203).
127    pub const ADMINISTRATIVE_SHUTDOWN: u8 = 2;
128    /// Subcode 3: Peer De-configured.
129    pub const PEER_DECONFIGURED: u8 = 3;
130    /// Subcode 4: Administrative Reset (RFC 8203).
131    pub const ADMINISTRATIVE_RESET: u8 = 4;
132    /// Subcode 8: Out of Resources.
133    pub const OUT_OF_RESOURCES: u8 = 8;
134    /// RFC 4271 §6.8
135    pub const CONNECTION_COLLISION_RESOLUTION: u8 = 7;
136    /// RFC 8538
137    pub const HARD_RESET: u8 = 9;
138}
139
140/// Encode a shutdown communication reason string (RFC 8203).
141///
142/// The format is: 1-byte length prefix + UTF-8 string, max 128 bytes.
143/// If the reason exceeds 128 bytes, it is truncated at a char boundary.
144/// An empty reason encodes as a zero-length field (`[0]`).
145#[must_use]
146pub fn encode_shutdown_communication(reason: &str) -> bytes::Bytes {
147    // Truncate to at most 128 bytes at a char boundary
148    let mut end = reason.len().min(128);
149    while end > 0 && !reason.is_char_boundary(end) {
150        end -= 1;
151    }
152    let truncated = &reason[..end];
153    // Safe: end ≤ 128, which always fits in u8.
154    #[expect(clippy::cast_possible_truncation)]
155    let len = truncated.len() as u8;
156    let mut buf = Vec::with_capacity(1 + truncated.len());
157    buf.push(len);
158    buf.extend_from_slice(truncated.as_bytes());
159    bytes::Bytes::from(buf)
160}
161
162/// Decode a shutdown communication reason string from NOTIFICATION data (RFC 8203).
163///
164/// Returns `None` if the data is empty or the length prefix is inconsistent.
165/// Extra trailing bytes after the declared shutdown-communication string are ignored.
166/// Invalid UTF-8 is replaced with the Unicode replacement character.
167#[must_use]
168pub fn decode_shutdown_communication(data: &[u8]) -> Option<String> {
169    if data.is_empty() {
170        return None;
171    }
172    let len = data[0] as usize;
173    if data.len() < 1 + len {
174        return None;
175    }
176    let raw = &data[1..=len];
177    Some(String::from_utf8_lossy(raw).into_owned())
178}
179
180/// Human-readable description for a NOTIFICATION code/subcode pair.
181#[must_use]
182pub fn description(code: NotificationCode, subcode: u8) -> &'static str {
183    match (code, subcode) {
184        // Message Header Error
185        (NotificationCode::MessageHeader, 1) => "Connection Not Synchronized",
186        (NotificationCode::MessageHeader, 2) => "Bad Message Length",
187        (NotificationCode::MessageHeader, 3) => "Bad Message Type",
188        // OPEN Message Error
189        (NotificationCode::OpenMessage, 1) => "Unsupported Version Number",
190        (NotificationCode::OpenMessage, 2) => "Bad Peer AS",
191        (NotificationCode::OpenMessage, 3) => "Bad BGP Identifier",
192        (NotificationCode::OpenMessage, 4) => "Unsupported Optional Parameter",
193        (NotificationCode::OpenMessage, 6) => "Unacceptable Hold Time",
194        (NotificationCode::OpenMessage, 7) => "Unsupported Capability",
195        (NotificationCode::OpenMessage, 11) => "Role Mismatch",
196        // UPDATE Message Error
197        (NotificationCode::UpdateMessage, 1) => "Malformed Attribute List",
198        (NotificationCode::UpdateMessage, 2) => "Unrecognized Well-known Attribute",
199        (NotificationCode::UpdateMessage, 3) => "Missing Well-known Attribute",
200        (NotificationCode::UpdateMessage, 4) => "Attribute Flags Error",
201        (NotificationCode::UpdateMessage, 5) => "Attribute Length Error",
202        (NotificationCode::UpdateMessage, 6) => "Invalid ORIGIN Attribute",
203        (NotificationCode::UpdateMessage, 8) => "Invalid NEXT_HOP Attribute",
204        (NotificationCode::UpdateMessage, 9) => "Optional Attribute Error",
205        (NotificationCode::UpdateMessage, 10) => "Invalid Network Field",
206        (NotificationCode::UpdateMessage, 11) => "Malformed AS_PATH",
207        // Hold Timer Expired
208        (NotificationCode::HoldTimerExpired, _) => "Hold Timer Expired",
209        // FSM Error
210        (NotificationCode::FsmError, _) => "Finite State Machine Error",
211        // Cease
212        (NotificationCode::Cease, 1) => "Maximum Number of Prefixes Reached",
213        (NotificationCode::Cease, 2) => "Administrative Shutdown",
214        (NotificationCode::Cease, 3) => "Peer De-configured",
215        (NotificationCode::Cease, 4) => "Administrative Reset",
216        (NotificationCode::Cease, 8) => "Out of Resources",
217        (NotificationCode::Cease, 7) => "Connection Collision Resolution",
218        (NotificationCode::Cease, 9) => "Hard Reset",
219        // Unknown code
220        (NotificationCode::Unknown(_), _) => "Unknown Error Code",
221        // Fallback for known code with unknown subcode
222        (_, _) => "Unknown",
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn from_u8_roundtrip() {
232        for code_val in 1..=6u8 {
233            let code = NotificationCode::from_u8(code_val);
234            assert_eq!(code.as_u8(), code_val);
235            assert!(!matches!(code, NotificationCode::Unknown(_)));
236        }
237    }
238
239    #[test]
240    fn from_u8_unknown_preserved() {
241        assert_eq!(NotificationCode::from_u8(0), NotificationCode::Unknown(0));
242        assert_eq!(NotificationCode::from_u8(7), NotificationCode::Unknown(7));
243        assert_eq!(
244            NotificationCode::from_u8(255),
245            NotificationCode::Unknown(255)
246        );
247        // Raw byte survives roundtrip
248        assert_eq!(NotificationCode::from_u8(42).as_u8(), 42);
249    }
250
251    #[test]
252    fn description_returns_nonempty_for_known_pairs() {
253        let pairs = [
254            (NotificationCode::MessageHeader, 1),
255            (NotificationCode::MessageHeader, 2),
256            (NotificationCode::MessageHeader, 3),
257            (NotificationCode::OpenMessage, 1),
258            (NotificationCode::OpenMessage, 6),
259            (NotificationCode::UpdateMessage, 1),
260            (NotificationCode::UpdateMessage, 11),
261            (NotificationCode::Cease, 2),
262            (NotificationCode::Cease, 4),
263        ];
264        for (code, subcode) in pairs {
265            let desc = description(code, subcode);
266            assert!(
267                !desc.is_empty(),
268                "empty description for ({code}, {subcode})"
269            );
270            assert_ne!(desc, "Unknown", "got Unknown for ({code}, {subcode})");
271        }
272    }
273
274    #[test]
275    fn shutdown_communication_roundtrip() {
276        let reason = "maintenance window";
277        let encoded = encode_shutdown_communication(reason);
278        assert_eq!(encoded[0] as usize, reason.len());
279        let decoded = decode_shutdown_communication(&encoded).unwrap();
280        assert_eq!(decoded, reason);
281    }
282
283    #[test]
284    fn shutdown_communication_empty() {
285        let encoded = encode_shutdown_communication("");
286        assert_eq!(encoded.as_ref(), &[0]);
287        assert_eq!(decode_shutdown_communication(&encoded).as_deref(), Some(""));
288        assert_eq!(decode_shutdown_communication(&[]), None);
289    }
290
291    #[test]
292    fn shutdown_communication_truncates_at_128() {
293        let long = "a".repeat(200);
294        let encoded = encode_shutdown_communication(&long);
295        assert_eq!(encoded[0], 128);
296        assert_eq!(encoded.len(), 129);
297        let decoded = decode_shutdown_communication(&encoded).unwrap();
298        assert_eq!(decoded.len(), 128);
299    }
300
301    #[test]
302    fn shutdown_communication_truncates_at_char_boundary() {
303        // 'é' is 2 bytes in UTF-8. Fill 127 bytes + 'é' = 129 bytes total → truncate
304        let reason = format!("{}é", "x".repeat(127));
305        assert_eq!(reason.len(), 129);
306        let encoded = encode_shutdown_communication(&reason);
307        // Should truncate to 127 bytes (before the multi-byte char)
308        assert_eq!(encoded[0], 127);
309        let decoded = decode_shutdown_communication(&encoded).unwrap();
310        assert_eq!(decoded, "x".repeat(127));
311    }
312
313    #[test]
314    fn shutdown_communication_invalid_utf8() {
315        // Length 3 + 3 bytes of invalid UTF-8
316        let data = [3, 0xff, 0xfe, 0xfd];
317        let decoded = decode_shutdown_communication(&data).unwrap();
318        assert!(decoded.contains('\u{FFFD}')); // replacement char
319    }
320
321    #[test]
322    fn shutdown_communication_ignores_trailing_bytes() {
323        let data = [3, b'f', b'o', b'o', b'x'];
324        assert_eq!(decode_shutdown_communication(&data).as_deref(), Some("foo"));
325    }
326}