diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0587eb..4635e82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - rust: [1.71.1, stable, beta] + rust: [1.74.1, stable, beta] steps: - name: Checkout repository uses: actions/checkout@v1 diff --git a/Cargo.lock b/Cargo.lock index 76c0855..1a6aa39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1104,9 +1104,9 @@ dependencies = [ [[package]] name = "rpki" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c619005c452f0e0d8895334e21c846158cf2591d1db0a948ab0610d70d16828" +checksum = "a20b4c3d0ee54ae5623463c84d032786805f12d139df93539434e45be11db659" dependencies = [ "arbitrary", "base64", diff --git a/Cargo.toml b/Cargo.toml index 5f6b854..e7120c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "rtrtr" version = "0.3.2-dev" edition = "2021" -rust-version = "1.71.1" +rust-version = "1.74.1" authors = ["NLnet Labs "] description = "A versatile tool for managing route filters" repository = "https://github.com/NLnetLabs/rtrtr" @@ -26,7 +26,7 @@ log = "0.4" pin-project-lite = "0.2.4" rand = "0.8.3" reqwest = { version = "0.12.5", default-features = false, features = ["blocking", "rustls-tls"] } -rpki = { version = "0.18.2", features = ["crypto", "rtr", "slurm"] } +rpki = { version = "0.18.5", features = ["crypto", "rtr", "slurm"] } rustls-pemfile = "2.1.2" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -45,7 +45,6 @@ default = [ "socks" ] arbitrary = [ "dep:arbitrary", "chrono/arbitrary", "rpki/arbitrary" ] socks = [ "reqwest/socks" ] - [dev-dependencies] stderrlog = "0.6" rand_pcg = "0.3" diff --git a/src/formats/json.rs b/src/formats/json.rs index aced3bd..9df46a4 100644 --- a/src/formats/json.rs +++ b/src/formats/json.rs @@ -19,7 +19,8 @@ use rpki::resources::asn::Asn; use rpki::resources::addr::{MaxLenError, MaxLenPrefix, Prefix}; -use rpki::rtr::payload::{RouteOrigin, Payload, PayloadRef}; +use rpki::rtr::payload::{Aspa as AspaPayload, Payload, PayloadRef, RouteOrigin}; +use rpki::rtr::pdu::{ProviderAsns, ProviderAsnsError}; use rpki::rtr::server::PayloadSet; use serde::{Deserialize, Serialize}; use crate::payload; @@ -34,6 +35,8 @@ use crate::payload; pub struct Set { /// The list of VRPs. roas: Vec, + /// The list of ASPAs. + aspas: Option>, } impl Set { @@ -43,6 +46,11 @@ impl Set { for item in self.roas { let _ = res.insert(item.into_payload()); } + if let Some(aspas) = self.aspas { + for item in aspas { + let _ = res.insert(item.into_payload()); + } + } res.finalize().into() } } @@ -78,6 +86,36 @@ impl TryFrom for Vrp { } +//------------ Aspa ---------------------------------------------------------- + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(try_from = "JsonAspa", into = "JsonAspa")] +struct Aspa { + /// The payload of the ASPA. + payload: AspaPayload, +} + +impl Aspa { + fn into_payload(self) -> Payload { + Payload::Aspa(self.payload) + } +} + +impl TryFrom for Aspa { + type Error = ProviderAsnsError; + + fn try_from(json: JsonAspa) -> Result { + let providers = ProviderAsns::try_from_iter(json.providers.into_iter())?; + Ok(Self { + payload: AspaPayload { + customer: json.customer_asid, + providers, + }, + }) + } +} + + //============ Serialization ================================================= @@ -90,7 +128,7 @@ impl TryFrom for Vrp { struct JsonVrp { /// The prefix member. prefix: Prefix, - + /// The ASN member. #[serde( serialize_with = "Asn::serialize_as_str", @@ -113,6 +151,24 @@ impl From for JsonVrp { } } +//------------ JsonAspa ------------------------------------------------------ + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct JsonAspa { + /// The customer ASN. + customer_asid: Asn, + /// The provider ASNs. + providers: Vec, +} + +impl From for JsonAspa { + fn from(aspa: Aspa) -> Self { + Self { + customer_asid: aspa.payload.customer, + providers: aspa.payload.providers.iter().collect(), + } + } +} //============ Output ======================================================== @@ -128,19 +184,34 @@ pub struct OutputStream { } /// The state of the stream. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] enum StreamState { - /// We need to write the header next. - Header, + /// `{` + Start, + + /// `"roas": [` + RoaStart, + + /// { .. } + RoaBody, + + /// `"]"` + RoaEnd, + + /// `"aspas": [` + AspaStart, + + /// { .. } + AspaBody, - /// We need to write the first element next. - First, + /// `']'` + AspaEnd, - /// We need to write more elements. - Body, + /// `'}'` + End, - /// We are done! - Done + /// None + Done, } impl OutputStream { @@ -148,20 +219,28 @@ impl OutputStream { pub fn new(set: payload::Set) -> Self { OutputStream { iter: set.into_owned_iter(), - state: StreamState::Header, + state: StreamState::Start, } } +} - /// Returns the next route origin in the payload set. - pub fn next_origin(&mut self) -> Option { - loop { - match self.iter.next() { - Some(PayloadRef::Origin(value)) => return Some(value), - None => return None, - _ => {} - } - } - } +fn format_origin(origin: RouteOrigin, last: bool) -> Vec { + format!( + r#" {{ "asn": "{}", "prefix": "{}", "maxLength": {}, "ta": "N/A" }}{}"#, + origin.asn, + origin.prefix.prefix(), + origin.prefix.resolved_max_len(), + if last { "\n" } else { ",\n" }, + ).into_bytes() +} + +fn format_aspa(aspa: AspaPayload, last: bool) -> Vec { + format!( + r#" {{ "customer_asid": {}, "providers": {:?} }}{}"#, + aspa.customer.into_u32(), + aspa.providers.iter().map(|a| a.into_u32()).collect::>(), + if last { "\n" } else { ",\n" }, + ).into_bytes() } impl Iterator for OutputStream { @@ -169,49 +248,85 @@ impl Iterator for OutputStream { fn next(&mut self) -> Option { match self.state { - StreamState::Header => { - self.state = StreamState::First; - Some(b"{\n \"roas\": [\n".to_vec()) + StreamState::Start => { + self.state = match self.iter.peek() { + Some(Payload::Origin(_)) => StreamState::RoaStart, + Some(Payload::Aspa(_)) => StreamState::AspaStart, + Some(Payload::RouterKey(_)) => StreamState::End, + None => StreamState::End, + }; + Some(b"{\n".to_vec()) } - StreamState::First => { - match self.next_origin() { - Some(payload) => { - self.state = StreamState::Body; - Some(format!( - " {{ \"asn\": \"{}\", \"prefix\": \"{}\", \ - \"maxLength\": {}, \"ta\": \"N/A\" }}", - payload.asn, - payload.prefix.prefix(), - payload.prefix.resolved_max_len(), - ).into_bytes()) - } - None => { - self.state = StreamState::Done; - Some(b"\n ]\n}".to_vec()) - } + StreamState::RoaStart => { + self.state = StreamState::RoaBody; + Some(b" \"roas\": [\n".to_vec()) + } + StreamState::RoaBody => { + let Some(PayloadRef::Origin(payload)) = self.iter.next() else { + unreachable!(); + }; + + self.state = match self.iter.peek() { + Some(Payload::Origin(_)) => StreamState::RoaBody, + Some(Payload::Aspa(_)) => StreamState::RoaEnd, + Some(Payload::RouterKey(_)) => StreamState::RoaEnd, + None => StreamState::RoaEnd, + }; + + let last = self.state != StreamState::RoaBody; + Some(format_origin(payload, last)) + } + StreamState::RoaEnd => { + self.state = match self.iter.peek() { + Some(Payload::Origin(_)) => unreachable!(), + Some(Payload::Aspa(_)) => StreamState::AspaStart, + Some(Payload::RouterKey(_)) => StreamState::End, + None => StreamState::End, + }; + if self.state == StreamState::End { + Some(b" ]\n".to_vec()) + } else { + Some(b" ],\n".to_vec()) } } - StreamState::Body => { - match self.next_origin() { - Some(payload) => { - Some(format!( - ",\n \ - {{ \"asn\": \"{}\", \"prefix\": \"{}\", \ - \"maxLength\": {}, \"ta\": \"N/A\" }}", - payload.asn, - payload.prefix.prefix(), - payload.prefix.resolved_max_len(), - ).into_bytes()) - } - None => { - self.state = StreamState::Done; - Some(b"\n ]\n}".to_vec()) - } + StreamState::AspaStart => { + self.state = StreamState::AspaBody; + Some(b" \"aspas\": [\n".to_vec()) + } + StreamState::AspaBody => { + let Some(PayloadRef::Aspa(payload)) = self.iter.next() else { + unreachable!(); + }; + let payload = payload.clone(); + + self.state = match self.iter.peek() { + Some(Payload::Origin(_)) => unreachable!(), + Some(Payload::Aspa(_)) => StreamState::AspaBody, + Some(Payload::RouterKey(_)) => StreamState::AspaEnd, + None => StreamState::AspaEnd, + }; + + let last = self.state != StreamState::AspaBody; + Some(format_aspa(payload, last)) + } + StreamState::AspaEnd => { + self.state = match self.iter.peek() { + Some(Payload::Origin(_)) => unreachable!(), + Some(Payload::Aspa(_)) => unreachable!(), + Some(Payload::RouterKey(_)) => StreamState::End, + None => StreamState::End, + }; + if self.state == StreamState::End { + Some(b" ]\n".to_vec()) + } else { + Some(b" ],\n".to_vec()) } } - StreamState::Done => { - None + StreamState::End => { + self.state = StreamState::Done; + Some(b"}".to_vec()) } + StreamState::Done => None, } } } @@ -257,5 +372,61 @@ mod test { include_bytes!("../../test-data/vrps.rpki-client.json") ).unwrap()); } + + #[test] + fn serialize() { + fn s(items: Vec) -> String { + let mut res = payload::PackBuilder::empty(); + for item in items { + res.insert(item).unwrap(); + } + let set: payload::Set = res.finalize().into(); + let output = OutputStream::new(set); + let mut out = vec![]; + for item in output { + out.extend_from_slice(&item); + } + String::from_utf8(out).unwrap() + } + + assert_eq!(s(vec![]), "{\n}"); + assert_eq!( + s(vec![ + Payload::Origin(RouteOrigin::new(MaxLenPrefix::new("fd00:1234::/32".parse().unwrap(), Some(48)).unwrap(), 42u32.into())), + ]), + "{\n \"roas\": [\n { \"asn\": \"AS42\", \"prefix\": \"fd00:1234::/32\", \"maxLength\": 48, \"ta\": \"N/A\" }\n ]\n}" + ); + assert_eq!( + s(vec![ + Payload::Origin(RouteOrigin::new(MaxLenPrefix::new("fd00:1234::/32".parse().unwrap(), Some(48)).unwrap(), 42u32.into())), + Payload::Origin(RouteOrigin::new(MaxLenPrefix::new("fd00:1235::/32".parse().unwrap(), Some(48)).unwrap(), 42u32.into())), + ]), + "{\n \"roas\": [\n { \"asn\": \"AS42\", \"prefix\": \"fd00:1234::/32\", \"maxLength\": 48, \"ta\": \"N/A\" },\n { \"asn\": \"AS42\", \"prefix\": \"fd00:1235::/32\", \"maxLength\": 48, \"ta\": \"N/A\" }\n ]\n}", + ); + + assert_eq!( + s(vec![ + Payload::Aspa(AspaPayload { customer: 42u32.into(), providers: ProviderAsns::try_from_iter(vec![44u32.into(), 45u32.into()]).unwrap() }), + ]), + "{\n \"aspas\": [\n { \"customer_asid\": 42, \"providers\": [44, 45] }\n ]\n}", + ); + assert_eq!( + s(vec![ + Payload::Aspa(AspaPayload { customer: 42u32.into(), providers: ProviderAsns::try_from_iter(vec![44u32.into(), 45u32.into()]).unwrap() }), + Payload::Aspa(AspaPayload { customer: 45u32.into(), providers: ProviderAsns::try_from_iter(vec![46u32.into(), 47u32.into()]).unwrap() }), + ]), + "{\n \"aspas\": [\n { \"customer_asid\": 42, \"providers\": [44, 45] },\n { \"customer_asid\": 45, \"providers\": [46, 47] }\n ]\n}", + ); + + assert_eq!( + s(vec![ + Payload::Aspa(AspaPayload { customer: 42u32.into(), providers: ProviderAsns::try_from_iter(vec![44u32.into(), 45u32.into()]).unwrap() }), + Payload::Origin(RouteOrigin::new(MaxLenPrefix::new("fd00:1234::/32".parse().unwrap(), Some(48)).unwrap(), 42u32.into())), + Payload::Aspa(AspaPayload { customer: 45u32.into(), providers: ProviderAsns::try_from_iter(vec![46u32.into(), 47u32.into()]).unwrap() }), + Payload::Origin(RouteOrigin::new(MaxLenPrefix::new("fd00:1235::/32".parse().unwrap(), Some(48)).unwrap(), 42u32.into())), + ]), + "{\n \"roas\": [\n { \"asn\": \"AS42\", \"prefix\": \"fd00:1234::/32\", \"maxLength\": 48, \"ta\": \"N/A\" },\n { \"asn\": \"AS42\", \"prefix\": \"fd00:1235::/32\", \"maxLength\": 48, \"ta\": \"N/A\" }\n ],\n \"aspas\": [\n { \"customer_asid\": 42, \"providers\": [44, 45] },\n { \"customer_asid\": 45, \"providers\": [46, 47] }\n ]\n}", + ); + } }