|
|
|
@ -6,29 +6,38 @@
|
|
|
|
|
|
|
|
|
|
#![allow(clippy::needless_pass_by_value)] // TODO HSS remove
|
|
|
|
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::{sync::Arc, time::Duration};
|
|
|
|
|
|
|
|
|
|
use futures::{
|
|
|
|
|
channel::{mpsc, oneshot},
|
|
|
|
|
channel::{
|
|
|
|
|
mpsc::{self, UnboundedReceiver},
|
|
|
|
|
oneshot,
|
|
|
|
|
},
|
|
|
|
|
StreamExt as _,
|
|
|
|
|
};
|
|
|
|
|
use tor_cell::relaycell::{
|
|
|
|
|
hs::est_intro::EstablishIntroDetails,
|
|
|
|
|
hs::est_intro::{self, EstablishIntroDetails},
|
|
|
|
|
msg::{AnyRelayMsg, IntroEstablished, Introduce2},
|
|
|
|
|
RelayMsg as _,
|
|
|
|
|
};
|
|
|
|
|
use tor_circmgr::hspool::HsCircPool;
|
|
|
|
|
use tor_error::{internal, into_internal};
|
|
|
|
|
use tor_error::{debug_report, internal, into_internal};
|
|
|
|
|
use tor_hscrypto::pk::HsIntroPtSessionIdKeypair;
|
|
|
|
|
use tor_linkspec::CircTarget;
|
|
|
|
|
use tor_linkspec::{ChanTarget as _, OwnedCircTarget};
|
|
|
|
|
use tor_netdir::{NetDir, NetDirProvider, Relay};
|
|
|
|
|
use tor_proto::circuit::{ConversationInHandler, MetaCellDisposition};
|
|
|
|
|
use tor_rtcompat::Runtime;
|
|
|
|
|
use tor_proto::circuit::{ClientCirc, ConversationInHandler, MetaCellDisposition};
|
|
|
|
|
use tor_rtcompat::{Runtime, SleepProviderExt as _};
|
|
|
|
|
use tracing::debug;
|
|
|
|
|
|
|
|
|
|
use crate::RendRequest;
|
|
|
|
|
|
|
|
|
|
/// Handle onto the task which is establishing and maintaining one IPT
|
|
|
|
|
pub(crate) struct IptEstablisher {}
|
|
|
|
|
pub(crate) struct IptEstablisher {
|
|
|
|
|
/// A stream of events that is produced whenever we have a change in the
|
|
|
|
|
/// IptStatus for this intro point. Note that this stream is potentially
|
|
|
|
|
/// lossy.
|
|
|
|
|
status_events: postage::watch::Receiver<IptStatus>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// When the `IptEstablisher` is dropped it is torn down
|
|
|
|
|
///
|
|
|
|
@ -69,6 +78,10 @@ pub(crate) enum IptError {
|
|
|
|
|
#[error("Unable to construct signed ESTABLISH_INTRO message")]
|
|
|
|
|
CreateEstablishIntro(#[source] tor_cell::Error),
|
|
|
|
|
|
|
|
|
|
/// We encountered a timeout after building the circuit.
|
|
|
|
|
#[error("Timeout during ESTABLISH_INTRO handshake.")]
|
|
|
|
|
EstablishTimeout,
|
|
|
|
|
|
|
|
|
|
/// We encountered an error while sending our establish_intro
|
|
|
|
|
/// message.
|
|
|
|
|
#[error("Unable to send an ESTABLISH_INTRO message")]
|
|
|
|
@ -81,11 +94,41 @@ pub(crate) enum IptError {
|
|
|
|
|
// Circuit,...
|
|
|
|
|
ReceiveAck,
|
|
|
|
|
|
|
|
|
|
/// We received an invalid INTRO_ESTABLISHED message.
|
|
|
|
|
#[error("Got an invalid INTRO_ESTABLISHED message")]
|
|
|
|
|
BadEstablished,
|
|
|
|
|
|
|
|
|
|
/// We encountered a programming error.
|
|
|
|
|
#[error("Internal error")]
|
|
|
|
|
Bug(#[from] tor_error::Bug),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl tor_error::HasKind for IptError {
|
|
|
|
|
fn kind(&self) -> tor_error::ErrorKind {
|
|
|
|
|
use tor_error::ErrorKind as EK;
|
|
|
|
|
use IptError as E;
|
|
|
|
|
match self {
|
|
|
|
|
E::NoNetdir(_) => EK::BootstrapRequired, // TODO HSS maybe not right.
|
|
|
|
|
E::NetdirProviderShutdown => EK::ArtiShuttingDown,
|
|
|
|
|
E::BuildCircuit(e) => e.kind(),
|
|
|
|
|
E::EstablishTimeout => EK::TorNetworkTimeout, // TODO HSS right?
|
|
|
|
|
E::SendEstablishIntro(e) => e.kind(),
|
|
|
|
|
E::ReceiveAck => EK::RemoteProtocolViolation, // TODO HSS not always right.
|
|
|
|
|
E::BadEstablished => EK::RemoteProtocolViolation,
|
|
|
|
|
E::CreateEstablishIntro(_) => EK::Internal,
|
|
|
|
|
E::Bug(e) => e.kind(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IptError {
|
|
|
|
|
/// Return true if this error appears to be the introduction point's fault.
|
|
|
|
|
fn is_ipt_failure(&self) -> bool {
|
|
|
|
|
// TODO HSS: actually test something here.
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IptEstablisher {
|
|
|
|
|
/// Try to set up, and maintain, an IPT at `Relay`
|
|
|
|
|
///
|
|
|
|
@ -94,14 +137,6 @@ impl IptEstablisher {
|
|
|
|
|
circ_pool: Arc<HsCircPool<R>>,
|
|
|
|
|
dirprovider: Arc<dyn NetDirProvider>,
|
|
|
|
|
relay: &Relay<'_>,
|
|
|
|
|
// Not a postage::watch since we want to count `Good` to `Faulty`
|
|
|
|
|
// transitions
|
|
|
|
|
//
|
|
|
|
|
// (The alternative would be to count them as part of this structure and
|
|
|
|
|
// use a postage watch.)
|
|
|
|
|
//
|
|
|
|
|
// bounded sender with a fixed small bound; OK to stall waiting for manager to catch up
|
|
|
|
|
status: mpsc::Sender<IptStatus>,
|
|
|
|
|
// TODO HSS: this needs to take some configuration
|
|
|
|
|
) -> Result<Self, IptError> {
|
|
|
|
|
todo!()
|
|
|
|
@ -122,7 +157,7 @@ impl IptEstablisher {
|
|
|
|
|
/// `hssvc-ipt-algorithms.md`.
|
|
|
|
|
///
|
|
|
|
|
/// TODO HSS Make that file unneeded.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub(crate) enum IptStatusStatus {
|
|
|
|
|
/// We are (re)establishing our connection to the IPT
|
|
|
|
|
///
|
|
|
|
@ -139,11 +174,11 @@ pub(crate) enum IptStatusStatus {
|
|
|
|
|
/// `Err(IptWantsToRetire)` indicates that the IPT Establisher wants to retire this IPT
|
|
|
|
|
///
|
|
|
|
|
/// This happens when the IPT has had (too) many rendezvous requests.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub(crate) struct IptWantsToRetire;
|
|
|
|
|
|
|
|
|
|
/// The current status of an introduction point.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub(crate) struct IptStatus {
|
|
|
|
|
/// The current state of this introduction point as defined by
|
|
|
|
|
/// `hssvc-ipt-algorithms.md`.
|
|
|
|
@ -151,11 +186,42 @@ pub(crate) struct IptStatus {
|
|
|
|
|
/// TODO HSS Make that file unneeded.
|
|
|
|
|
pub(crate) status: IptStatusStatus,
|
|
|
|
|
|
|
|
|
|
/// How many times have we transitioned into a Faulty state?
|
|
|
|
|
///
|
|
|
|
|
/// (This is not the same as the total number of failed attempts, since it
|
|
|
|
|
/// does not count times we retry from a Faulty state.)
|
|
|
|
|
pub(crate) n_faults: u32,
|
|
|
|
|
|
|
|
|
|
/// The current status of whether this introduction point circuit wants to be
|
|
|
|
|
/// retired based on having processed too many requests.
|
|
|
|
|
pub(crate) wants_to_retire: Result<(), IptWantsToRetire>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IptStatus {
|
|
|
|
|
/// Record that we have successfully connected to an introduction point.
|
|
|
|
|
fn note_open(&mut self) {
|
|
|
|
|
self.status = IptStatusStatus::Good;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Record that we are trying to connect to an introduction point.
|
|
|
|
|
fn note_attempt(&mut self) {
|
|
|
|
|
use IptStatusStatus::*;
|
|
|
|
|
self.status = match self.status {
|
|
|
|
|
Establishing | Good => Establishing,
|
|
|
|
|
Faulty => Faulty, // We don't change status if we think we're broken.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Record that an error has occurred.
|
|
|
|
|
fn note_error(&mut self, err: &IptError) {
|
|
|
|
|
use IptStatusStatus::*;
|
|
|
|
|
if err.is_ipt_failure() && self.status == Good {
|
|
|
|
|
self.n_faults += 1;
|
|
|
|
|
self.status = Faulty;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tor_cell::restricted_msg! {
|
|
|
|
|
/// An acceptable message to receive from an introduction point.
|
|
|
|
|
enum IptMsg : RelayMsg {
|
|
|
|
@ -164,29 +230,125 @@ tor_cell::restricted_msg! {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A set of extensions to send with our `ESTABLISH_INTRO` message.
|
|
|
|
|
///
|
|
|
|
|
/// NOTE: we eventually might want to support unrecognized extensions. But
|
|
|
|
|
/// that's potentially troublesome, since the set of extensions we sent might
|
|
|
|
|
/// have an affect on how we validate the reply.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub(crate) struct EstIntroExtensionSet {
|
|
|
|
|
/// Parameters related to rate-limiting to prevent denial-of-service
|
|
|
|
|
/// attacks.
|
|
|
|
|
dos_params: Option<est_intro::DosParams>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Implementation structure for the task that implements an IptEstablisher.
|
|
|
|
|
struct Reactor<R: Runtime> {
|
|
|
|
|
/// A copy of our runtime, used for timeouts and sleeping.
|
|
|
|
|
runtime: R,
|
|
|
|
|
/// A pool used to create circuits to the introduction point.
|
|
|
|
|
pool: Arc<HsCircPool<R>>,
|
|
|
|
|
/// A provider used to select the other relays in the circuit.
|
|
|
|
|
netdir_provider: Arc<dyn NetDirProvider>,
|
|
|
|
|
/// The target introduction point.
|
|
|
|
|
///
|
|
|
|
|
/// TODO: Should this instead be an identity that we look up in the netdir
|
|
|
|
|
/// provider?
|
|
|
|
|
target: OwnedCircTarget,
|
|
|
|
|
/// The keypair to use when establishing the introduction point.
|
|
|
|
|
///
|
|
|
|
|
/// Knowledge of this private key prevents anybody else from impersonating
|
|
|
|
|
/// us to the introduction point.
|
|
|
|
|
ipt_sid_keypair: HsIntroPtSessionIdKeypair,
|
|
|
|
|
/// The extensions to use when establishing the introduction point.
|
|
|
|
|
///
|
|
|
|
|
/// TODO: Should this be able to change over time if we re-establish this
|
|
|
|
|
/// intro point?
|
|
|
|
|
extensions: EstIntroExtensionSet,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An open session with a single introduction point.
|
|
|
|
|
//
|
|
|
|
|
// TODO: I've used Ipt and IntroPt in this module; maybe we shouldn't.
|
|
|
|
|
pub(crate) struct IntroPtSession {
|
|
|
|
|
/// The circuit to the introduction point, on which we're receiving
|
|
|
|
|
/// Introduce2 messages.
|
|
|
|
|
intro_circ: Arc<ClientCirc>,
|
|
|
|
|
|
|
|
|
|
/// The stream that will receive Introduce2 messages.
|
|
|
|
|
///
|
|
|
|
|
/// TODO: we'll likely want to refactor this. @diziet favors having
|
|
|
|
|
/// `establish_intro_once` take a Sink as an argument, but I think that we
|
|
|
|
|
/// may need to keep this separate so that we can keep the ability to
|
|
|
|
|
/// start/stop the stream of Introduce2 messages, and/or detect when it's
|
|
|
|
|
/// closed. If we don't need to do that, we can refactor.
|
|
|
|
|
introduce_rx: UnboundedReceiver<Introduce2>,
|
|
|
|
|
// TODO HSS: How shall we know if the other side has closed the circuit? We
|
|
|
|
|
// can either wait for introduce_rx to close, or we can use
|
|
|
|
|
// ClientCirc::wait_for_close, if we stabilize it.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How long to allow for an introduction point to get established?
|
|
|
|
|
const ESTABLISH_TIMEOUT: Duration = Duration::new(10, 0); // TODO use a better timeout, taken from circuit estimator.
|
|
|
|
|
|
|
|
|
|
/// How long to wait after a single failure.
|
|
|
|
|
const DELAY_ON_FAILURE: Duration = Duration::new(2, 0); // TODO use stochastic jitter.
|
|
|
|
|
|
|
|
|
|
impl<R: Runtime> Reactor<R> {
|
|
|
|
|
/// Run forever, keeping an introduction point established.
|
|
|
|
|
///
|
|
|
|
|
/// TODO: If we're running this in its own task, we'll want some way to
|
|
|
|
|
/// cancel it.
|
|
|
|
|
async fn keep_intro_established(
|
|
|
|
|
&self,
|
|
|
|
|
mut status_tx: postage::watch::Sender<IptStatus>,
|
|
|
|
|
) -> Result<(), IptError> {
|
|
|
|
|
loop {
|
|
|
|
|
status_tx.borrow_mut().note_attempt();
|
|
|
|
|
let outcome = self
|
|
|
|
|
.runtime
|
|
|
|
|
.timeout(ESTABLISH_TIMEOUT, self.establish_intro_once())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or(Err(IptError::EstablishTimeout));
|
|
|
|
|
|
|
|
|
|
match self.establish_intro_once().await {
|
|
|
|
|
Ok(session) => {
|
|
|
|
|
status_tx.borrow_mut().note_open();
|
|
|
|
|
debug!(
|
|
|
|
|
"Successfully established introduction point with {}",
|
|
|
|
|
self.target.display_chan_target()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// TODO HSS: let session continue until it dies, actually
|
|
|
|
|
// implementing it.
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
status_tx.borrow_mut().note_error(&e);
|
|
|
|
|
debug_report!(
|
|
|
|
|
e,
|
|
|
|
|
"Problem establishing introduction point with {}",
|
|
|
|
|
self.target.display_chan_target()
|
|
|
|
|
);
|
|
|
|
|
self.runtime.sleep(DELAY_ON_FAILURE).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Try, once, to make a circuit to a single relay and establish an introduction
|
|
|
|
|
/// point there.
|
|
|
|
|
///
|
|
|
|
|
/// Does not retry. Does not time out except via `HsCircPool`.
|
|
|
|
|
async fn establish_intro_once<R, T>(
|
|
|
|
|
pool: Arc<HsCircPool<R>>,
|
|
|
|
|
netdir_provider: Arc<dyn NetDirProvider>,
|
|
|
|
|
target: T,
|
|
|
|
|
ipt_sid_keypair: &HsIntroPtSessionIdKeypair,
|
|
|
|
|
// TODO HSS: Take other extensions. Ideally we would take not only
|
|
|
|
|
// DoSParams, but a full IntoIterator<Item=EstablishIntroExt> or ExtList.
|
|
|
|
|
// But both of those types are private in tor-cell. We should consider
|
|
|
|
|
// whether that's what we want.
|
|
|
|
|
) -> Result<(), IptError>
|
|
|
|
|
where
|
|
|
|
|
R: Runtime,
|
|
|
|
|
T: CircTarget,
|
|
|
|
|
{
|
|
|
|
|
async fn establish_intro_once(&self) -> Result<IntroPtSession, IptError> {
|
|
|
|
|
let circuit = {
|
|
|
|
|
let netdir =
|
|
|
|
|
wait_for_netdir(netdir_provider.as_ref(), tor_netdir::Timeliness::Timely).await?;
|
|
|
|
|
let netdir = wait_for_netdir(
|
|
|
|
|
self.netdir_provider.as_ref(),
|
|
|
|
|
tor_netdir::Timeliness::Timely,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
let kind = tor_circmgr::hspool::HsCircKind::SvcIntro;
|
|
|
|
|
pool.get_or_launch_specific(netdir.as_ref(), kind, target)
|
|
|
|
|
self.pool
|
|
|
|
|
.get_or_launch_specific(netdir.as_ref(), kind, self.target.clone())
|
|
|
|
|
.await
|
|
|
|
|
.map_err(IptError::BuildCircuit)?
|
|
|
|
|
// note that netdir is dropped here, to avoid holding on to it any
|
|
|
|
@ -197,16 +359,16 @@ where
|
|
|
|
|
.map_err(into_internal!("Somehow built a circuit with no hops!?"))?;
|
|
|
|
|
|
|
|
|
|
let establish_intro = {
|
|
|
|
|
let ipt_sid_id = ipt_sid_keypair.as_ref().public.into();
|
|
|
|
|
let details = EstablishIntroDetails::new(ipt_sid_id);
|
|
|
|
|
let ipt_sid_id = self.ipt_sid_keypair.as_ref().public.into();
|
|
|
|
|
let mut details = EstablishIntroDetails::new(ipt_sid_id);
|
|
|
|
|
if let Some(dos_params) = &self.extensions.dos_params {
|
|
|
|
|
details.set_extension_dos(dos_params.clone());
|
|
|
|
|
}
|
|
|
|
|
let circuit_binding_key = circuit
|
|
|
|
|
.binding_key(intro_pt_hop)
|
|
|
|
|
.ok_or(internal!("No binding key for introduction point!?"))?;
|
|
|
|
|
if false {
|
|
|
|
|
// TODO HSS: Insert extensions as needed.
|
|
|
|
|
}
|
|
|
|
|
let body: Vec<u8> = details
|
|
|
|
|
.sign_and_encode(ipt_sid_keypair.as_ref(), circuit_binding_key.hs_mac())
|
|
|
|
|
.sign_and_encode(self.ipt_sid_keypair.as_ref(), circuit_binding_key.hs_mac())
|
|
|
|
|
.map_err(IptError::CreateEstablishIntro)?;
|
|
|
|
|
|
|
|
|
|
// TODO HSS: This is ugly, but it is the sensible way to munge the above
|
|
|
|
@ -242,18 +404,17 @@ where
|
|
|
|
|
|
|
|
|
|
let established = established_rx.await.map_err(|_| IptError::ReceiveAck)?;
|
|
|
|
|
|
|
|
|
|
// TODO HSS: handle all the extension data in the established field.
|
|
|
|
|
if established.iter_extensions().next().is_some() {
|
|
|
|
|
// We do not support any extensions from the introduction point; if it
|
|
|
|
|
// sent us any, that's a protocol violation.
|
|
|
|
|
return Err(IptError::BadEstablished);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO HSS: Return the introduce_rx stream along with any related types.
|
|
|
|
|
// Or should we have taken introduce_tx as an argument? (@diziet endorses
|
|
|
|
|
// the "take it as an argument" idea.)
|
|
|
|
|
|
|
|
|
|
// TODO HSS: Return the circuit too, of course.
|
|
|
|
|
|
|
|
|
|
// TODO HSS: How shall we know if the other side has closed the circuit? We could wait
|
|
|
|
|
// for introduce_rx.next() to yield None, but that will only work if we use
|
|
|
|
|
// one mpsc::Sender per circuit...
|
|
|
|
|
todo!()
|
|
|
|
|
Ok(IntroPtSession {
|
|
|
|
|
intro_circ: circuit,
|
|
|
|
|
introduce_rx,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a NetDir from `provider`, waiting until one exists.
|
|
|
|
|