Skip to content
Merged
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file.

### New features

* **Minimal route-level parser**: Added `BgpRouteElem` and `into_route_iter()` / `into_fallible_route_iter()` for fast scans when only prefix, AS path, peer metadata, and timestamp are needed.
- `BgpRouteElem` struct with `Option<Arc<AsPath>>` — shares the same parsed AS path across all announced prefixes from a single BGP UPDATE via `Arc`
- `RouteIterator` and `FallibleRouteIterator` for MRT records (BGP4MP, TableDump v1/v2, IPv4/IPv6, add-path)
- Filter support shared via `RouteFilterView` trait: `origin_asn`, `prefix`, `peer_ip`, `peer_asn`, `type`, `ts_start`/`ts_end`, `as_path`, `ip_version` all match identically to `BgpElem`
- Community filters fail-closed (return no match) since communities are not parsed at the route level
- Trait parity with `BgpElem`: `Display` (PSV-style), `Eq`, `PartialOrd`, `Ord` all implemented
- Performance: ~10-15% faster on updates files, ~50-70% faster on RIB dumps
- Added `examples/route_level_parsing.rs` demonstrating performance comparison

* **RFC 3107/8277 MPLS Labeled NLRI support**: Added parsing and encoding for BGP MPLS Labeled NLRI (SAFI 4).
Note: RFC 8277 obsoletes RFC 3107; both are supported for compatibility:
- New `MplsLabel` type with label value, TC, S-bit, and TTL
Expand All @@ -17,6 +26,8 @@ All notable changes to this project will be documented in this file.

### Performance improvements

* **Route-level parsing**: Selective attribute parsing skips communities, MED, next-hop, local-pref, and other attributes not needed for route identity. Combined with `Arc<AsPath>` sharing across prefixes from the same message, this yields ~10-15% faster updates parsing and ~50-70% faster RIB dump parsing.

* **AS Path memory optimization**: Reduced allocations for common cases using `smallvec`:
- `AsPath::segments`: Uses `SmallVec<[AsPathSegment; 1]>` — 99.99% of routes have exactly 1 segment (zero-allocation coverage)
- `AsPathSegment` variants: Uses `SmallVec<[Asn; 6]>` — 90.85% of segments have ≤6 ASNs (zero-allocation coverage)
Expand All @@ -39,8 +50,14 @@ All notable changes to this project will be documented in this file.

### Added

* **`AttributeValidationState`**: Extracted reusable validation struct from `parse_attributes()` so that both full attribute parsing and route-level parsing share identical RFC 7606 / RFC 4271 validation logic.

* **`RouteFilterView` trait**: New internal trait that enables sharing filter matching logic between `BgpElem` and `BgpRouteElem`. Community filters return `false` for route elements since communities are not parsed.

* **`Attributes::check_mandatory_attributes()`**: New method to validate mandatory attributes with proper NLRI context awareness. Must be called after NLRI parsing to correctly determine requirements.
* **`Attributes::attr_mask`**: Internal `[u64; 4]` bitmask field for O(1) attribute presence tracking (32 bytes vs 256 bytes for `[bool; 256]`)
* **`serde` feature**: Enabled `serde/rc` for `Arc<AsPath>` serialization support in `BgpRouteElem` when the `serde` feature is active.

* **Integration tests**: Added `tests/test_bgp_update_validation.rs` with comprehensive scenario coverage for all validation cases
* **Example `examples/parse_bmp_mpls.rs`**: Demonstrates extracting MPLS-labeled NLRI from BMP Route Monitoring messages, showing how to access label stack information via `MpReachNlri` attribute

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ wasm = [
serde = [
"dep:serde",
"ipnet/serde",
"serde/rc",
]
native-tls = [
"oneio/native-tls",
Expand Down
26 changes: 26 additions & 0 deletions benches/internals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ pub fn criterion_benchmark(c: &mut Criterion) {
})
});

c.bench_function("updates into_route_iter", |b| {
b.iter(|| {
let mut reader = black_box(&updates[..]);

BgpkitParser::from_reader(&mut reader)
.into_route_iter()
.take(RECORD_LIMIT)
.for_each(|x| {
black_box(x);
});
})
});

c.bench_function("updates into_raw_record_iter", |b| {
b.iter(|| {
let mut reader = black_box(&updates[..]);
Expand Down Expand Up @@ -143,6 +156,19 @@ pub fn criterion_benchmark(c: &mut Criterion) {
})
});

c.bench_function("rib into_route_iter", |b| {
b.iter(|| {
let mut reader = black_box(&rib_dump[..]);

BgpkitParser::from_reader(&mut reader)
.into_route_iter()
.take(RECORD_LIMIT)
.for_each(|x| {
black_box(x);
});
})
});

c.bench_function("rib into_raw_record_iter", |b| {
b.iter(|| {
let mut reader = black_box(&rib_dump[..]);
Expand Down
144 changes: 144 additions & 0 deletions examples/route_level_parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use bgpkit_parser::{BgpkitParser, Filterable};
use std::time::Instant;

/// This example demonstrates the lightweight route-level parser (`into_route_iter()`)
/// which provides significantly faster processing when you only need basic route
/// information (prefix, AS path, peer metadata) without full BGP attributes.
///
/// Performance characteristics:
/// - Updates files: ~10-15% faster (fewer attributes to skip)
/// - RIB dump files: ~50-70% faster (many attributes per route)
///
/// Use `into_route_iter()` when you need:
/// - prefix, AS path, peer IP/AS, timestamp
/// - Fast scanning/filtering of large datasets
/// - No need for communities, MED, next-hop, local-pref, etc.
///
/// Use `into_elem_iter()` when you need:
/// - Full BGP attributes (communities, MED, next-hop, local-pref, etc.)
/// - Community-based filtering
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

// Example 1: Download and parse an updates file using route iterator
log::info!("=== Example 1: Route-level parsing ===");

let url = "http://archive.routeviews.org/bgpdata/2021.10/UPDATES/updates.20211001.0000.bz2";

let start = Instant::now();
let parser = BgpkitParser::new(url).unwrap();

let mut route_count = 0;
for route in parser.into_route_iter().take(1000) {
if route_count < 3 {
// Show first few routes
log::info!(
"Route {}: {} via AS{} (peer: {})",
route_count + 1,
route.prefix,
route.peer_asn,
route.peer_ip
);
if let Some(ref path) = route.as_path {
log::info!(" AS Path: {}", path);
}
}
route_count += 1;
}
let route_time = start.elapsed();
log::info!(
"Route-level parsing: {} routes in {:.3}s",
route_count,
route_time.as_secs_f64()
);

// Example 2: Compare with element-level parsing
log::info!("\n=== Example 2: Element-level parsing (full attributes) ===");

let start = Instant::now();
let parser = BgpkitParser::new(url).unwrap();

let mut elem_count = 0;
for elem in parser.into_elem_iter().take(1000) {
if elem_count < 3 {
log::info!(
"Element {}: {} via AS{} (next-hop: {:?})",
elem_count + 1,
elem.prefix,
elem.peer_asn,
elem.next_hop
);
if let Some(ref communities) = elem.communities {
log::info!(" Communities: {:?}", communities);
}
}
elem_count += 1;
}
let elem_time = start.elapsed();
log::info!(
"Element-level parsing: {} elements in {:.3}s",
elem_count,
elem_time.as_secs_f64()
);

// Example 3: Filtering with route elements
log::info!("\n=== Example 3: Filtering route elements ===");

let parser = BgpkitParser::new(url).unwrap();
// Filter for routes from peer AS49788 (seen in the output above)
let filter = bgpkit_parser::Filter::new("peer_asn", "49788").unwrap();

let mut filtered_count = 0;
for route in parser.into_route_iter().take(1000) {
if route.match_filter(&filter) {
filtered_count += 1;
if filtered_count <= 3 {
log::info!(
"Matched filter (peer_asn=49788): {} from AS{}",
route.prefix,
route.peer_asn
);
}
}
}
log::info!(
"Total routes matching filter (first 1000): {}",
filtered_count
);

// Example 4: Demonstrate AS path filtering
log::info!("\n=== Example 4: AS path filtering ===");

let parser = BgpkitParser::new(url).unwrap();
// Filter for routes with AS1299 somewhere in the path
let as_path_filter = bgpkit_parser::Filter::new("as_path", "1299").unwrap();

let mut as_path_matches = 0;
for route in parser.into_route_iter().take(1000) {
if route.match_filter(&as_path_filter) {
as_path_matches += 1;
if as_path_matches <= 3 {
log::info!(
"AS Path contains 1299: {} - path: {:?}",
route.prefix,
route.as_path.as_ref().map(|p| p.to_string())
);
}
}
}
log::info!(
"Total routes with AS1299 in path (first 1000): {}",
as_path_matches
);

log::info!("\n=== Summary ===");
log::info!(
"Route-level: {:.3}s | Element-level: {:.3}s",
route_time.as_secs_f64(),
elem_time.as_secs_f64()
);
log::info!("");
log::info!("Performance gain is most significant for RIB dumps with many attributes.");
log::info!("For update files, the difference is smaller since there are fewer attributes.");
log::info!("Note: Community filters are NOT supported with route elements.");
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ pub mod parser;
pub mod wasm;

pub use models::BgpElem;
pub use models::BgpRouteElem;
pub use models::MrtRecord;
#[cfg(feature = "parser")]
pub use parser::*;
Loading
Loading