Skip to content

Commit

Permalink
feat: add path type with metadata and basic daemon requests (#24)
Browse files Browse the repository at this point in the history
* feat(daemon): add basic types and requests

* refactor: address review comments

* refactor: opaque PathParseError

* refactor: reorganize path module

* feat: use StandardPath within Path

* feat: check lengths of path-metadata vectors

* test(paths): extend and improve tests

* refactor(path): minor reorganization and some tests

* fix: address review comments

* refactor: use Paths iterator type
  • Loading branch information
mlegner authored Nov 21, 2023
1 parent da79f22 commit f2dc8c2
Show file tree
Hide file tree
Showing 16 changed files with 950 additions and 58 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ jobs:
with:
save-if: "false"
- run: sudo apt-get install protobuf-compiler
- run: cargo install cargo-tarpaulin@0.26.1
- run: cargo install cargo-tarpaulin@0.27.1

- name: Run tests and record coverage
run: cargo tarpaulin --workspace --skip-clean --all-targets --doc --out html --out xml
run: cargo tarpaulin --workspace --skip-clean --all-targets --doc --out html --out xml --exclude-files "crates/scion-grpc/*"

- name: Upload coverage report
uses: actions/upload-artifact@v3
Expand Down
7 changes: 7 additions & 0 deletions crates/scion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@ publish = false

[dependencies]
bytes = "1.5.0"
chrono = { version = "0.4.31", default-features = false }
scion-grpc = { version = "0.1.0", path = "../scion-grpc" }
serde = { version = "1.0.188", features = ["derive"] }
thiserror = "1.0.48"
tonic = "0.10.2"
tracing = "0.1.40"

[dev-dependencies]
prost-types = "0.12.2"
7 changes: 5 additions & 2 deletions crates/scion/src/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
mod types;
pub use types::{AsInfo, PathRequestFlags};
mod messages;
pub use messages::{AsInfo, PathRequest};

mod client;
pub use client::{DaemonClient, DaemonClientError};
112 changes: 112 additions & 0 deletions crates/scion/src/daemon/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use scion_grpc::daemon::{v1 as daemon_grpc, v1::daemon_service_client::DaemonServiceClient};
use thiserror::Error;
use tonic::transport::Channel;
use tracing::warn;

use super::{messages::PathRequest, AsInfo};
use crate::{address::IsdAsn, packet::ByEndpoint, path::Path};

#[derive(Debug, Error)]
pub enum DaemonClientError {
#[error("A communication error occurred: {0}")]
ConnectionError(#[from] tonic::transport::Error),
#[error("A gRPC error occurred: {0}")]
GrpcError(#[from] tonic::Status),
#[error("Response contained invalid data")]
InvalidData,
}

/// A service to communicate with the local SCION daemon
#[derive(Clone, Debug)]
pub struct DaemonClient {
connection: Channel,
local_isd_asn: IsdAsn,
}

impl DaemonClient {
/// Create a new client to communicate with the SCION daemon located at `address`
///
/// This attempts to connect to the daemon directly and fetch the local ISD-ASN.
///
/// Errors:
///
/// This returns an error, if any error occurs during the connection setup or during the request
/// for the AS info.
pub async fn connect(address: &str) -> Result<Self, DaemonClientError> {
let mut client = Self {
connection: tonic::transport::Endpoint::new(address.to_string())?
.connect()
.await?,
local_isd_asn: IsdAsn::WILDCARD,
};
client.local_isd_asn = client.as_info(IsdAsn::WILDCARD).await?.isd_asn;

Ok(client)
}

/// Request information about an AS; [`IsdAsn::WILDCARD`] can be used to obtain information
/// about the local AS.
pub async fn as_info(&self, isd_asn: IsdAsn) -> Result<AsInfo, DaemonClientError> {
self.client()
.r#as(daemon_grpc::AsRequest::from(isd_asn))
.await?
.into_inner()
.try_into()
.map_err(|_| DaemonClientError::InvalidData)
}

/// Request a set of end-to-end paths between the source and destination AS
pub async fn paths(&self, request: &PathRequest) -> Result<Paths, DaemonClientError> {
let src_isd_asn = if request.source.is_wildcard() {
self.local_isd_asn
} else {
request.source
};
let isd_asn = ByEndpoint {
source: src_isd_asn,
destination: request.destination,
};
Ok(Paths {
isd_asn,
grpc_paths: self
.client()
.paths(daemon_grpc::PathsRequest::from(request))
.await?
.into_inner()
.paths
.into_iter(),
})
}

#[inline]
pub async fn paths_to(
&self,
destination: IsdAsn,
) -> Result<impl Iterator<Item = Path>, DaemonClientError> {
self.paths(&PathRequest::new(destination)).await
}

fn client(&self) -> DaemonServiceClient<Channel> {
DaemonServiceClient::new(self.connection.clone())
}
}

/// Iterator for SCION [Path]s obtained from the SCION Daemon via gRPC
pub struct Paths {
isd_asn: ByEndpoint<IsdAsn>,
grpc_paths: std::vec::IntoIter<daemon_grpc::Path>,
}

impl Iterator for Paths {
type Item = Path;

fn next(&mut self) -> Option<Self::Item> {
for grpc_path in self.grpc_paths.by_ref() {
match Path::try_from_grpc_with_endpoints(grpc_path, self.isd_asn) {
Ok(path) => return Some(path),
Err(e) => warn!(?e, "a parse error occurred for a path"),
}
}
None
}
}
170 changes: 170 additions & 0 deletions crates/scion/src/daemon/messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::num::TryFromIntError;

use scion_grpc::daemon::v1::{self as daemon_grpc};

use crate::address::IsdAsn;

impl From<IsdAsn> for daemon_grpc::AsRequest {
fn from(value: IsdAsn) -> Self {
Self {
isd_as: value.as_u64(),
}
}
}

/// Information about an AS
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AsInfo {
/// The AS's ISD-ASN
pub isd_asn: IsdAsn,
/// Is the AS a core AS?
pub core: bool,
/// The maximum transmission unit (MTU) in the AS
pub mtu: u16,
}

impl TryFrom<daemon_grpc::AsResponse> for AsInfo {
type Error = TryFromIntError;
fn try_from(value: daemon_grpc::AsResponse) -> Result<Self, Self::Error> {
Ok(Self {
isd_asn: value.isd_as.into(),
core: value.core,
mtu: value.mtu.try_into()?,
})
}
}

/// Path requests specifying source and destination ISD-ASN with some flags
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PathRequest {
pub source: IsdAsn,
pub destination: IsdAsn,
pub flags: PathRequestFlags,
}

/// Flags for path requests
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct PathRequestFlags {
pub refresh: bool,
pub hidden: bool,
}

impl From<&PathRequest> for daemon_grpc::PathsRequest {
fn from(value: &PathRequest) -> Self {
Self {
source_isd_as: value.source.as_u64(),
destination_isd_as: value.destination.as_u64(),
refresh: value.flags.refresh,
hidden: value.flags.hidden,
}
}
}

impl PathRequest {
pub fn new(dst_isd_asn: IsdAsn) -> Self {
Self {
source: IsdAsn::WILDCARD,
destination: dst_isd_asn,
flags: PathRequestFlags::default(),
}
}

pub fn with_src_isd_asn(mut self, src_isd_asn: IsdAsn) -> Self {
self.source = src_isd_asn;
self
}

pub fn with_refresh(mut self) -> Self {
self.flags.refresh = true;
self
}

pub fn with_hidden(mut self) -> Self {
self.flags.hidden = true;
self
}
}

#[cfg(test)]
mod tests {
use super::*;

mod as_info {
use super::*;

#[test]
fn request_conversion() {
assert_eq!(
daemon_grpc::AsRequest::from(IsdAsn::from(42)),
daemon_grpc::AsRequest { isd_as: 42 }
)
}

#[test]
fn response_conversion() {
assert_eq!(
AsInfo::try_from(daemon_grpc::AsResponse {
isd_as: 42,
core: true,
mtu: 1500
}),
Ok(AsInfo {
isd_asn: IsdAsn::from(42),
core: true,
mtu: 1500
})
)
}
}

mod path {
use super::*;

#[test]
fn grpc_conversion() {
assert_eq!(
daemon_grpc::PathsRequest::from(&PathRequest::new(IsdAsn::from(1))),
daemon_grpc::PathsRequest {
source_isd_as: 0,
destination_isd_as: 1,
refresh: false,
hidden: false,
}
)
}

#[test]
fn full_construction() {
let source = IsdAsn::from(42);
let destination = IsdAsn::from(314);
let request = PathRequest::new(destination);
assert_eq!(
request,
PathRequest {
source: IsdAsn::WILDCARD,
destination,
flags: PathRequestFlags::default()
}
);
assert_eq!(
request.with_src_isd_asn(source),
PathRequest {
source,
destination,
flags: PathRequestFlags::default()
}
);
assert_eq!(
request.with_hidden().with_refresh(),
PathRequest {
source: IsdAsn::WILDCARD,
destination,
flags: PathRequestFlags {
refresh: true,
hidden: true
}
}
);
}
}
}
15 changes: 0 additions & 15 deletions crates/scion/src/daemon/types.rs

This file was deleted.

12 changes: 6 additions & 6 deletions crates/scion/src/packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use bytes::{Buf, Bytes};

use crate::{
path::PathErrorKind,
path::DataplanePathErrorKind,
wire_encoding::{WireDecode, WireDecodeWithContext},
};

Expand All @@ -24,8 +24,8 @@ pub use path_header::PathHeader;
/// Instances of an object associated with both a source and destination endpoint.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ByEndpoint<T> {
destination: T,
source: T,
pub destination: T,
pub source: T,
}

/// A SCION network packet.
Expand Down Expand Up @@ -92,11 +92,11 @@ pub enum DecodeError {
#[error("attempted to decode the empty path type")]
EmptyPath,
#[error("invalid path header: {0}")]
InvalidPath(PathErrorKind),
InvalidPath(DataplanePathErrorKind),
}

impl From<PathErrorKind> for DecodeError {
fn from(value: PathErrorKind) -> Self {
impl From<DataplanePathErrorKind> for DecodeError {
fn from(value: DataplanePathErrorKind) -> Self {
Self::InvalidPath(value)
}
}
Loading

0 comments on commit f2dc8c2

Please sign in to comment.