Add linkspec::decode to convert linkspecs to an OwnedChanTarget
Relays and onion service services/clients will both need this. I'm marking this experimental for now; we should stabilize it before we release onion services.
This commit is contained in:
parent
7ce808b75b
commit
88e9976556
|
@ -14,8 +14,9 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
|||
[features]
|
||||
default = []
|
||||
full = ["pt-client"]
|
||||
experimental = []
|
||||
experimental = ["decode"]
|
||||
pt-client = []
|
||||
decode = []
|
||||
|
||||
[dependencies]
|
||||
base64ct = "1.5.1"
|
||||
|
@ -26,6 +27,7 @@ derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
|
|||
derive_more = "0.99.3"
|
||||
educe = "0.4.6"
|
||||
hex = "0.4"
|
||||
itertools = "0.10.1"
|
||||
safelog = { path = "../safelog", version = "0.3.0" }
|
||||
serde = { version = "1.0.103", features = ["derive"] }
|
||||
serde_with = "2.0.1"
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
//! Analyze a list of link specifiers as a `OwnedChanTarget`.
|
||||
//!
|
||||
//! This functionality is used in the onion service subsystem, and for relays.
|
||||
//! The onion service subsystem uses this to decode a description of a relay as
|
||||
//! provided in a HsDesc or an INTRODUCE2 message; relays use this to handle
|
||||
//! EXTEND2 messages and figure out where to send a circuit.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::{LinkSpec, OwnedChanTargetBuilder, RelayIdType};
|
||||
use itertools::Itertools as _;
|
||||
|
||||
/// A rule for how strictly to parse a list of LinkSpecifiers when converting it into
|
||||
/// an [`OwnedChanTarget`](crate::OwnedChanTarget).
|
||||
//
|
||||
// For now, there is only one level of strictness, but it is all but certain
|
||||
// that we will add more in the future.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum Strictness {
|
||||
/// Enforce the standard rules described in `tor-spec`:
|
||||
///
|
||||
/// Namely:
|
||||
/// * There must be exactly one Ed25519 identity.
|
||||
/// * There must be exactly one RSA identity.
|
||||
/// * There must be at least one IPv4 ORPort.
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl OwnedChanTargetBuilder {
|
||||
/// Construct an [`OwnedChanTargetBuilder`] from a list of [`LinkSpec`],
|
||||
/// validating it according to a given level of [`Strictness`].
|
||||
pub fn from_linkspecs(
|
||||
strictness: Strictness,
|
||||
linkspecs: &[LinkSpec],
|
||||
) -> Result<Self, ChanTargetDecodeError> {
|
||||
// We ignore the strictness for now, since there is only one variant.
|
||||
let _ = strictness;
|
||||
|
||||
// There must be exactly one Ed25519 identity.
|
||||
let ed_id = linkspecs
|
||||
.iter()
|
||||
.filter_map(|ls| match ls {
|
||||
LinkSpec::Ed25519Id(ed) => Some(ed),
|
||||
_ => None,
|
||||
})
|
||||
.exactly_one()
|
||||
.map_err(|mut e| match e.next() {
|
||||
Some(_) => ChanTargetDecodeError::DuplicatedId(RelayIdType::Ed25519),
|
||||
None => ChanTargetDecodeError::MissingId(RelayIdType::Ed25519),
|
||||
})?;
|
||||
|
||||
// There must be exactly one RSA identity.
|
||||
let rsa_id = linkspecs
|
||||
.iter()
|
||||
.filter_map(|ls| match ls {
|
||||
LinkSpec::RsaId(rsa) => Some(rsa),
|
||||
_ => None,
|
||||
})
|
||||
.exactly_one()
|
||||
.map_err(|mut e| match e.next() {
|
||||
Some(_) => ChanTargetDecodeError::DuplicatedId(RelayIdType::Rsa),
|
||||
None => ChanTargetDecodeError::MissingId(RelayIdType::Rsa),
|
||||
})?;
|
||||
|
||||
let addrs: Vec<SocketAddr> = linkspecs
|
||||
.iter()
|
||||
.filter_map(|ls| match ls {
|
||||
LinkSpec::OrPort(addr, port) => Some(SocketAddr::new(*addr, *port)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// There must be at least one IPv4 ORPort.
|
||||
if !addrs.iter().any(|addr| addr.is_ipv4()) {
|
||||
return Err(ChanTargetDecodeError::MissingAddr);
|
||||
}
|
||||
let mut builder = OwnedChanTargetBuilder::default();
|
||||
|
||||
builder
|
||||
.ed_identity(*ed_id)
|
||||
.rsa_identity(*rsa_id)
|
||||
.addrs(addrs);
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occurred while constructing a `ChanTarget` from a set of link
|
||||
/// specifiers.
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ChanTargetDecodeError {
|
||||
/// A required identity key was missing.
|
||||
#[error("Missing a required {0} identity key")]
|
||||
MissingId(RelayIdType),
|
||||
/// A required identity key was included more than once.
|
||||
#[error("Duplicated a {0} identity key")]
|
||||
DuplicatedId(RelayIdType),
|
||||
/// A required address type was missing.
|
||||
#[error("Missing a required address type")]
|
||||
MissingAddr,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
#![allow(clippy::bool_assert_comparison)]
|
||||
#![allow(clippy::clone_on_copy)]
|
||||
#![allow(clippy::dbg_macro)]
|
||||
#![allow(clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
#![allow(clippy::single_char_pattern)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::unchecked_duration_subtraction)]
|
||||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
|
||||
use crate::OwnedChanTarget;
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn decode_ok() {
|
||||
let ct = OwnedChanTarget::builder()
|
||||
.addrs(vec![
|
||||
"[::1]:99".parse().unwrap(),
|
||||
"127.0.0.1:11".parse().unwrap(),
|
||||
])
|
||||
.ed_identity([42; 32].into())
|
||||
.rsa_identity([45; 20].into())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let ls = vec![
|
||||
LinkSpec::OrPort("::1".parse().unwrap(), 99),
|
||||
LinkSpec::OrPort("127.0.0.1".parse().unwrap(), 11),
|
||||
LinkSpec::Ed25519Id([42; 32].into()),
|
||||
LinkSpec::RsaId([45; 20].into()),
|
||||
];
|
||||
let ct2 = OwnedChanTargetBuilder::from_linkspecs(Strictness::Standard, &ls)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(format!("{:?}", &ct), format!("{:?}", ct2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_errs() {
|
||||
use ChanTargetDecodeError as E;
|
||||
use RelayIdType as ID;
|
||||
|
||||
let ipv4 = LinkSpec::OrPort("127.0.0.1".parse().unwrap(), 11);
|
||||
let ipv6 = LinkSpec::OrPort("::1".parse().unwrap(), 99);
|
||||
let ed = LinkSpec::Ed25519Id([42; 32].into());
|
||||
let rsa = LinkSpec::RsaId([45; 20].into());
|
||||
let err_from = |lst: &[&LinkSpec]| {
|
||||
OwnedChanTargetBuilder::from_linkspecs(
|
||||
Strictness::Standard,
|
||||
&lst.iter().map(|ls| (*ls).clone()).collect::<Vec<_>>()[..],
|
||||
)
|
||||
.err()
|
||||
};
|
||||
|
||||
assert!(matches!(err_from(&[&ipv4, &ipv6, &ed, &rsa]), None));
|
||||
assert!(matches!(err_from(&[&ipv4, &ed, &rsa]), None));
|
||||
assert!(matches!(
|
||||
err_from(&[&ipv4, &ed, &ed, &rsa]),
|
||||
Some(E::DuplicatedId(ID::Ed25519))
|
||||
));
|
||||
assert!(matches!(
|
||||
err_from(&[&ipv4, &ed, &rsa, &rsa]),
|
||||
Some(E::DuplicatedId(ID::Rsa))
|
||||
));
|
||||
assert!(matches!(
|
||||
err_from(&[&ipv4, &rsa]),
|
||||
Some(E::MissingId(ID::Ed25519))
|
||||
));
|
||||
assert!(matches!(
|
||||
err_from(&[&ipv4, &ed]),
|
||||
Some(E::MissingId(ID::Rsa))
|
||||
));
|
||||
assert!(matches!(
|
||||
err_from(&[&ipv6, &ed, &rsa]),
|
||||
Some(E::MissingAddr)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -38,6 +38,8 @@
|
|||
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
|
||||
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
|
||||
|
||||
#[cfg(feature = "decode")]
|
||||
pub mod decode;
|
||||
mod ids;
|
||||
mod ls;
|
||||
mod owned;
|
||||
|
|
Loading…
Reference in New Issue