hsservice: Adapt data structures from onion-service-notes.md

Also, removed some older structures that don't make sense in the
current design.

Closes #970
This commit is contained in:
Nick Mathewson 2023-07-31 09:52:53 -04:00
parent 5906c2c92e
commit 20b3679dbb
8 changed files with 298 additions and 89 deletions

2
Cargo.lock generated
View File

@ -4920,6 +4920,8 @@ dependencies = [
"thiserror",
"tor-circmgr",
"tor-hscrypto",
"tor-keymgr",
"tor-linkspec",
"tor-llcrypto",
"tor-netdir",
"tor-proto",

View File

@ -30,6 +30,8 @@ rand_core = "0.6.2"
thiserror = "1"
tor-circmgr = { version = "0.9.1", path = "../tor-circmgr", features = ["hs-service"] }
tor-hscrypto = { version = "0.3.0", path = "../tor-hscrypto" }
tor-keymgr = { version = "0.1.0", path = "../tor-keymgr" }
tor-linkspec = { version = "0.8.1", path = "../tor-linkspec" }
tor-llcrypto = { version = "0.5.2", path = "../tor-llcrypto" }
tor-netdir = { version = "0.9.2", path = "../tor-netdir" }
tor-proto = { version = "0.11.1", path = "../tor-proto" }

View File

@ -0,0 +1,25 @@
//! Configuration information for onion services.
/// Configuration for a single onion service.
#[derive(Debug, Clone)]
pub struct OnionServiceConfig {
/// An arbitrary identifier or "nickname" used to look up this service's
/// keys, state, configuration, etc,
/// and distinguish them from other services. This is local-only.
//
// TODO HSS: It's possible that instead of having this be _part_ of the
// service's configuration, we want this to be the key for a map in
// which the service's configuration is stored. We'll see how the code
// evolves.
nickname: String,
/// Whether we want this to be a non-anonymous "single onion service".
/// We could skip this in v1. We should make sure that our state
/// is built to make it hard to accidentally set this.
anonymity: crate::Anonymity,
/// Number of intro points; defaults to 3; max 20.
num_intro_points: Option<u8>,
// TODO HSS: I'm not sure if client encryption belongs as a configuration
// item, or as a directory like C tor does it. Or both?
}

View File

@ -1,46 +1,74 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
// TODO hss: Add complete suite of warnings here.
// @@ begin lint list maintained by maint/add_warning @@
#![cfg_attr(not(ci_arti_stable), allow(renamed_and_removed_lints))]
#![cfg_attr(not(ci_arti_nightly), allow(unknown_lints))]
#![deny(missing_docs)]
#![warn(noop_method_call)]
#![deny(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
#![allow(dead_code, unused_variables)] // TODO hss remove.
mod config;
mod err;
mod keys;
mod req;
mod status;
mod streamproxy;
mod svc;
use async_trait::async_trait;
pub use config::OnionServiceConfig;
pub use err::Error;
pub use req::{OnionServiceDataStream, RendRequest, StreamRequest};
pub use status::OnionServiceStatus;
pub use svc::OnionService;
/// A Result type describing an onion service operation.
pub type Result<T> = std::result::Result<T, Error>;
/// An object that knows how to handle stream requests.
#[async_trait]
pub trait StreamHandler {
/// Handle an incoming stream request on a given onion service.
/// The level of anonymity that an onion service should try to run with.
#[derive(Debug, Default, Copy, Clone)]
#[non_exhaustive]
pub enum Anonymity {
/// Try to keep the location of the onion service private.
#[default]
Anonymous,
/// Do not try to keep the location of the onion service private.
///
/// (This is implemented using our "single onion service" design.)
//
// TODO hss: the `circ_info` argument should have data about the circuit on
// which the request arrived. If the client authenticated, it might tell us
// who they are. Or it might have information about how many requests
// (and/or failed requests) we've gotten on the circuit.
//
// TODO hss: The `circ_info` argument should at a minimum include the
// circuit; ideally in a form that we can get a weak reference to it, and
// use it in the key of a `PtrWeakKeyHashMap`. (Or we could stick the info
// in the circuit itself somehow, and access it as a Box<dyn Any>, but
// that's a bit sketchy type-wise.)
//
// TODO hss: the `stream` argument should be an IncomingStream from
// tor-proto, but that branch is not yet merged as of this writing.
async fn handle_request(&self, circ_info: &(), stream: ());
}
mod mgr {
// TODO hss: Do we want to have the notion of a collection of onion services,
// running in tandem? Or is that a higher-level crate, possibly a part of
// TorClient?
// TODO HSS: We may want to put this behind a feature?
DangerouslyNonAnonymous,
}

View File

@ -0,0 +1,100 @@
//! Request objects used to implement onion services.
//!
//! These requests are yielded on a stream, and the calling code needs to decide
//! whether to permit or reject them.
use std::net::SocketAddr;
use tor_linkspec::OwnedChanTarget;
use tor_llcrypto::pk::curve25519;
use tor_proto::stream::DataStream;
/// Request to complete an introduction/rendezvous handshake.
///
/// A request of this kind indicates that a client has asked permission to
/// connect to an onion service through an introduction point. The caller needs
/// to decide whether or not to complete the handshake.
///
/// Protocol details: More specifically, we create one of these whenever we get a well-formed
/// `INTRODUCE2` message. Based on this, the caller decides whether to send a
/// `RENDEZVOUS1` message.
#[derive(Debug)]
pub struct RendRequest {
/// Which introduction point gave us this request?
from_intro_point: crate::svc::IntroPointId,
/// What proof-of-work did the client send us?
proof_of_work_provided: Option<ProofOfWork>,
/// Information about the rendezvous point that the client wanted us to
/// connect to.
rend_pt: RendPt,
//
// TODO HSS: We'll also need additional information to actually complete the
// request, maybe including a Weak<OnionService>, or maybe including a
// oneshot::Sender.
}
/// Information needed to complete a rendezvous handshake.
#[derive(Debug, Clone)]
struct RendPt {
/// The location and identity of the rendezvous point.
location: OwnedChanTarget,
/// The public Ntor key for the rendezvous point.
ntor_key: curve25519::PublicKey,
/// Cryptographic state to use when completing the handshake.
///
/// TODO HSS: This is not at all final, and should be refactored freely.
handshake: HandshakeState,
}
/// The cryptographic state needed to complete an introduce/rendezvous
/// handshake.
#[derive(Debug, Clone)]
struct HandshakeState {
// TODO HSS: replace this type or its contents as needed.
}
/// Information about a proof of work received from a client's introduction
/// point.
///
// Todo: use Beth's API instead.
#[derive(Debug, Clone)]
enum ProofOfWork {
/// TODO HSS document or replace.
EquixV1 {
/// TODO HSS document or replace
effort_level: usize,
},
}
/// A request from a client to open a new stream to an onion service.
///
/// We can only receive these _after_ we have already permitted the client to
/// connect via a [`RendRequest`].
///
/// Protocol details: More specifically, we create one of these whenever we get a well-formed
/// `BEGIN` message. Based on this, the caller decides whether to send a
/// `CONNECTED` message.
#[derive(Debug)]
pub struct StreamRequest {
/// The object that will be used to send data to and from the client.
///
/// TODO HSS: Possibly instead this will be some type from tor_proto that
/// can turn into a DataStream.
stream: DataStream,
/// The address that the client has asked to connect to.
///
/// TODO HSS: This is the wrong type! It may be a hostname.
target: SocketAddr,
}
/// A stream opened over an onion service.
//
// TODO HSS: This may belong in another module.
#[derive(Debug)]
pub struct OnionServiceDataStream {
/// The underlying data stream; this type is just a thin wrapper.
inner: DataStream,
}

View File

@ -1,4 +1,10 @@
//! Support for reporting the status of an onion service.
/// The current reported status of an onion service.
#[derive(Debug, Clone)]
pub struct OnionServiceStatus {
// TODO hss Should say how many intro points are active, how many descriptors
// are updated, whether we're "healthy", etc.
/// An ignored field to suppress warnings. TODO HSS remove this.
_ignore: (),
}

View File

@ -1,36 +0,0 @@
//! Implement a StreamHandler that proxies connections to ports, typically on
//! localhost.
use std::{collections::HashMap, net::SocketAddr};
use async_trait::async_trait;
use crate::StreamHandler;
pub(crate) struct StreamProxy {
/// Map from virtual port on the onion service to an address we connect to
/// in order to implement that port.
ports: HashMap<u16, SocketAddr>,
}
impl StreamProxy {
// TODO hss need a new() function. It should reject non-localhost addresses
// by default, and have a way to override. (Alternatively, that should be
// done in the configuration code?)
}
#[allow(clippy::diverging_sub_expression)] // for todo!() + async_trait.
#[async_trait]
impl StreamHandler for StreamProxy {
async fn handle_request(&self, circinfo: &(), stream: ()) {
todo!() // TODO hss: implement
// - Look up the port for the incoming stream request.
// - If no port is found, reject the request, and possibly increment a
// counter in circinfo.
// - Otherwise, open a TCP connection to the target address.
// - On success, accept the stream, and launch tasks to relay traffic
// from the stream to the TCP connection.
// - On failure, reject the stream with an error.
}
}

View File

@ -1,6 +1,16 @@
use std::sync::Arc;
//! Principal types for onion services.
use tor_circmgr::CircMgr;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::Instant,
};
use tor_circmgr::hspool::HsCircPool;
use tor_hscrypto::pk::HsBlindIdKey;
use tor_keymgr::KeyMgr;
use tor_linkspec::RelayIds;
use tor_llcrypto::pk::curve25519;
use tor_netdir::NetDirProvider;
use tor_rtcompat::Runtime;
@ -8,42 +18,114 @@ use crate::{OnionServiceStatus, Result};
/// A handle to an instance of an onion service.
//
// TODO hss: We might want to wrap this in an Arc<Mutex<>>, and have an inner
// structure that contains these elements. Or we might want to refactor this in
// some other way.
// TODO HSS: Write more.
//
// (APIs should return Arc<OnionService>)
//
// NOTE: This might not need to be parameterized on Runtime; if we can avoid it
// without too much trouble, we should.
pub struct OnionService<R: Runtime> {
/// Needs some kind of configuration about: what is our identity (if we know
/// it), is this anonymous, do we store persistent info and if so where and
/// how, etc.
/// The mutable implementation details of this onion service.
inner: Mutex<SvcInner<R>>,
}
/// Implementation details for an onion service.
struct SvcInner<R: Runtime> {
/// Configuration information about this service.
///
/// Authorized client public keys might be here, or they might be in a
/// TODO HSS: Authorized client public keys might be here, or they might be in a
/// separate structure.
config: (),
config: crate::OnionServiceConfig,
/// A netdir provider to use in finding our directories and choosing our
/// introduction points.
netdir_provider: Arc<dyn NetDirProvider>,
/// A circuit manager to use in making circuits to our introduction points,
/// A keymgr used to look up our keys and store new medium-term keys.
keymgr: Arc<KeyMgr>,
/// A circuit pool to use in making circuits to our introduction points,
/// HsDirs, and rendezvous points.
//
// TODO hss: Maybe we can make a trait that only gives a minimal "build a
// circuit" API from CircMgr, so that we can have this be a dyn reference
// too?
circmgr: Arc<CircMgr<R>>,
/// Private keys in actual use for this onion service.
///
/// TODO hss: This will need heavy refactoring.
///
/// TODO hss: There's a separate blinded ID, certificate, and signing key
/// for each active time period.
keys: (),
/// Status for each active introduction point for this onion service.
intro_points: Vec<()>,
/// Status for our onion service descriptor
descriptor_status: (),
circmgr: Arc<HsCircPool<R>>,
/// Object that handles incoming streams from the client.
stream_handler: Arc<dyn crate::StreamHandler>,
/// Authentication information for descriptor encryption.
///
/// (Our protocol defines two kinds of client authentication: in the first
/// type, we encrypt the descriptor to client public keys. In the second,
/// we require authentictaion as part of the `INTRODUCE2` message. Only the
/// first type has ever been implemented.)
encryption_auth: Option<DescEncryptionAuth>,
/// Private keys in actual use for this onion service.
//
// TODO hss: This will need heavy refactoring.
//
// TODO hss: There's a separate blinded ID, certificate, and signing key
// for each active time period.
keys: (),
/// Status for each active introduction point for this onion service.
//
// TODO HSS: This might want to be a generational arena, and might want to be
// use a different map for each descriptor epoch. Feel free to refactor!
intro_points: Vec<IntroPointState>,
/// Status for our onion service descriptor
desc_status: DescUploadHistory,
}
/// Information about encryption-based authentication.
struct DescEncryptionAuth {
/// A list of the public keys for which we should encrypt our
/// descriptor.
//
// TODO HSS: maybe this should instead be a place to find the keys, so that
// we can reload them on change?
//
// TODO HSS: maybe this should instead be part of our configuration
keys: Vec<curve25519::PublicKey>,
}
/// Current history and status for our descriptor uploads.
///
// TODO HSS: Remember, there are *multiple simultaneous variants* of our
// descriptor. we will probably need to make this structure different.
struct DescUploadHistory {
/// When did we last rebuild our descriptors?
last_rebuilt: Instant,
/// Each current descriptor that we need to try to maintain and upload.
descriptors: HashMap<HsBlindIdKey, String>,
/// Status of uploading each descriptor to each HsDir.
//
// Note that is possible that multiple descriptors will need to be uploaded
// to the same HsDir. When this happens, we MUST use separate circuits to
// uplaod them.
target_status: HashMap<HsBlindIdKey, HashMap<RelayIds, RetryState>>,
}
/// State of uploading a single descriptor
struct RetryState {
// TODO HSS: implement this as needed.
}
/// State of a current introduction point.
struct IntroPointState {
// TODO HSS: use diziet's structures from `hssvc-ipt-algorithms.md` once those are more settled.
}
/// Identifier for a single introduction point of an onion point.
//
// TODO HSS maybe use a nicer type, like a generational arena index.
#[derive(Debug, Clone)]
pub(crate) struct IntroPointId(RelayIds);
impl<R: Runtime> OnionService<R> {
/// Create (but do not launch) a new onion service.
pub fn new(config: (), netdir_provider: (), circmgr: ()) -> Self {