channel padding: Test through most of the layers
This commit is contained in:
parent
e4e06f66ec
commit
18a6234101
|
@ -3471,6 +3471,7 @@ dependencies = [
|
|||
"futures",
|
||||
"futures-await-test",
|
||||
"hex-literal",
|
||||
"itertools",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
|
|
|
@ -41,6 +41,9 @@ void = "1"
|
|||
float_eq = "1.0.0"
|
||||
futures-await-test = "0.3.0"
|
||||
hex-literal = "0.3"
|
||||
itertools = "0.10.1"
|
||||
tor-cell = { path = "../tor-cell", version = "0.5.0", features = ["testing"] }
|
||||
tor-netdir = { path = "../tor-netdir", version = "0.5.0", features = ["testing"] }
|
||||
tor-proto = { path = "../tor-proto", version = "0.5.0", features = ["testing"] }
|
||||
tor-rtcompat = { path = "../tor-rtcompat", version = "0.5.0", features = ["tokio", "native-tls"] }
|
||||
tor-rtmock = { path = "../tor-rtmock", version = "0.4.0" }
|
||||
|
|
|
@ -19,6 +19,9 @@ use tor_proto::ChannelsParams;
|
|||
use tor_units::{BoundedInt32, IntegerMilliseconds};
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(test)]
|
||||
mod padding_test;
|
||||
|
||||
/// A map from channel id to channel state, plus necessary auxiliary state
|
||||
///
|
||||
/// We make this a separate type instead of just using
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
//! Tests for padding
|
||||
|
||||
// @@ 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::unwrap_used)]
|
||||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::iter;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::channel::mpsc;
|
||||
use futures_await_test::async_test;
|
||||
use itertools::{zip_eq, Itertools};
|
||||
|
||||
use tor_cell::chancell::msg::PaddingNegotiateCmd;
|
||||
use tor_config::PaddingLevel;
|
||||
use tor_linkspec::HasRelayIds;
|
||||
use tor_proto::channel::{Channel, ChannelUsage, CtrlMsg};
|
||||
|
||||
use crate::mgr::{AbstractChanMgr, ChannelFactory};
|
||||
|
||||
use PaddingLevel as PL;
|
||||
|
||||
const DEF_MS: [u32; 2] = [1500, 9500];
|
||||
const REDUCED_MS: [u32; 2] = [9000, 14000];
|
||||
|
||||
const ADJ_MS: [u32; 2] = [1499, 9499];
|
||||
const ADJ_REDUCED_MS: [u32; 2] = [8999, 13999];
|
||||
|
||||
/// Returns a NetDir that has consensus parameters as specified
|
||||
fn some_interesting_netdir<'v, V>(values: V) -> Arc<NetDir>
|
||||
where
|
||||
V: IntoIterator<Item = (&'v str, i32)>,
|
||||
{
|
||||
tor_netdir::testnet::construct_custom_netdir_with_params(|_, _| {}, values)
|
||||
.unwrap()
|
||||
.unwrap_if_sufficient()
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Returns a NetDir that has consensus parameters different from the protocol default
|
||||
fn interesting_netdir() -> Arc<NetDir> {
|
||||
some_interesting_netdir(
|
||||
[
|
||||
("nf_ito_low", ADJ_MS[0]),
|
||||
("nf_ito_high", ADJ_MS[1]),
|
||||
("nf_ito_low_reduced", ADJ_REDUCED_MS[0]),
|
||||
("nf_ito_high_reduced", ADJ_REDUCED_MS[1]),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v as _)),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_parameters_calculation() {
|
||||
fn one(pconfig: PaddingLevel, netdir: StdResult<&NetDirExtract, &()>, exp: [u32; 2]) {
|
||||
eprintln!(
|
||||
"### {:?} {:?}",
|
||||
&pconfig,
|
||||
netdir.map(|n| n.nf_ito.map(|l| l.map(|v| v.as_millis().get())))
|
||||
);
|
||||
let got = padding_parameters(pconfig, netdir).unwrap();
|
||||
let exp = PaddingParameters::builder()
|
||||
.low_ms(exp[0].into())
|
||||
.high_ms(exp[1].into())
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(got, exp);
|
||||
}
|
||||
|
||||
one(PL::default(), Err(&()), DEF_MS);
|
||||
one(
|
||||
PL::default(),
|
||||
Ok(&NetDirExtract::from(&*interesting_netdir())),
|
||||
ADJ_MS,
|
||||
);
|
||||
|
||||
one(PL::Reduced, Err(&()), REDUCED_MS);
|
||||
one(
|
||||
PL::Reduced,
|
||||
Ok(&NetDirExtract::from(&*interesting_netdir())),
|
||||
ADJ_REDUCED_MS,
|
||||
);
|
||||
|
||||
let make_bogus_netdir = |values: &[(&str, i32)]| {
|
||||
NetDirExtract::from(
|
||||
&tor_netdir::testnet::construct_custom_netdir_with_params(
|
||||
|_, _| {},
|
||||
values.iter().cloned(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap_if_sufficient()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
|
||||
let bogus_netdir = make_bogus_netdir(&[
|
||||
// for testing low > hight
|
||||
("nf_ito_low", ADJ_REDUCED_MS[1] as _),
|
||||
("nf_ito_high", ADJ_REDUCED_MS[0] as _),
|
||||
]);
|
||||
one(PL::default(), Ok(&bogus_netdir), DEF_MS);
|
||||
}
|
||||
|
||||
struct FakeChannelFactory {
|
||||
channel: Channel,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ChannelFactory for FakeChannelFactory {
|
||||
type Channel = Channel;
|
||||
type BuildSpec = ();
|
||||
|
||||
async fn build_channel(&self, _target: &Self::BuildSpec) -> Result<Self::Channel> {
|
||||
Ok(self.channel.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct CaseContext {
|
||||
channel: Channel,
|
||||
recv: mpsc::UnboundedReceiver<CtrlMsg>,
|
||||
chanmgr: AbstractChanMgr<FakeChannelFactory>,
|
||||
netdir: tor_netdir::Result<Arc<NetDir>>,
|
||||
}
|
||||
|
||||
/// Details of an expected control message
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Expected {
|
||||
enabled: Option<bool>,
|
||||
timing: Option<[u32; 2]>,
|
||||
nego: Option<(PaddingNegotiateCmd, [u32; 2])>,
|
||||
}
|
||||
|
||||
async fn case(level: PaddingLevel, dormancy: Dormancy, usage: ChannelUsage) -> CaseContext {
|
||||
let mut cconfig = ChannelConfig::builder();
|
||||
cconfig.padding(level);
|
||||
let cconfig = cconfig.build().unwrap();
|
||||
|
||||
eprintln!("\n---- {:?} {:?} {:?} ----", &cconfig, &dormancy, &usage);
|
||||
|
||||
let (channel, recv) = Channel::new_fake();
|
||||
let peer_id = channel.target().ed_identity().unwrap().clone();
|
||||
let factory = FakeChannelFactory { channel };
|
||||
|
||||
let chanmgr = AbstractChanMgr::new(factory, &cconfig, dormancy);
|
||||
|
||||
let (channel, _prov) = chanmgr.get_or_launch(peer_id, (), usage).await.unwrap();
|
||||
|
||||
let netdir = Err(tor_netdir::Error::NoInfo);
|
||||
|
||||
CaseContext {
|
||||
channel,
|
||||
recv,
|
||||
chanmgr,
|
||||
netdir,
|
||||
}
|
||||
}
|
||||
|
||||
impl CaseContext {
|
||||
fn netdir(&self) -> tor_netdir::Result<Arc<NetDir>> {
|
||||
self.netdir.clone()
|
||||
}
|
||||
|
||||
fn expect_1(&mut self, exp: Expected) {
|
||||
self.expect(vec![exp]);
|
||||
}
|
||||
fn expect_0(&mut self) {
|
||||
self.expect(vec![]);
|
||||
}
|
||||
|
||||
fn expect(&mut self, expected: Vec<Expected>) {
|
||||
let messages = iter::from_fn(|| match self.recv.try_next() {
|
||||
Ok(Some(t)) => Some(Ok(t)),
|
||||
Ok(None) => Some(Err(())),
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
eprintln!("{:#?}", &messages);
|
||||
|
||||
for (i, (got, exp)) in zip_eq(messages, expected).enumerate() {
|
||||
eprintln!("{i} {got:?} {exp:?}");
|
||||
let got: ChannelsParamsUpdates = match got {
|
||||
Ok(CtrlMsg::ConfigUpdate(u)) => (*u).clone(),
|
||||
_ => panic!("wrong message {got:?}"),
|
||||
};
|
||||
|
||||
let Expected {
|
||||
enabled,
|
||||
timing,
|
||||
nego,
|
||||
} = exp;
|
||||
let nego =
|
||||
nego.map(|(cmd, [low, high])| PaddingNegotiate::from_raw(cmd, low as _, high as _));
|
||||
let timing = timing.map(|[low, high]| {
|
||||
PaddingParameters::builder()
|
||||
.low_ms(low.into())
|
||||
.high_ms(high.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(got.padding_enable(), enabled.as_ref());
|
||||
assert_eq!(got.padding_parameters(), timing.as_ref());
|
||||
assert_eq!(got.padding_negotiate(), nego.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test padding control from the top of chanmgr through to just before the channel reactor
|
||||
///
|
||||
/// The rules about when to send padding and what negotiation cells to send are super complex.
|
||||
/// Furthermore, our implementation is spread across several layers, mostly for performance
|
||||
/// reasons (in particular, to do as much of the work centrally, in
|
||||
/// the channel manager, as possible).
|
||||
///
|
||||
/// So here we test what happens if we call the various channel manager methods (the methods on
|
||||
/// `AbstractChanMgr`, not `ChanMgr`, because our channel factory is strange, but the methods of
|
||||
/// `ChanMgr` are simple passthroughs).
|
||||
///
|
||||
/// We observe the effect by pretending that we are the channel reactor, and reading out
|
||||
/// the control messages. The channel reactor logic is very simple: it just does as it's
|
||||
/// told. For example each PaddingNegotiation in a control message will be sent precisely
|
||||
/// once (assuming it can be sent before the channel is closed or the next one arrives).
|
||||
/// The timing parameters, and enablement, are passed directly to the padding timer.
|
||||
#[async_test]
|
||||
async fn padding_control_through_layers() {
|
||||
const STOP_MSG: (PaddingNegotiateCmd, [u32; 2]) = (PaddingNegotiateCmd::STOP, [0, 0]);
|
||||
const START_CMD: PaddingNegotiateCmd = PaddingNegotiateCmd::START;
|
||||
|
||||
// ---- simple case, active exit, defaults ----
|
||||
|
||||
let mut c = case(PL::default(), Dormancy::Active, ChannelUsage::Exit).await;
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(true),
|
||||
timing: None,
|
||||
nego: None,
|
||||
});
|
||||
|
||||
// ---- reduced padding ----
|
||||
|
||||
let mut c = case(PL::Reduced, Dormancy::Active, ChannelUsage::Exit).await;
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(true),
|
||||
timing: Some(REDUCED_MS),
|
||||
nego: Some(STOP_MSG),
|
||||
});
|
||||
|
||||
// ---- dormant ----
|
||||
|
||||
let mut c = case(PL::default(), Dormancy::Dormant, ChannelUsage::Exit).await;
|
||||
c.expect_1(Expected {
|
||||
enabled: None,
|
||||
timing: None,
|
||||
nego: Some(STOP_MSG),
|
||||
});
|
||||
|
||||
// ---- more complicated evolution ----
|
||||
|
||||
let cconfig_reduced = {
|
||||
let mut cconfig = ChannelConfig::builder();
|
||||
cconfig.padding(PL::Reduced);
|
||||
cconfig.build().unwrap()
|
||||
};
|
||||
|
||||
let mut c = case(PL::default(), Dormancy::Active, ChannelUsage::Dir).await;
|
||||
// directory circuits don't get padding (and we don't need to tell the peer to disable)
|
||||
c.expect_0();
|
||||
|
||||
eprintln!("### Exit ###");
|
||||
c.channel.note_usage(ChannelUsage::Exit).unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(true), // we now turn on our padding sender
|
||||
timing: None, // with default parameters
|
||||
nego: None, // the peer will start padding when it sees us do non-dir stuff
|
||||
});
|
||||
|
||||
eprintln!("### set_dormancy - Dormant ###");
|
||||
c.chanmgr
|
||||
.set_dormancy(Dormancy::Dormant, c.netdir())
|
||||
.unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(false), // we now must turn off our padding sender
|
||||
timing: None,
|
||||
nego: Some(STOP_MSG), // and tell the peer to stop
|
||||
});
|
||||
|
||||
eprintln!("### change to reduced padding while dormant ###");
|
||||
c.chanmgr.reconfigure(&cconfig_reduced, c.netdir()).unwrap();
|
||||
c.expect_0();
|
||||
|
||||
eprintln!("### set_dormancy - Active ###");
|
||||
c.chanmgr
|
||||
.set_dormancy(Dormancy::Active, c.netdir())
|
||||
.unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(true),
|
||||
timing: Some(REDUCED_MS),
|
||||
nego: None, // don't enable inbound padding again
|
||||
});
|
||||
|
||||
eprintln!("### imagine a netdir turns up, with some different parameters ###");
|
||||
c.netdir = Ok(interesting_netdir());
|
||||
c.chanmgr.update_netdir(c.netdir()).unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: None, // still enabled
|
||||
timing: Some(ADJ_REDUCED_MS), // parameters adjusted a bit
|
||||
nego: None, // no need to send an update
|
||||
});
|
||||
|
||||
eprintln!("### change back to normal padding ###");
|
||||
c.chanmgr
|
||||
.reconfigure(&ChannelConfig::default(), c.netdir())
|
||||
.unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: None, // still enabled
|
||||
timing: Some(ADJ_MS), // parameters adjusted
|
||||
nego: Some((START_CMD, [0, 0])), // ask peer to use consensus default
|
||||
});
|
||||
|
||||
eprintln!("### consensus changes to no padding ###");
|
||||
// ---- consensus is no padding ----
|
||||
c.netdir = Ok(some_interesting_netdir(
|
||||
[
|
||||
"nf_ito_low",
|
||||
"nf_ito_high",
|
||||
"nf_ito_low_reduced",
|
||||
"nf_ito_high_reduced",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|k| (k, 0)),
|
||||
));
|
||||
c.chanmgr.update_netdir(c.netdir()).unwrap();
|
||||
c.expect_1(Expected {
|
||||
enabled: Some(false),
|
||||
timing: None,
|
||||
nego: None,
|
||||
});
|
||||
|
||||
// Ideally we would somehow test the sending of a START message with nonzero parameters.
|
||||
//
|
||||
// However, that can only occur if we want the peer to send some padding which is not the
|
||||
// consensus default. And we get our own desired parameters from our idea of the consensus:
|
||||
// the config can only enable/disable/reduce (and for reduced, we ask our peer not to send
|
||||
// padding at all).
|
||||
//
|
||||
// The only current arrangements for providing alternative parameters are via netdir overrides,
|
||||
// which (because they override our view of the netdir) alter not only our idea of what to do,
|
||||
// but also our idea of what our peer will do.
|
||||
//
|
||||
// Possibly at some future point we might support specifying padding parameters
|
||||
// separately in the config.
|
||||
}
|
Loading…
Reference in New Issue