#![forbid(unsafe_code)]
use crate::mixnet::Indicator;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransmitPolicy {
Mixnet,
Nakednet,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MixnetRoute {
Nakednet,
Mixnet(zingo_netutils::conduit::MixnetConduit),
}
#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
pub enum MixnetNotReady {
#[error(
"the Nym mixnet is not enabled; this operation requires it and refuses rather than \
use nakednet without consent. Enable Mixnet Mode to proceed"
)]
Unattached,
#[error("the Nym mixnet is bootstrapping; this operation requires it to be ready")]
Bootstrapping,
#[error(
"the Nym mixnet proxy died; this operation refuses rather than fall back to \
nakednet. Re-enable Mixnet Mode to restart the proxy"
)]
Died,
}
pub fn resolve_mixnet_only_route(
mode: Indicator,
conduit: Option<zingo_netutils::conduit::MixnetConduit>,
) -> Result<zingo_netutils::conduit::MixnetConduit, MixnetNotReady> {
match mode {
Indicator::Unattached | Indicator::SwitchedOff => Err(MixnetNotReady::Unattached),
Indicator::Ready | Indicator::PreviouslyProvenThisEpoch => {
conduit.ok_or(MixnetNotReady::Bootstrapping)
}
Indicator::Bootstrapping => Err(MixnetNotReady::Bootstrapping),
Indicator::Died => Err(MixnetNotReady::Died),
}
}
pub fn resolve_send_route(
policy: TransmitPolicy,
mode: Indicator,
conduit: Option<zingo_netutils::conduit::MixnetConduit>,
) -> Result<MixnetRoute, MixnetNotReady> {
match policy {
TransmitPolicy::Nakednet => Ok(MixnetRoute::Nakednet),
TransmitPolicy::Mixnet => resolve_mixnet_only_route(mode, conduit).map(MixnetRoute::Mixnet),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn conduit() -> zingo_netutils::conduit::MixnetConduit {
zingo_netutils::conduit::MixnetConduit::over(
"127.0.0.1:9050".parse().expect("the test address parses"),
)
}
const REFUSING: [(Indicator, MixnetNotReady); 4] = [
(Indicator::Unattached, MixnetNotReady::Unattached),
(Indicator::SwitchedOff, MixnetNotReady::Unattached),
(Indicator::Bootstrapping, MixnetNotReady::Bootstrapping),
(Indicator::Died, MixnetNotReady::Died),
];
const ROUTING: [Indicator; 2] = [Indicator::Ready, Indicator::PreviouslyProvenThisEpoch];
#[test]
fn the_matrix_covers_every_indicator_exactly_once() {
let mut rows: Vec<Indicator> = REFUSING
.iter()
.map(|(mode, _)| *mode)
.chain(ROUTING)
.collect();
rows.sort_by_key(|mode| mode.as_str());
let mut all = Indicator::ALL.to_vec();
all.sort_by_key(|mode| mode.as_str());
assert_eq!(rows, all);
}
#[test]
fn a_mixnet_only_surface_refuses_every_state_but_ready() {
for (mode, refusal) in REFUSING {
assert_eq!(
resolve_mixnet_only_route(mode, None),
Err(refusal),
"{mode} must refuse"
);
assert_eq!(
resolve_mixnet_only_route(mode, Some(conduit())),
Err(refusal),
"a stray conduit must not conjure a route while {mode}"
);
}
}
#[test]
fn a_mixnet_only_surface_rides_the_conduit_when_ready() {
for mode in ROUTING {
let routed = resolve_mixnet_only_route(mode, Some(conduit()))
.unwrap_or_else(|refusal| panic!("{mode} must route, refused {refusal}"));
assert_eq!(
routed.dial().socks5(),
"127.0.0.1:9050"
.parse::<std::net::SocketAddr>()
.expect("the test address parses")
);
}
}
#[test]
fn ready_without_a_conduit_refuses_as_bootstrapping() {
for mode in ROUTING {
assert_eq!(
resolve_mixnet_only_route(mode, None),
Err(MixnetNotReady::Bootstrapping)
);
assert_eq!(
resolve_send_route(TransmitPolicy::Mixnet, mode, None),
Err(MixnetNotReady::Bootstrapping)
);
}
}
#[test]
fn the_route_carries_the_session_s_own_conduit() {
let session = conduit();
let routed = resolve_mixnet_only_route(Indicator::Ready, Some(session.clone())).unwrap();
let held = routed.dial();
assert_eq!(
session.in_flight(),
1,
"a use of the routed conduit must count against the session's"
);
drop(held);
assert_eq!(session.in_flight(), 0);
}
#[test]
fn the_nakednet_policy_transmits_over_nakednet_in_every_state() {
for mode in Indicator::ALL {
assert_eq!(
resolve_send_route(TransmitPolicy::Nakednet, mode, None),
Ok(MixnetRoute::Nakednet),
"{mode} must not block a nakednet send"
);
assert_eq!(
resolve_send_route(TransmitPolicy::Nakednet, mode, Some(conduit())),
Ok(MixnetRoute::Nakednet),
"a ready conduit must not override the nakednet policy while {mode}"
);
}
}
#[test]
fn the_mixnet_policy_refuses_exactly_as_a_mixnet_only_surface() {
for (mode, refusal) in REFUSING {
assert_eq!(
resolve_send_route(TransmitPolicy::Mixnet, mode, None),
Err(refusal),
"{mode} must refuse a mixnet send"
);
assert_eq!(
resolve_send_route(TransmitPolicy::Mixnet, mode, Some(conduit())),
Err(refusal),
"a stray conduit must not conjure a mixnet send while {mode}"
);
}
}
#[test]
fn the_mixnet_policy_rides_the_conduit_when_ready() {
for mode in ROUTING {
let session = conduit();
match resolve_send_route(TransmitPolicy::Mixnet, mode, Some(session.clone())) {
Ok(MixnetRoute::Mixnet(routed)) => {
let _held = routed.dial();
assert_eq!(
session.in_flight(),
1,
"the send must ride the session's conduit"
);
}
other => panic!("{mode} must route through the proxy, got {other:?}"),
}
}
}
#[test]
fn the_refusal_message_names_the_actual_state() {
let bootstrapping = MixnetNotReady::Bootstrapping.to_string();
assert!(bootstrapping.contains("bootstrapping"), "{bootstrapping}");
let died = MixnetNotReady::Died.to_string();
assert!(died.contains("died"), "{died}");
assert!(!died.contains("bootstrapping"), "{died}");
let unattached = MixnetNotReady::Unattached.to_string();
assert!(unattached.contains("not enabled"), "{unattached}");
assert!(!unattached.contains("bootstrapping"), "{unattached}");
assert!(!unattached.contains("died"), "{unattached}");
}
#[test]
fn no_refusal_speaks_a_frontend_s_vocabulary() {
for refusal in [
MixnetNotReady::Unattached,
MixnetNotReady::Bootstrapping,
MixnetNotReady::Died,
] {
let rendered = refusal.to_string();
for command in ["network on", "network off", "network status"] {
assert!(
!rendered.contains(command),
"the {refusal:?} refusal spells the command `{command}`, \
which a frontend without a command line cannot offer: \
{rendered}"
);
}
}
}
}