channel padding: Test through most of the layers

This commit is contained in:
Ian Jackson 2022-07-22 18:14:05 +01:00
parent e4e06f66ec
commit 18a6234101
4 changed files with 367 additions and 0 deletions

1
Cargo.lock generated
View File

@ -3471,6 +3471,7 @@ dependencies = [
"futures",
"futures-await-test",
"hex-literal",
"itertools",
"postage",
"rand 0.8.5",
"serde",

View File

@ -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" }

View File

@ -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

View File

@ -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.
}