Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ The repository has a `.git/hooks/pre-push` script that runs automatically on eve

If any check fails, the push is aborted. Fix the issue, commit, and push again.

## Code Coverage (Codecov)

CI uploads coverage to Codecov on every push and PR. The `codecov.yml` at the repo root configures ignored paths, thresholds, and PR comment layout.

### Finding uncovered lines in a PR

Use `scripts/coverage-misses.sh` to get exact line numbers for uncovered patch lines via the public Codecov API v2 (no token needed):

```bash
./scripts/coverage-misses.sh <PR_NUMBER>
```

Example output:

```
📄 src/parser/bgp/attributes/attr_26_aigp.rs
Coverage: 85.29% | Patch misses: 10 lines
Uncovered lines: 20, 21, 22, 27, 28, 29, 30, 50, 76, 91
```

This is more actionable than the Codecov PR comment, which only shows miss counts with links to codecov.io. After running the script, use `read` on the file to inspect the uncovered lines.

### How it works

The script calls:
1. `GET /compare/impacted_files?pullid=N` — lists files with patch misses
2. `GET /compare/segments/{file_path}?pullid=N` — returns line-by-line coverage (`head_coverage: 1` = uncovered, `0` = covered, `null` = non-code)

These endpoints are public and documented at https://docs.codecov.com/reference.

### When to check coverage

Before pushing a new feature, run the script on your PR to identify lines that need tests. The pre-push hook does NOT gate on coverage — coverage checks in CI are informational (non-blocking, 10% threshold).

## Notes

- The `bgpkit-parser` binary requires the `cli` feature (`required-features = ["cli"]`).
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Breaking changes

* **`AttrRaw` fields changed**: `attr_type: AttrType` replaced with `code: u8`. Use `.attr_type()` to get the `AttrType` back. `bytes` changed from `Vec<u8>` to `Bytes`.
* **`AttrType::CLUSTER_ID` removed**: Code 13 is the deprecated `RCID_PATH / CLUSTER_ID` per IANA registry and is now retained as `AttributeValue::Deprecated`.
* **`AttributeValue` gains new variants**: `Raw(AttrRaw)`, `BfdDiscriminator(...)`, `BgpPrefixSid(...)`, `Bier(...)`, `Sfp(...)`. Downstream exhaustive matches must handle these.
* **`AigpTlv.value` changed**: From `Vec<u8>` to `Bytes`.
* **No more silent attribute loss**: Known-but-unsupported attributes that were previously dropped are now retained as `AttributeValue::Raw(AttrRaw)`. If your code expected these to be absent, it may now see them.

### New features

* **Raw attribute retention**: Known BGP path attributes without semantic parsing are no longer silently dropped. They are retained as `AttributeValue::Raw(AttrRaw)` with their original wire code and bytes, enabling faithful re-encoding.
* **Deprecated code point handling**: Removed `AttrType::CLUSTER_ID = 13` (deprecated per IANA); code 13 is now retained as `Deprecated(AttrRaw)`. Added `is_deprecated_attr_type()` helper.
* **`AttrType::BIER` added**: Code 41 (RFC 9793) added to the enum.
* **Typed parser failure fallback**: If a typed attribute parser fails, the original bytes are retained as `AttributeValue::Raw` instead of being dropped. A validation warning is still recorded.
* **Serialization**: Added `serde` feature to `bytes` dependency so `AttrRaw` (with `Bytes`) can be serialized.

### New attribute parsers

* **RFC 7311 — AIGP (code 26)**: Parses AIGP TLVs, preserves unknown TLVs, supports round-trip encoding. Exposes `accumulated_metric()` helper.
* **RFC 9015 — SFP attribute (code 37)**: Parses SFP TLVs with 1-octet type and 2-octet length. Preserves unknown TLVs.
* **RFC 9026 — BFD Discriminator (code 38)**: Parses mode, discriminator, and optional 1-octet TLVs. Preserves unknown optional TLVs.
* **RFC 8669 — BGP Prefix-SID (code 40)**: Parses Prefix-SID TLVs with 1-octet type and 2-octet length. Preserves unknown TLVs.
* **RFC 9793 — BIER (code 41)**: Parses BIER TLVs with 2-octet type and 2-octet length. Preserves unknown TLVs.

### Added

* `AttributeValue::Raw(AttrRaw)` — for known but undecoded attributes.
* `AttrRaw { code: u8, bytes: Bytes }` — stores wire code and raw value bytes.
* `AttributeValue::attr_code()` — returns the raw wire attribute type code as `u8`.
* `AttrRaw::attr_type()` — convenience conversion to `AttrType`.
* Model structs: `BfdDiscriminatorAttribute`, `BgpPrefixSidAttribute`, `BierAttribute`, `SfpAttribute`.
* Raw TLV helper types: `RawTlv8`, `RawTlv8Ext`, `RawTlv16`.
* `examples/raw_attributes.rs` — demonstrates inspecting raw/deprecated/unknown attributes.

### Fixed

* Code 13 (`RCID_PATH / CLUSTER_ID`) no longer maps to an active `AttrType` variant; correctly handled as deprecated.
* Known but unsupported attributes (codes 0, 22, 24, 27, 33, 128) are now retained as `Raw` instead of dropped.
* Malformed typed attributes retain their original bytes as `Raw` after recording a validation warning.

## v0.17.0 - 2026-05-24

### New features
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ serde = { version = "1.0", features = ["derive"], optional = true }
#######################
# Parser dependencies #
#######################
bytes = { version = "1.7", optional = true }
bytes = { version = "1.7" }
zerocopy = { version = "0.8", features = ["derive"], optional = true }
hex = { version = "0.4.3", optional = true } # bmp/openbmp parsing
oneio = { version = "0.23", default-features = false, features = ["http", "gz", "bz"], optional = true }
Expand All @@ -67,7 +67,6 @@ default = ["parser", "rustls"]
local = ["parser", "oneio"]

parser = [
"bytes",
"chrono",
"regex",
"zerocopy",
Expand Down Expand Up @@ -96,6 +95,7 @@ wasm = [
]
serde = [
"dep:serde",
"bytes/serde",
"ipnet/serde",
"serde/rc",
]
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,18 @@ Full support for standard, extended, and large communities:
- [RFC 6810](https://datatracker.ietf.org/doc/html/rfc6810): The Resource Public Key Infrastructure (RPKI) to Router Protocol
- [RFC 8210](https://datatracker.ietf.org/doc/html/rfc8210): The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 1

### BGP Path Attributes

Typed parsing for these RFC-defined BGP path attributes:

- [RFC 7311](https://datatracker.ietf.org/doc/html/rfc7311): Accumulated IGP Metric (AIGP) Attribute
- [RFC 9015](https://datatracker.ietf.org/doc/html/rfc9015): BGP SFP Attribute
- [RFC 9026](https://datatracker.ietf.org/doc/html/rfc9026): BFD Discriminator Attribute
- [RFC 8669](https://datatracker.ietf.org/doc/html/rfc8669): BGP Prefix-SID Attribute
- [RFC 9793](https://datatracker.ietf.org/doc/html/rfc9793): BGP Extensions for BIER

Additional known attribute type codes are raw-retained (`AttributeValue::Raw`) and re-encoded faithfully. Deprecated and unassigned codes are also preserved.

### Advanced Features

**FlowSpec**:
Expand Down
119 changes: 119 additions & 0 deletions examples/raw_attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/// Example: inspecting raw, deprecated, and unknown BGP path attributes.
///
/// v0.18 introduces `AttributeValue::Raw` for known but undecoded attributes,
/// making it possible to inspect, count, and round-trip attributes even when
/// full semantic parsing is not yet implemented.
///
/// Run with:
/// ```bash
/// cargo run --example raw_attributes --features cli
/// ```
use bgpkit_parser::models::*;

fn describe_value(value: &AttributeValue) {
match value {
AttributeValue::Raw(raw) => {
println!(
" Raw code {} ({} bytes, {:?})",
raw.code,
raw.bytes.len(),
raw.attr_type()
);
}
AttributeValue::Deprecated(raw) => {
println!(" Deprecated code {} ({} bytes)", raw.code, raw.bytes.len());
}
AttributeValue::Unknown(raw) => {
println!(" Unknown code {} ({} bytes)", raw.code, raw.bytes.len());
}
AttributeValue::Aigp(aigp) => {
println!(
" AIGP ({} TLVs, metric={:?})",
aigp.tlvs.len(),
aigp.accumulated_metric()
);
}
AttributeValue::BfdDiscriminator(bfd) => {
println!(
" BFD Discriminator (mode={}, discriminator={})",
bfd.mode, bfd.discriminator
);
}
AttributeValue::BgpPrefixSid(psid) => {
println!(" BGP Prefix-SID ({} TLVs)", psid.tlvs.len());
}
AttributeValue::Bier(bier) => println!(" BIER ({} TLVs)", bier.tlvs.len()),
AttributeValue::Sfp(sfp) => println!(" SFP ({} TLVs)", sfp.tlvs.len()),
other => println!(" {:?}", other.attr_type()),
}
}

fn main() {
println!("=== Demonstrating raw/deprecated/unknown attribute handling ===\n");

// Build an Attributes collection from AttributeValue iterators.
let attributes = Attributes::from_iter(vec![
// Known but unsupported: PMSI_TUNNEL (code 22)
AttributeValue::Raw(AttrRaw {
code: 22,
bytes: bytes::Bytes::from_static(&[0x01, 0x02, 0x03]),
}),
// Deprecated: code 13 (RCID_PATH / CLUSTER_ID)
AttributeValue::Deprecated(AttrRaw {
code: 13,
bytes: bytes::Bytes::from_static(&[0xaa, 0xbb, 0xcc, 0xdd]),
}),
// Unknown / unassigned: code 127
AttributeValue::Unknown(AttrRaw {
code: 127,
bytes: bytes::Bytes::from_static(&[0xde, 0xad]),
}),
// Typed: a structured AIGP attribute
AttributeValue::Aigp(Aigp {
tlvs: vec![AigpTlv {
tlv_type: 1,
length: 11,
value: bytes::Bytes::from_static(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a]),
}],
}),
]);

println!("All attributes:");
for value in &attributes {
describe_value(value);
}

// Check warnings
println!(
"\nValidation warnings: {}",
attributes.validation_warnings().len()
);

// Encode and decode round-trip
let encoded = attributes.encode(AsnLength::Bits32);
println!("\nEncoded size: {} bytes", encoded.len());

// Show raw access to the undecoded bytes
println!("\nRaw bytes access:");
for value in &attributes {
match value {
AttributeValue::Raw(raw) => {
println!(
" Raw code {}: {} bytes (first bytes: {:02x?})",
raw.code,
raw.bytes.len(),
&raw.bytes[..raw.bytes.len().min(8)]
);
}
AttributeValue::Deprecated(raw) => {
println!(" Deprecated code {}: {} bytes", raw.code, raw.bytes.len());
}
AttributeValue::Unknown(raw) => {
println!(" Unknown code {}: {} bytes", raw.code, raw.bytes.len());
}
_ => {}
}
}

println!("\n=== Done ===");
}
135 changes: 135 additions & 0 deletions examples/scan_path_attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/// Scan MRT archives for interesting BGP path attributes.
///
/// Iterates over recent RouteViews and RIPE RIS update files, scanning for
/// deprecated, unknown/unassigned, and raw-retained attributes.
///
/// NOTE: The date range (year, month, days) is hardcoded below.
/// Update it to a recent window before running.
///
/// Usage:
/// ```bash
/// cargo run --example scan_path_attributes --features cli
/// ```
use bgpkit_parser::BgpkitParser;
use std::collections::HashMap;

/// Scan a single MRT file, sampling up to `max_elems` elements.
fn scan_file(url: &str, max_elems: u64) -> Result<(HashMap<String, u64>, u64), String> {
let parser = BgpkitParser::new(url).map_err(|e| format!("parser error: {e}"))?;
let mut counts: HashMap<String, u64> = HashMap::new();
let mut processed = 0u64;

for elem in parser.into_elem_iter() {
// Check unknown/unassigned and raw-retained known attributes
if let Some(ref unknown) = elem.unknown {
for raw in unknown {
let key = format!("unknown(code={}, type={:?})", raw.code, raw.attr_type());
*counts.entry(key).or_default() += 1;
}
}

// Check deprecated attributes
if let Some(ref deprecated) = elem.deprecated {
for raw in deprecated {
let key = format!("deprecated(code={})", raw.code);
*counts.entry(key).or_default() += 1;
}
}

processed += 1;
if processed >= max_elems {
break;
}
}
Ok((counts, processed))
}

fn main() {
println!("=== BGP Path Attribute Scanner ===");
println!("Looks for unsupported/raw/deprecated attributes in public archive files.\n");

// RouteViews collectors
let collectors = [
"route-views4",
"route-views6",
"route-views2",
"route-views3",
"route-views.linx",
"route-views.eqix",
"route-views.amsix",
];

let mut urls: Vec<String> = Vec::new();

// NOTE: Update year/month/days below to a recent date window before running.
// RouteViews update files — last 2 days, sampling hours 0 and 12
for collector in &collectors {
for day in [1, 2] {
for hour in [0, 12] {
let url = format!(
"http://archive.routeviews.org/{}/bgpdata/2026.06/UPDATES/updates.202606{:02}.{:04}00.bz2",
collector, day, hour
);
urls.push(url);
}
}
}

// RIPE RIS update files
for rrc in 0..=6 {
for day in [1, 2] {
let url = format!(
"https://data.ris.ripe.net/rrc{:02}/2026.06/updates.202606{:02}.0000.gz",
rrc, day
);
urls.push(url);
}
}

println!(
"Candidates: {} files (sampling 50K elements each)",
urls.len()
);
println!();

let mut found_files: Vec<(String, HashMap<String, u64>)> = Vec::new();

for url in &urls {
print!(" {:65}", url);
std::io::Write::flush(&mut std::io::stdout()).ok();

match scan_file(url, 50_000) {
Ok((counts, processed)) => {
if counts.is_empty() {
println!(" ({}K elems, nothing)", processed / 1000);
} else {
println!(" ({}K elems, {} hits)", processed / 1000, counts.len());
found_files.push((url.clone(), counts));
}
}
Err(e) => {
eprintln!(" error: {}", e);
}
}
}

println!();
if found_files.is_empty() {
println!("No interesting attributes found in the scanned window.");
println!(
"This is expected: deprecated and specialized attributes are rare in public data."
);
} else {
println!("=== Files with interesting attributes ===");
println!();
for (url, counts) in &found_files {
println!("File: {}", url);
let mut entries: Vec<_> = counts.iter().collect();
entries.sort_by_key(|(k, _)| String::clone(k));
for (key, count) in &entries {
println!(" {}: {}", key, count);
}
println!();
}
}
}
Loading
Loading