Skip to main content

bgpkit_parser/
error.rs

1/*!
2error module defines the error types used in bgpkit-parser.
3*/
4use crate::models::{Afi, AttrType, Bgp4MpType, BgpState, EntryType, Safi, TableDumpV2Type};
5use num_enum::TryFromPrimitiveError;
6#[cfg(feature = "oneio")]
7use oneio::OneIoError;
8use std::fmt::{Display, Formatter};
9use std::io::ErrorKind;
10use std::{error::Error, fmt, io};
11
12#[derive(Debug)]
13pub enum ParserError {
14    IoError(io::Error),
15    EofError(io::Error),
16    #[cfg(feature = "oneio")]
17    OneIoError(OneIoError),
18    EofExpected,
19    ParseError(String),
20    TruncatedMsg(String),
21    Unsupported(String),
22    /// An MRT common header declared a body over the 16 MiB limit. The length is
23    /// untrusted, so the reader is left past the header with no record boundary to
24    /// resume from, and the iterators stop instead of reading the body as headers.
25    OversizedRecord {
26        /// The declared body length that exceeded the limit.
27        length: u32,
28    },
29    FilterError(String),
30    /// NLRI length field is inconsistent with content
31    /// (total_bits < minimum required, or underflow in prefix calculation)
32    InvalidLabeledNlriLength,
33    /// Input ended before completing NLRI structure
34    TruncatedLabeledNlri,
35    /// Input ended in the middle of prefix data
36    TruncatedPrefix,
37    /// Exceeded configured max_labels without finding Bottom-of-Stack bit
38    MaxLabelStackDepthExceeded,
39    /// Exceeded peer-advertised max labels (Multiple Labels Capability)
40    /// Per RFC 8277 §2.1, this should be treated as a withdrawal
41    PeerMaxLabelsExceeded,
42    /// Invalid prefix in NLRI
43    InvalidPrefix,
44}
45
46impl Error for ParserError {}
47
48/// Errors that can occur when encoding BGP/MRT messages to wire format.
49///
50/// These arise when in-memory data structures contain values that cannot be
51/// represented in their wire-format fields (e.g. an AS_PATH segment with more
52/// than 255 ASes, or an attribute value exceeding 65535 bytes). All such
53/// conditions were previously handled by silently truncating — see issue #313.
54#[derive(Debug, Clone, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum EncodingError {
57    /// A value exceeded the maximum size that fits in its wire-format field.
58    ///
59    /// `field` names the wire field (e.g. `"AS_PATH segment count"`,
60    /// `"BGP attribute value length"`). `actual` is the byte/element count
61    /// that overflowed; `max` is the field's capacity.
62    ValueTooLarge {
63        field: &'static str,
64        actual: usize,
65        max: usize,
66    },
67    /// The value cannot be represented in wire format at all (e.g. ATTR_SET
68    /// encoding is not implemented, a labeled NLRI has an empty label stack,
69    /// or a BGP OPEN optional parameter uses the reserved type 255).
70    Unencodable { field: &'static str, reason: String },
71}
72
73impl EncodingError {
74    pub(crate) fn too_large(field: &'static str, actual: usize, max: usize) -> Self {
75        EncodingError::ValueTooLarge { field, actual, max }
76    }
77
78    #[cfg(feature = "parser")]
79    pub(crate) fn unencodable(field: &'static str, reason: impl Into<String>) -> Self {
80        EncodingError::Unencodable {
81            field,
82            reason: reason.into(),
83        }
84    }
85}
86
87/// Check that `actual` fits within `max`, returning `actual` unchanged.
88///
89/// This is the single place where wire-format capacity checks happen, so
90/// non-power-of-two bounds (e.g. the 12-bit FlowSpec NLRI length) are an
91/// explicit, greppable decision at the call site.
92pub(crate) fn check_max(
93    field: &'static str,
94    actual: usize,
95    max: usize,
96) -> Result<usize, EncodingError> {
97    if actual > max {
98        return Err(EncodingError::too_large(field, actual, max));
99    }
100    Ok(actual)
101}
102
103impl Display for EncodingError {
104    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
105        match self {
106            EncodingError::ValueTooLarge { field, actual, max } => write!(
107                f,
108                "encoding error: {field} ({actual}) exceeds maximum ({max})"
109            ),
110            EncodingError::Unencodable { field, reason } => {
111                write!(f, "encoding error: {field} cannot be encoded: {reason}")
112            }
113        }
114    }
115}
116
117impl Error for EncodingError {}
118
119#[derive(Debug)]
120pub struct ParserErrorWithBytes {
121    pub error: ParserError,
122    pub bytes: Option<Vec<u8>>,
123}
124
125impl Display for ParserErrorWithBytes {
126    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
127        write!(f, "{}", self.error)
128    }
129}
130
131impl Error for ParserErrorWithBytes {}
132
133/// implement Display trait for Error which satistifies the std::error::Error
134/// trait's requirement (must implement Display and Debug traits, Debug already derived)
135impl Display for ParserError {
136    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137        match self {
138            ParserError::IoError(e) => write!(f, "Error: {e}"),
139            ParserError::EofError(e) => write!(f, "Error: {e}"),
140            ParserError::ParseError(s) => write!(f, "Error: {s}"),
141            ParserError::TruncatedMsg(s) => write!(f, "Error: {s}"),
142            ParserError::Unsupported(s) => write!(f, "Error: {s}"),
143            ParserError::OversizedRecord { length } => {
144                write!(f, "Error: MRT message too large: {length} bytes")
145            }
146            ParserError::EofExpected => write!(f, "Error: reach end of file"),
147            #[cfg(feature = "oneio")]
148            ParserError::OneIoError(e) => write!(f, "Error: {e}"),
149            ParserError::FilterError(e) => write!(f, "Error: {e}"),
150            ParserError::InvalidLabeledNlriLength => {
151                write!(f, "Error: invalid labeled NLRI length field")
152            }
153            ParserError::TruncatedLabeledNlri => write!(f, "Error: truncated labeled NLRI"),
154            ParserError::TruncatedPrefix => write!(f, "Error: truncated prefix in NLRI"),
155            ParserError::MaxLabelStackDepthExceeded => write!(
156                f,
157                "Error: max label stack depth exceeded without finding BoS bit"
158            ),
159            ParserError::PeerMaxLabelsExceeded => write!(
160                f,
161                "Error: received more labels than peer advertised maximum"
162            ),
163            ParserError::InvalidPrefix => write!(f, "Error: invalid prefix in NLRI"),
164        }
165    }
166}
167
168#[cfg(feature = "oneio")]
169impl From<OneIoError> for ParserErrorWithBytes {
170    fn from(error: OneIoError) -> Self {
171        ParserErrorWithBytes {
172            error: ParserError::OneIoError(error),
173            bytes: None,
174        }
175    }
176}
177
178#[cfg(feature = "oneio")]
179impl From<OneIoError> for ParserError {
180    fn from(error: OneIoError) -> Self {
181        ParserError::OneIoError(error)
182    }
183}
184
185impl From<ParserError> for ParserErrorWithBytes {
186    fn from(error: ParserError) -> Self {
187        ParserErrorWithBytes { error, bytes: None }
188    }
189}
190
191impl From<io::Error> for ParserError {
192    fn from(io_error: io::Error) -> Self {
193        match io_error.kind() {
194            ErrorKind::UnexpectedEof => ParserError::EofError(io_error),
195            _ => ParserError::IoError(io_error),
196        }
197    }
198}
199
200impl From<TryFromPrimitiveError<Bgp4MpType>> for ParserError {
201    fn from(value: TryFromPrimitiveError<Bgp4MpType>) -> Self {
202        ParserError::ParseError(format!("cannot parse bgp4mp subtype: {}", value.number))
203    }
204}
205
206impl From<TryFromPrimitiveError<BgpState>> for ParserError {
207    fn from(value: TryFromPrimitiveError<BgpState>) -> Self {
208        ParserError::ParseError(format!("cannot parse bgp4mp state: {}", value.number))
209    }
210}
211
212impl From<TryFromPrimitiveError<TableDumpV2Type>> for ParserError {
213    fn from(value: TryFromPrimitiveError<TableDumpV2Type>) -> Self {
214        ParserError::ParseError(format!("cannot parse table dump v2 type: {}", value.number))
215    }
216}
217
218impl From<TryFromPrimitiveError<EntryType>> for ParserError {
219    fn from(value: TryFromPrimitiveError<EntryType>) -> Self {
220        ParserError::ParseError(format!("cannot parse entry type: {}", value.number))
221    }
222}
223
224impl From<TryFromPrimitiveError<Afi>> for ParserError {
225    fn from(value: TryFromPrimitiveError<Afi>) -> Self {
226        ParserError::ParseError(format!("Unknown AFI type: {}", value.number))
227    }
228}
229
230impl From<TryFromPrimitiveError<Safi>> for ParserError {
231    fn from(value: TryFromPrimitiveError<Safi>) -> Self {
232        ParserError::ParseError(format!("Unknown SAFI type: {}", value.number))
233    }
234}
235
236/// BGP validation warnings for RFC 7606 compliant error handling.
237/// These represent non-fatal validation issues that don't prevent parsing.
238///
239/// This enum is `#[non_exhaustive]` so that new warning variants can be added
240/// in minor releases without breaking exhaustive matches.
241#[derive(Debug, Clone, PartialEq, Eq)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize))]
243#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
244#[non_exhaustive]
245pub enum BgpValidationWarning {
246    /// Attribute flags conflict with attribute type code (RFC 4271 Section 6.3)
247    AttributeFlagsError {
248        attr_type: AttrType,
249        expected_flags: u8,
250        actual_flags: u8,
251    },
252    /// Attribute length conflicts with expected length (RFC 4271 Section 6.3)
253    AttributeLengthError {
254        attr_type: AttrType,
255        expected_length: Option<usize>,
256        actual_length: usize,
257    },
258    /// Missing well-known mandatory attribute (RFC 4271 Section 6.3)
259    MissingWellKnownAttribute { attr_type: AttrType },
260    /// Unrecognized well-known attribute (RFC 4271 Section 6.3)
261    UnrecognizedWellKnownAttribute { attr_type_code: u8 },
262    /// Invalid origin attribute value (RFC 4271 Section 6.3)
263    InvalidOriginAttribute { value: u8 },
264    /// Invalid next hop attribute (RFC 4271 Section 6.3)
265    InvalidNextHopAttribute { reason: String },
266    /// Malformed AS_PATH attribute (RFC 4271 Section 6.3)
267    MalformedAsPath { reason: String },
268    /// Optional attribute error (RFC 4271 Section 6.3)
269    OptionalAttributeError { attr_type: AttrType, reason: String },
270    /// Attribute appears more than once (RFC 4271 Section 6.3)
271    DuplicateAttribute { attr_type: AttrType },
272    /// Invalid network field in NLRI (RFC 4271 Section 6.3)
273    InvalidNetworkField { reason: String },
274    /// Malformed attribute list (RFC 4271 Section 6.3)
275    MalformedAttributeList { reason: String },
276    /// Partial attribute with errors (RFC 7606)
277    PartialAttributeError { attr_type: AttrType, reason: String },
278    /// Malformed NLRI field (RFC 7606 §5.3). The UPDATE message was parseable
279    /// up to the NLRI section, but the NLRI itself contained syntactic errors.
280    /// Per RFC 7606, the recommended action is "treat-as-withdrawal": all
281    /// routes carried in the NLRI section should be withdrawn.
282    ///
283    /// `nlri_type` distinguishes between the standard (IPv4 unicast) NLRI
284    /// field and the multiprotocol NLRI carried inside MP_REACH/MP_UNREACH
285    /// path attributes. `raw_bytes` preserves the offending NLRI bytes so
286    /// callers can inspect or export them without re-encoding.
287    MalformedNlri {
288        /// "withdrawn", "announced", "mp_reach", or "mp_unreach"
289        nlri_type: &'static str,
290        /// Human-readable description of the parse error
291        reason: String,
292        /// Raw NLRI bytes as they appeared in the UPDATE message
293        raw_bytes: Vec<u8>,
294    },
295    /// ROUTE-REFRESH message subtype other than 0, 1, or 2 (RFC 7313
296    /// Section 5). A live speaker MUST ignore such a message and SHOULD log
297    /// an error; the parser retains it raw and reports this warning.
298    UnknownRouteRefreshSubtype { subtype: u8 },
299    /// BoRR/EoRR ROUTE-REFRESH message (RFC 7313 subtype 1 or 2) whose body
300    /// is not exactly 4 bytes. On the wire this is a fatal "Invalid Message
301    /// Length" NOTIFICATION when the Enhanced Route Refresh capability was
302    /// negotiated; MRT data carries no session context, so the parser
303    /// retains the message and reports this warning instead.
304    InvalidRouteRefreshLength {
305        subtype: u8,
306        /// Message body length excluding the 19-byte BGP header
307        length: usize,
308    },
309}
310
311impl Display for BgpValidationWarning {
312    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
313        match self {
314            BgpValidationWarning::AttributeFlagsError { attr_type, expected_flags, actual_flags } => {
315                write!(f, "Attribute flags error for {attr_type:?}: expected 0x{expected_flags:02x}, got 0x{actual_flags:02x}")
316            }
317            BgpValidationWarning::AttributeLengthError { attr_type, expected_length, actual_length } => {
318                match expected_length {
319                    Some(expected) => write!(f, "Attribute length error for {attr_type:?}: expected {expected}, got {actual_length}"),
320                    None => write!(f, "Attribute length error for {attr_type:?}: invalid length {actual_length}"),
321                }
322            }
323            BgpValidationWarning::MissingWellKnownAttribute { attr_type } => {
324                write!(f, "Missing well-known mandatory attribute: {attr_type:?}")
325            }
326            BgpValidationWarning::UnrecognizedWellKnownAttribute { attr_type_code } => {
327                write!(f, "Unrecognized well-known attribute: type code {attr_type_code}")
328            }
329            BgpValidationWarning::InvalidOriginAttribute { value } => {
330                write!(f, "Invalid origin attribute value: {value}")
331            }
332            BgpValidationWarning::InvalidNextHopAttribute { reason } => {
333                write!(f, "Invalid next hop attribute: {reason}")
334            }
335            BgpValidationWarning::MalformedAsPath { reason } => {
336                write!(f, "Malformed AS_PATH: {reason}")
337            }
338            BgpValidationWarning::OptionalAttributeError { attr_type, reason } => {
339                write!(f, "Optional attribute error for {attr_type:?}: {reason}")
340            }
341            BgpValidationWarning::DuplicateAttribute { attr_type } => {
342                write!(f, "Duplicate attribute: {attr_type:?}")
343            }
344            BgpValidationWarning::InvalidNetworkField { reason } => {
345                write!(f, "Invalid network field: {reason}")
346            }
347            BgpValidationWarning::MalformedAttributeList { reason } => {
348                write!(f, "Malformed attribute list: {reason}")
349            }
350            BgpValidationWarning::PartialAttributeError { attr_type, reason } => {
351                write!(f, "Partial attribute error for {attr_type:?}: {reason}")
352            }
353            BgpValidationWarning::MalformedNlri { nlri_type, reason, .. } => {
354                write!(f, "Malformed NLRI ({nlri_type}): {reason}")
355            }
356            BgpValidationWarning::UnknownRouteRefreshSubtype { subtype } => {
357                write!(f, "Unknown ROUTE-REFRESH message subtype: {subtype}")
358            }
359            BgpValidationWarning::InvalidRouteRefreshLength { subtype, length } => {
360                write!(f, "Invalid ROUTE-REFRESH message length for subtype {subtype}: body is {length} bytes, expected 4")
361            }
362        }
363    }
364}
365
366/// Result type for BGP attribute parsing that includes validation warnings
367#[derive(Debug, Clone)]
368pub struct BgpValidationResult<T> {
369    pub value: T,
370    pub warnings: Vec<BgpValidationWarning>,
371}
372
373impl<T> BgpValidationResult<T> {
374    pub fn new(value: T) -> Self {
375        Self {
376            value,
377            warnings: Vec::new(),
378        }
379    }
380
381    pub fn with_warnings(value: T, warnings: Vec<BgpValidationWarning>) -> Self {
382        Self { value, warnings }
383    }
384
385    pub fn add_warning(&mut self, warning: BgpValidationWarning) {
386        self.warnings.push(warning);
387    }
388
389    pub fn has_warnings(&self) -> bool {
390        !self.warnings.is_empty()
391    }
392}