Lines
0 %
Functions
Branches
100 %
//! Implementation for the introduce-and-rendezvous handshake.
use super::*;
// These imports just here, because they have names unsuitable for importing widely.
use tor_cell::relaycell::{
hs::intro_payload::{IntroduceHandshakePayload, OnionKey},
msg::{Introduce2, Rendezvous1},
};
use tor_circmgr::build::onion_circparams_from_netparams;
use tor_linkspec::{decode::Strictness, verbatim::VerbatimLinkSpecCircTarget};
use tor_proto::{
circuit::handshake::{
self,
hs_ntor::{self, HsNtorHkdfKeyGenerator},
},
stream::{IncomingStream, IncomingStreamRequestFilter},
/// An error produced while trying to process an introduction request we have
/// received from a client via an introduction point.
#[derive(Debug, Clone, thiserror::Error)]
#[allow(clippy::enum_variant_names)]
#[non_exhaustive]
pub enum IntroRequestError {
/// The handshake (e.g. hs_ntor) in the Introduce2 message was invalid and
/// could not be completed.
#[error("Introduction handshake was invalid")]
InvalidHandshake(#[source] tor_proto::Error),
/// The decrypted payload of the Introduce2 message could not be parsed.
#[error("Could not parse INTRODUCE2 payload")]
InvalidPayload(#[source] tor_bytes::Error),
/// We weren't able to build a ChanTarget from the Introduce2 message.
#[error("Invalid link specifiers in INTRODUCE2 payload")]
InvalidLinkSpecs(#[source] tor_linkspec::decode::ChanTargetDecodeError),
/// We weren't able to obtain the subcredentials for decrypting the Introduce2 message.
#[error("Could not obtain subcredentials")]
Subcredentials(#[source] crate::FatalError),
}
impl HasKind for IntroRequestError {
fn kind(&self) -> tor_error::ErrorKind {
use tor_error::ErrorKind as EK;
use IntroRequestError as E;
match self {
E::InvalidHandshake(e) => e.kind(),
E::InvalidPayload(_) => EK::RemoteProtocolViolation,
E::InvalidLinkSpecs(_) => EK::RemoteProtocolViolation,
E::Subcredentials(e) => e.kind(),
/// An error produced while trying to connect to a rendezvous point and open a
/// session with a client.
pub enum EstablishSessionError {
/// We couldn't get a timely network directory in order to build our
/// chosen circuits.
#[error("Network directory not available")]
NetdirUnavailable(#[source] tor_netdir::Error),
/// Got an onion key with an unrecognized type (not ntor).
#[error("Received an unsupported type of onion key")]
UnsupportedOnionKey,
/// Unable to build a circuit to the rendezvous point.
#[error("Could not establish circuit to rendezvous point")]
RendCirc(#[source] RetryError<tor_circmgr::Error>),
/// Encountered a failure while trying to add a virtual hop to the circuit.
#[error("Could not add virtual hop to circuit")]
VirtualHop(#[source] tor_proto::Error),
/// We encountered an error while configuring the virtual hop to send us
/// BEGIN messages.
#[error("Could not configure circuit to allow BEGIN messages")]
AcceptBegins(#[source] tor_proto::Error),
/// We encountered an error while sending the rendezvous1 message.
#[error("Could not send RENDEZVOUS1 message")]
SendRendezvous(#[source] tor_proto::Error),
/// The client sent us a rendezvous point with an impossible set of identities.
///
/// (For example, it gave us `(Ed1, Rsa1)`, but in the network directory `Ed1` is
/// associated with `Rsa2`.)
#[error("Impossible combination of identities for rendezvous point")]
ImpossibleIds(#[source] tor_netdir::RelayLookupError),
/// An internal error occurred.
#[error("Internal error")]
Bug(#[from] tor_error::Bug),
impl HasKind for EstablishSessionError {
use EstablishSessionError as E;
E::NetdirUnavailable(e) => e.kind(),
E::UnsupportedOnionKey => EK::RemoteProtocolViolation,
EstablishSessionError::RendCirc(e) => {
tor_circmgr::Error::summarized_error_kind(e.sources())
EstablishSessionError::VirtualHop(e) => e.kind(),
EstablishSessionError::AcceptBegins(e) => e.kind(),
EstablishSessionError::SendRendezvous(e) => e.kind(),
EstablishSessionError::ImpossibleIds(_) => EK::RemoteProtocolViolation,
EstablishSessionError::Bug(e) => e.kind(),
/// A decrypted request from an onion service client which we can
/// choose to answer (or not).
/// This corresponds to a processed INTRODUCE2 message.
/// To accept this request, call its
/// [`establish_session`](IntroRequest::establish_session) method.
/// To reject this request, simply drop it.
#[derive(educe::Educe)]
#[educe(Debug)]
pub(crate) struct IntroRequest {
/// The introduce2 message itself. We keep this in case we want to look at
/// the outer header.
req: Introduce2,
/// The key generator we'll use to derive our shared keys with the client when
/// creating a virtual hop.
#[educe(Debug(ignore))]
key_gen: HsNtorHkdfKeyGenerator,
/// The RENDEZVOUS1 message we'll send to the rendezvous point.
/// (The rendezvous point will in turn send this to the client as a RENDEZVOUS2.)
rend1_msg: Rendezvous1,
/// The decrypted and parsed body of the introduce2 message.
intro_payload: IntroduceHandshakePayload,
/// The (in progress) ChanTarget that we'll use to build a circuit target
/// for connecting to the rendezvous point.
chan_target: OwnedChanTargetBuilder,
/// An open session with a single client.
/// (We consume this type and take ownership of its members later in
/// [`RendRequest::accept()`](crate::req::RendRequest::accept).)
pub(crate) struct OpenSession {
/// A stream of incoming BEGIN requests.
pub(crate) stream_requests: BoxStream<'static, IncomingStream>,
/// Our circuit with the client in question.
/// See `RendRequest::accept()` for more information on the life cycle of
/// this circuit.
pub(crate) circuit: Arc<ClientCirc>,
/// Dyn-safe trait to represent a `HsCircPool`.
/// We need this so that we can hold an `Arc<HsCircPool<R>>` in
/// `RendRequestContext` without needing to parameterize on R.
#[async_trait]
pub(crate) trait RendCircConnector: Send + Sync {
async fn get_or_launch_specific(
&self,
netdir: &tor_netdir::NetDir,
kind: HsCircKind,
target: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
) -> tor_circmgr::Result<Arc<ClientCirc>>;
impl<R: Runtime> RendCircConnector for HsCircPool<R> {
) -> tor_circmgr::Result<Arc<ClientCirc>> {
HsCircPool::get_or_launch_specific(self, netdir, kind, target).await
/// Filter callback used to enforce early requirements on streams.
#[derive(Clone, Debug)]
pub(crate) struct RequestFilter {
/// Largest number of streams we will accept on a circuit at a time.
//
// TODO: Conceivably, this should instead be a
// watch::Receiver<Arc<OnionServiceConfig>>, so we can re-check the latest
// value of the setting every time. Instead, we currently only copy this
// setting when an intro request is accepted.
pub(crate) max_concurrent_streams: usize,
impl IncomingStreamRequestFilter for RequestFilter {
fn disposition(
&mut self,
_ctx: &tor_proto::stream::IncomingStreamRequestContext<'_>,
circ: &tor_proto::circuit::ClientCircSyncView<'_>,
) -> tor_proto::Result<tor_proto::stream::IncomingStreamRequestDisposition> {
if circ.n_open_streams() >= self.max_concurrent_streams {
// TODO: We may want to have a way to send back an END message as
// well and not tear down the circuit.
Ok(tor_proto::stream::IncomingStreamRequestDisposition::CloseCircuit)
} else {
Ok(tor_proto::stream::IncomingStreamRequestDisposition::Accept)
impl IntroRequest {
/// Try to decrypt an incoming Introduce2 request, using the set of keys provided.
pub(crate) fn decrypt_from_introduce2(
context: &RendRequestContext,
) -> Result<Self, IntroRequestError> {
let mut rng = rand::rng();
// We need the subcredential for the *current time period* in order to do the hs_ntor
// handshake. But that can change over time. We will instead use KeyMgr::get_matching to
// find all current subcredentials.
let subcredentials = context
.compute_subcredentials()
.map_err(IntroRequestError::Subcredentials)?;
let (key_gen, rend1_body, msg_body) = hs_ntor::server_receive_intro(
&mut rng,
&context.kp_hss_ntor,
&context.kp_hs_ipt_sid,
&subcredentials[..],
req.encoded_header(),
req.encrypted_body(),
)
.map_err(E::InvalidHandshake)?;
let intro_payload: IntroduceHandshakePayload = {
let mut r = tor_bytes::Reader::from_slice(&msg_body);
r.extract().map_err(E::InvalidPayload)?
// Note: we _do not_ call `should_be_exhausted` here, since we
// explicitly expect the payload of an introduce2 message to be
// padded to hide its size.
// We build the OwnedChanTargetBuilder now, so that we can detect any
// problems here earlier.
let chan_target = OwnedChanTargetBuilder::from_encoded_linkspecs(
Strictness::Standard,
intro_payload.link_specifiers(),
.map_err(E::InvalidLinkSpecs)?;
let rend1_msg = Rendezvous1::new(*intro_payload.cookie(), rend1_body);
Ok(IntroRequest {
req,
key_gen,
rend1_msg,
intro_payload,
chan_target,
})
/// Try to accept this client's request.
/// To do so, we open a circuit to the client's chosen rendezvous point,
/// send it a RENDEZVOUS1 message, and wait for incoming BEGIN messages from
/// the client.
pub(crate) async fn establish_session(
filter: RequestFilter,
hs_pool: Arc<dyn RendCircConnector>,
provider: Arc<dyn NetDirProvider>,
) -> Result<OpenSession, EstablishSessionError> {
// Find a netdir. Note that we _won't_ try to wait or retry if the
// netdir isn't there: we probably can't answer this user's request.
let netdir = provider
.netdir(tor_netdir::Timeliness::Timely)
.map_err(E::NetdirUnavailable)?;
// Try to construct a CircTarget for rendezvous point based on the
// intro_payload.
let rend_point = {
// TODO: We might have checked for a recognized onion key type earlier.
let ntor_onion_key = match self.intro_payload.onion_key() {
OnionKey::NtorOnionKey(ntor_key) => ntor_key,
_ => return Err(E::UnsupportedOnionKey),
let mut bld = OwnedCircTarget::builder();
*bld.chan_target() = self.chan_target;
// TODO (#1223): This block is very similar to circtarget_from_pieces in
// relay_info.rs.
// Is there a clean way to refactor this?
let protocols = {
let chan_target = bld.chan_target().build().map_err(into_internal!(
"from_encoded_linkspecs gave an invalid output"
))?;
match netdir
.by_ids_detailed(&chan_target)
.map_err(E::ImpossibleIds)?
{
Some(relay) => relay.protovers().clone(),
None => netdir.relay_protocol_status().required_protocols().clone(),
bld.protocols(protocols);
bld.ntor_onion_key(*ntor_onion_key);
VerbatimLinkSpecCircTarget::new(
bld.build()
.map_err(into_internal!("Failed to construct a valid circtarget"))?,
self.intro_payload.link_specifiers().into(),
let max_n_attempts = netdir.params().hs_service_rendezvous_failures_max;
let mut circuit = None;
let mut retry_err: RetryError<tor_circmgr::Error> =
RetryError::in_attempt_to("Establish a circuit to a rendezvous point");
// Open circuit to rendezvous point.
for _attempt in 1..=max_n_attempts.into() {
match hs_pool
.get_or_launch_specific(&netdir, HsCircKind::SvcRend, rend_point.clone())
.await
Ok(circ) => {
circuit = Some(circ);
break;
Err(e) => {
retry_err.push(e);
// Note that we do not sleep on errors: if there is any
// error that will be solved by waiting, it would probably
// require waiting too long to satisfy the client.
let circuit = circuit.ok_or_else(|| E::RendCirc(retry_err))?;
// We'll need parameters to extend the virtual hop.
let params = onion_circparams_from_netparams(netdir.params())
.map_err(into_internal!("Unable to build CircParameters"))?;
// We won't need the netdir any longer; stop holding the reference.
drop(netdir);
let last_real_hop = circuit
.last_hop_num()
.map_err(into_internal!("Circuit with no final hop"))?;
// Add a virtual hop.
circuit
.extend_virtual(
handshake::RelayProtocol::HsV3,
handshake::HandshakeRole::Responder,
self.key_gen,
params,
.map_err(E::VirtualHop)?;
let virtual_hop = circuit
.map_err(into_internal!("Circuit with no virtual hop"))?;
// Accept begins from that virtual hop
let stream_requests = circuit
.allow_stream_requests(&[tor_cell::relaycell::RelayCmd::BEGIN], virtual_hop, filter)
.map_err(E::AcceptBegins)?
.boxed();
// Send the RENDEZVOUS1 message.
.send_raw_msg(self.rend1_msg.into(), last_real_hop)
.map_err(E::SendRendezvous)?;
Ok(OpenSession {
stream_requests,
circuit,
/// Get the [`IntroduceHandshakePayload`] associated with this [`IntroRequest`].
pub(crate) fn intro_payload(&self) -> &IntroduceHandshakePayload {
&self.intro_payload