diff --git a/Cargo.lock b/Cargo.lock index d8d6738132..0d3bc3e846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4870,6 +4870,7 @@ dependencies = [ "rand 0.8.5", "serde", "thiserror", + "time", "tokio", "tokio-util", ] diff --git a/common/ip-packet-requests/Cargo.toml b/common/ip-packet-requests/Cargo.toml index e4b1f7311a..d25578e87f 100644 --- a/common/ip-packet-requests/Cargo.toml +++ b/common/ip-packet-requests/Cargo.toml @@ -16,5 +16,6 @@ nym-sphinx = { path = "../nymsphinx" } rand = "0.8.5" serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +time = { workspace = true } tokio = { workspace = true, features = ["time"] } tokio-util = { workspace = true, features = ["codec"] } diff --git a/common/ip-packet-requests/src/lib.rs b/common/ip-packet-requests/src/lib.rs index e4d555bcf7..1da258c256 100644 --- a/common/ip-packet-requests/src/lib.rs +++ b/common/ip-packet-requests/src/lib.rs @@ -12,11 +12,13 @@ pub use v6::response; pub mod codec; pub mod v6; +pub mod v7; // version 3: initial version // version 4: IPv6 support // version 5: Add severity level to info response // version 6: Increase the available IPs +// version 7: Add signature support (for the future) pub const CURRENT_VERSION: u8 = 6; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/common/ip-packet-requests/src/v7/mod.rs b/common/ip-packet-requests/src/v7/mod.rs new file mode 100644 index 0000000000..e0062185c4 --- /dev/null +++ b/common/ip-packet-requests/src/v7/mod.rs @@ -0,0 +1,2 @@ +pub mod request; +pub mod response; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs new file mode 100644 index 0000000000..9efeaf2507 --- /dev/null +++ b/common/ip-packet-requests/src/v7/request.rs @@ -0,0 +1,395 @@ +use nym_sphinx::addressing::clients::Recipient; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::{make_bincode_serializer, IpPair, CURRENT_VERSION}; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IpPacketRequest { + pub version: u8, + pub data: IpPacketRequestData, +} + +impl IpPacketRequest { + pub fn new_static_connect_request( + ips: IpPair, + reply_to: Recipient, + reply_to_hops: Option, + reply_to_avg_mix_delays: Option, + buffer_timeout: Option, + ) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::StaticConnect(SignedStaticConnectRequest { + request: StaticConnectRequest { + request_id, + ips, + reply_to, + reply_to_hops, + reply_to_avg_mix_delays, + buffer_timeout, + timestamp: OffsetDateTime::now_utc(), + }, + signature: None, + }), + }, + request_id, + ) + } + + pub fn new_dynamic_connect_request( + reply_to: Recipient, + reply_to_hops: Option, + reply_to_avg_mix_delays: Option, + buffer_timeout: Option, + ) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::DynamicConnect(SignedDynamicConnectRequest { + request: DynamicConnectRequest { + request_id, + reply_to, + reply_to_hops, + reply_to_avg_mix_delays, + buffer_timeout, + timestamp: OffsetDateTime::now_utc(), + }, + signature: None, + }), + }, + request_id, + ) + } + + pub fn new_disconnect_request(reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::Disconnect(SignedDisconnectRequest { + request: DisconnectRequest { + request_id, + reply_to, + timestamp: OffsetDateTime::now_utc(), + }, + signature: None, + }), + }, + request_id, + ) + } + + pub fn new_data_request(ip_packets: bytes::Bytes) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::Data(DataRequest { ip_packets }), + } + } + + pub fn new_ping(reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::Ping(PingRequest { + request_id, + reply_to, + timestamp: OffsetDateTime::now_utc(), + }), + }, + request_id, + ) + } + + pub fn new_health_request(reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + version: CURRENT_VERSION, + data: IpPacketRequestData::Health(HealthRequest { + request_id, + reply_to, + timestamp: OffsetDateTime::now_utc(), + }), + }, + request_id, + ) + } + + pub fn id(&self) -> Option { + match &self.data { + IpPacketRequestData::StaticConnect(request) => Some(request.request.request_id), + IpPacketRequestData::DynamicConnect(request) => Some(request.request.request_id), + IpPacketRequestData::Disconnect(request) => Some(request.request.request_id), + IpPacketRequestData::Data(_) => None, + IpPacketRequestData::Ping(request) => Some(request.request_id), + IpPacketRequestData::Health(request) => Some(request.request_id), + } + } + + pub fn recipient(&self) -> Option<&Recipient> { + match &self.data { + IpPacketRequestData::StaticConnect(request) => Some(&request.request.reply_to), + IpPacketRequestData::DynamicConnect(request) => Some(&request.request.reply_to), + IpPacketRequestData::Disconnect(request) => Some(&request.request.reply_to), + IpPacketRequestData::Data(_) => None, + IpPacketRequestData::Ping(request) => Some(&request.reply_to), + IpPacketRequestData::Health(request) => Some(&request.reply_to), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum IpPacketRequestData { + StaticConnect(SignedStaticConnectRequest), + DynamicConnect(SignedDynamicConnectRequest), + Disconnect(SignedDisconnectRequest), + Data(DataRequest), + Ping(PingRequest), + Health(HealthRequest), +} + +impl IpPacketRequestData { + pub fn add_signature(&mut self, signature: Vec) -> Option> { + match self { + IpPacketRequestData::StaticConnect(request) => { + request.signature = Some(signature); + request.signature.clone() + } + IpPacketRequestData::DynamicConnect(request) => { + request.signature = Some(signature); + request.signature.clone() + } + IpPacketRequestData::Disconnect(request) => { + request.signature = Some(signature); + request.signature.clone() + } + IpPacketRequestData::Data(_) + | IpPacketRequestData::Ping(_) + | IpPacketRequestData::Health(_) => None, + } + } +} + +// A static connect request is when the client provides the internal IP address it will use on the +// ip packet router. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct StaticConnectRequest { + pub request_id: u64, + + pub ips: IpPair, + + // The nym-address the response should be sent back to + pub reply_to: Recipient, + + // The number of mix node hops that responses should take, in addition to the entry and exit + // node. Zero means only client -> entry -> exit -> client. + pub reply_to_hops: Option, + + // The average delay at each mix node, in milliseconds. Currently this is not supported by the + // ip packet router. + pub reply_to_avg_mix_delays: Option, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +impl StaticConnectRequest { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedStaticConnectRequest { + pub request: StaticConnectRequest, + pub signature: Option>, +} + +// A dynamic connect request is when the client does not provide the internal IP address it will use +// on the ip packet router, and instead requests one to be assigned to it. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DynamicConnectRequest { + pub request_id: u64, + + // The nym-address the response should be sent back to + pub reply_to: Recipient, + + // The number of mix node hops that responses should take, in addition to the entry and exit + // node. Zero means only client -> entry -> exit -> client. + pub reply_to_hops: Option, + + // The average delay at each mix node, in milliseconds. Currently this is not supported by the + // ip packet router. + pub reply_to_avg_mix_delays: Option, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +impl DynamicConnectRequest { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDynamicConnectRequest { + pub request: DynamicConnectRequest, + pub signature: Option>, +} + +// A disconnect request is when the client wants to disconnect from the ip packet router and free +// up the allocated IP address. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DisconnectRequest { + pub request_id: u64, + + // The nym-address the response should be sent back to + pub reply_to: Recipient, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +impl DisconnectRequest { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDisconnectRequest { + pub request: DisconnectRequest, + pub signature: Option>, +} + +// A data request is when the client wants to send an IP packet to a destination. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DataRequest { + pub ip_packets: bytes::Bytes, +} + +// A ping request is when the client wants to check if the ip packet router is still alive. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PingRequest { + pub request_id: u64, + + // The nym-address the response should be sent back to + pub reply_to: Recipient, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct HealthRequest { + pub request_id: u64, + + // The nym-address the response should be sent back to + pub reply_to: Recipient, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + + #[test] + fn check_size_of_request() { + let connect = IpPacketRequest { + version: 4, + data: IpPacketRequestData::StaticConnect( + SignedStaticConnectRequest { + request: StaticConnectRequest { + request_id: 123, + ips: IpPair::new(Ipv4Addr::from_str("10.0.0.1").unwrap(), Ipv6Addr::from_str("2001:db8:a160::1").unwrap()), + reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(), + reply_to_hops: None, + reply_to_avg_mix_delays: None, + buffer_timeout: None, + timestamp: OffsetDateTime::now_utc(), + }, + signature: None, + } + ), + }; + assert_eq!(connect.to_bytes().unwrap().len(), 139); + } + + #[test] + fn check_size_of_data() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1u8; 32]), + }), + }; + assert_eq!(data.to_bytes().unwrap().len(), 35); + } + + #[test] + fn serialize_and_deserialize_data_request() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }), + }; + + let serialized = data.to_bytes().unwrap(); + let deserialized = IpPacketRequest::from_reconstructed_message( + &nym_sphinx::receiver::ReconstructedMessage { + message: serialized, + sender_tag: None, + }, + ) + .unwrap(); + + assert_eq!(deserialized.version, 4); + assert_eq!( + deserialized.data, + IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }) + ); + } +} diff --git a/common/ip-packet-requests/src/v7/response.rs b/common/ip-packet-requests/src/v7/response.rs new file mode 100644 index 0000000000..13312dc5cd --- /dev/null +++ b/common/ip-packet-requests/src/v7/response.rs @@ -0,0 +1,410 @@ +use nym_sphinx::addressing::clients::Recipient; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, IpPair, CURRENT_VERSION}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IpPacketResponse { + pub version: u8, + pub data: IpPacketResponseData, +} + +impl IpPacketResponse { + pub fn new_static_connect_success(request_id: u64, reply_to: Recipient) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::StaticConnect(StaticConnectResponse { + request_id, + reply_to, + reply: StaticConnectResponseReply::Success, + }), + } + } + + pub fn new_static_connect_failure( + request_id: u64, + reply_to: Recipient, + reason: StaticConnectFailureReason, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::StaticConnect(StaticConnectResponse { + request_id, + reply_to, + reply: StaticConnectResponseReply::Failure(reason), + }), + } + } + + pub fn new_dynamic_connect_success(request_id: u64, reply_to: Recipient, ips: IpPair) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse { + request_id, + reply_to, + reply: DynamicConnectResponseReply::Success(DynamicConnectSuccess { ips }), + }), + } + } + + pub fn new_dynamic_connect_failure( + request_id: u64, + reply_to: Recipient, + reason: DynamicConnectFailureReason, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse { + request_id, + reply_to, + reply: DynamicConnectResponseReply::Failure(reason), + }), + } + } + + pub fn new_disconnect_success(request_id: u64, reply_to: Recipient) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Disconnect(DisconnectResponse { + request_id, + reply_to, + reply: DisconnectResponseReply::Success, + }), + } + } + + pub fn new_disconnect_failure( + request_id: u64, + reply_to: Recipient, + reason: DisconnectFailureReason, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Disconnect(DisconnectResponse { + request_id, + reply_to, + reply: DisconnectResponseReply::Failure(reason), + }), + } + } + + pub fn new_unrequested_disconnect( + reply_to: Recipient, + reason: UnrequestedDisconnectReason, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::UnrequestedDisconnect(UnrequestedDisconnect { + reply_to, + reason, + }), + } + } + + pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Data(DataResponse { ip_packet }), + } + } + + pub fn new_version_mismatch( + request_id: u64, + reply_to: Recipient, + request_version: u8, + our_version: u8, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Info(InfoResponse { + request_id, + reply_to, + reply: InfoResponseReply::VersionMismatch { + request_version, + response_version: our_version, + }, + level: InfoLevel::Error, + }), + } + } + + pub fn new_data_info_response( + reply_to: Recipient, + reply: InfoResponseReply, + level: InfoLevel, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Info(InfoResponse { + request_id: 0, + reply_to, + reply, + level, + }), + } + } + + pub fn new_pong(request_id: u64, reply_to: Recipient) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Pong(PongResponse { + request_id, + reply_to, + }), + } + } + + pub fn new_health_response( + request_id: u64, + reply_to: Recipient, + build_info: nym_bin_common::build_information::BinaryBuildInformationOwned, + routable: Option, + ) -> Self { + Self { + version: CURRENT_VERSION, + data: IpPacketResponseData::Health(HealthResponse { + request_id, + reply_to, + reply: HealthResponseReply { + build_info, + routable, + }, + }), + } + } + + pub fn id(&self) -> Option { + match &self.data { + IpPacketResponseData::StaticConnect(response) => Some(response.request_id), + IpPacketResponseData::DynamicConnect(response) => Some(response.request_id), + IpPacketResponseData::Disconnect(response) => Some(response.request_id), + IpPacketResponseData::UnrequestedDisconnect(_) => None, + IpPacketResponseData::Data(_) => None, + IpPacketResponseData::Pong(response) => Some(response.request_id), + IpPacketResponseData::Health(response) => Some(response.request_id), + IpPacketResponseData::Info(response) => Some(response.request_id), + } + } + + pub fn recipient(&self) -> Option<&Recipient> { + match &self.data { + IpPacketResponseData::StaticConnect(response) => Some(&response.reply_to), + IpPacketResponseData::DynamicConnect(response) => Some(&response.reply_to), + IpPacketResponseData::Disconnect(response) => Some(&response.reply_to), + IpPacketResponseData::UnrequestedDisconnect(response) => Some(&response.reply_to), + IpPacketResponseData::Data(_) => None, + IpPacketResponseData::Pong(response) => Some(&response.reply_to), + IpPacketResponseData::Health(response) => Some(&response.reply_to), + IpPacketResponseData::Info(response) => Some(&response.reply_to), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum IpPacketResponseData { + // Response for a static connect request + StaticConnect(StaticConnectResponse), + + // Response for a dynamic connect request + DynamicConnect(DynamicConnectResponse), + + // Response for a disconnect initiqated by the client + Disconnect(DisconnectResponse), + + // Message from the server that the client got disconnected without the client initiating it + UnrequestedDisconnect(UnrequestedDisconnect), + + // Response to a data request + Data(DataResponse), + + // Response to ping request + Pong(PongResponse), + + // Response for a health request + Health(HealthResponse), + + // Info response. This can be anything from informative messages to errors + Info(InfoResponse), +} + +impl IpPacketResponseData { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StaticConnectResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: StaticConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum StaticConnectResponseReply { + Success, + Failure(StaticConnectFailureReason), +} + +impl StaticConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + StaticConnectResponseReply::Success => true, + StaticConnectResponseReply::Failure(_) => false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum StaticConnectFailureReason { + #[error("requested ip address is already in use")] + RequestedIpAlreadyInUse, + #[error("requested nym-address is already in use")] + RequestedNymAddressAlreadyInUse, + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: DynamicConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DynamicConnectResponseReply { + Success(DynamicConnectSuccess), + Failure(DynamicConnectFailureReason), +} + +impl DynamicConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + DynamicConnectResponseReply::Success(_) => true, + DynamicConnectResponseReply::Failure(_) => false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectSuccess { + pub ips: IpPair, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DynamicConnectFailureReason { + #[error("requested nym-address is already in use")] + RequestedNymAddressAlreadyInUse, + #[error("no available ip address")] + NoAvailableIp, + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DisconnectResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: DisconnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DisconnectResponseReply { + Success, + Failure(DisconnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DisconnectFailureReason { + #[error("requested nym-address is not currently connected")] + RequestedNymAddressNotConnected, + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UnrequestedDisconnect { + pub reply_to: Recipient, + pub reason: UnrequestedDisconnectReason, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum UnrequestedDisconnectReason { + #[error("client mixnet traffic timeout")] + ClientMixnetTrafficTimeout, + #[error("client tun traffic timeout")] + ClientTunTrafficTimeout, + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DataResponse { + pub ip_packet: bytes::Bytes, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PongResponse { + pub request_id: u64, + pub reply_to: Recipient, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: HealthResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponseReply { + // Return the binary build information of the IPR + pub build_info: nym_bin_common::build_information::BinaryBuildInformationOwned, + // Return if the IPR has performed a successful routing test. + pub routable: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InfoResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: InfoResponseReply, + pub level: InfoLevel, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum InfoResponseReply { + #[error("{msg}")] + Generic { msg: String }, + #[error( + "version mismatch: response is v{request_version} and response is v{response_version}" + )] + VersionMismatch { + request_version: u8, + response_version: u8, + }, + #[error("destination failed exit policy filter check: {dst}")] + ExitPolicyFilterCheckFailed { dst: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum InfoLevel { + Info, + Warn, + Error, +}