Skip to content

Commit

Permalink
support aspa
Browse files Browse the repository at this point in the history
  • Loading branch information
devsnek committed Jan 15, 2025
1 parent b79ebe7 commit 8c56d81
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 63 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = { git = "https://github.com/NLnetLabs/rpki-rs", rev = "df247c091824fa58c60c7322d260d21a4f2bcebd", features = ["crypto", "rtr", "slurm"] }
rustls-pemfile = "2.1.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand All @@ -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"
Expand Down
289 changes: 230 additions & 59 deletions src/formats/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,8 @@ use crate::payload;
pub struct Set {
/// The list of VRPs.
roas: Vec<Vrp>,
/// The list of ASPAs.
aspas: Option<Vec<Aspa>>,
}

impl Set {
Expand All @@ -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()
}
}
Expand Down Expand Up @@ -78,6 +86,36 @@ impl TryFrom<JsonVrp> 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<JsonAspa> for Aspa {
type Error = ProviderAsnsError;

fn try_from(json: JsonAspa) -> Result<Self, Self::Error> {
let providers = ProviderAsns::try_from_iter(json.providers.into_iter())?;
Ok(Self {
payload: AspaPayload {
customer: json.customer_asid,
providers,
},
})
}
}


//============ Serialization =================================================


Expand All @@ -90,7 +128,7 @@ impl TryFrom<JsonVrp> for Vrp {
struct JsonVrp {
/// The prefix member.
prefix: Prefix,

/// The ASN member.
#[serde(
serialize_with = "Asn::serialize_as_str",
Expand All @@ -113,6 +151,24 @@ impl From<Vrp> for JsonVrp {
}
}

//------------ JsonAspa ------------------------------------------------------

#[derive(Clone, Debug, Deserialize, Serialize)]
struct JsonAspa {
/// The customer ASN.
customer_asid: Asn,
/// The provider ASNs.
providers: Vec<Asn>,
}

impl From<Aspa> for JsonAspa {
fn from(aspa: Aspa) -> Self {
Self {
customer_asid: aspa.payload.customer,
providers: aspa.payload.providers.iter().collect(),
}
}
}

//============ Output ========================================================

Expand All @@ -128,90 +184,149 @@ 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 {
/// Creates a new output stream for the given payload set.
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<RouteOrigin> {
loop {
match self.iter.next() {
Some(PayloadRef::Origin(value)) => return Some(value),
None => return None,
_ => {}
}
}
}
fn format_origin(origin: RouteOrigin, last: bool) -> Vec<u8> {
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<u8> {
format!(
r#" {{ "customer_asid": {}, "providers": {:?} }}{}"#,
aspa.customer.into_u32(),
aspa.providers.iter().map(|a| a.into_u32()).collect::<Vec<_>>(),
if last { "\n" } else { ",\n" },
).into_bytes()
}

impl Iterator for OutputStream {
type Item = Vec<u8>;

fn next(&mut self) -> Option<Self::Item> {
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".into())
}
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".into())
}
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".into())
} else {
Some(b" ],\n".into())
}
}
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".into())
}
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".into())
} else {
Some(b" ],\n".into())
}
}
StreamState::Done => {
None
StreamState::End => {
self.state = StreamState::Done;
Some(b"}".into())
}
StreamState::Done => None,
}
}
}
Expand Down Expand Up @@ -257,5 +372,61 @@ mod test {
include_bytes!("../../test-data/vrps.rpki-client.json")
).unwrap());
}

#[test]
fn serialize() {
fn s(items: Vec<Payload>) -> 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}",
);
}
}

0 comments on commit 8c56d81

Please sign in to comment.