From 20b3679dbbbf54db7f6e6c0e92949dbcef5a68f3 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Mon, 31 Jul 2023 09:52:53 -0400 Subject: [PATCH] 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 --- Cargo.lock | 2 + crates/tor-hsservice/Cargo.toml | 2 + crates/tor-hsservice/src/config.rs | 25 +++++ crates/tor-hsservice/src/lib.rs | 84 ++++++++++----- crates/tor-hsservice/src/req.rs | 100 ++++++++++++++++++ crates/tor-hsservice/src/status.rs | 6 ++ crates/tor-hsservice/src/streamproxy.rs | 36 ------- crates/tor-hsservice/src/svc.rs | 132 +++++++++++++++++++----- 8 files changed, 298 insertions(+), 89 deletions(-) create mode 100644 crates/tor-hsservice/src/config.rs create mode 100644 crates/tor-hsservice/src/req.rs delete mode 100644 crates/tor-hsservice/src/streamproxy.rs diff --git a/Cargo.lock b/Cargo.lock index ef768c0ca..747e500d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4920,6 +4920,8 @@ dependencies = [ "thiserror", "tor-circmgr", "tor-hscrypto", + "tor-keymgr", + "tor-linkspec", "tor-llcrypto", "tor-netdir", "tor-proto", diff --git a/crates/tor-hsservice/Cargo.toml b/crates/tor-hsservice/Cargo.toml index 76f45cbb6..75ceeebc0 100644 --- a/crates/tor-hsservice/Cargo.toml +++ b/crates/tor-hsservice/Cargo.toml @@ -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" } diff --git a/crates/tor-hsservice/src/config.rs b/crates/tor-hsservice/src/config.rs new file mode 100644 index 000000000..984ea728a --- /dev/null +++ b/crates/tor-hsservice/src/config.rs @@ -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, + // 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? +} diff --git a/crates/tor-hsservice/src/lib.rs b/crates/tor-hsservice/src/lib.rs index 5e39b8635..81421d933 100644 --- a/crates/tor-hsservice/src/lib.rs +++ b/crates/tor-hsservice/src/lib.rs @@ -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 +//! + #![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 = std::result::Result; -/// 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, 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, } diff --git a/crates/tor-hsservice/src/req.rs b/crates/tor-hsservice/src/req.rs new file mode 100644 index 000000000..8fe0a9513 --- /dev/null +++ b/crates/tor-hsservice/src/req.rs @@ -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, + + /// 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, 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, +} diff --git a/crates/tor-hsservice/src/status.rs b/crates/tor-hsservice/src/status.rs index 2324e0102..ea18ecb78 100644 --- a/crates/tor-hsservice/src/status.rs +++ b/crates/tor-hsservice/src/status.rs @@ -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: (), } diff --git a/crates/tor-hsservice/src/streamproxy.rs b/crates/tor-hsservice/src/streamproxy.rs deleted file mode 100644 index 1a88b697c..000000000 --- a/crates/tor-hsservice/src/streamproxy.rs +++ /dev/null @@ -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, -} - -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. - } -} diff --git a/crates/tor-hsservice/src/svc.rs b/crates/tor-hsservice/src/svc.rs index c942ab55f..8938f1cbc 100644 --- a/crates/tor-hsservice/src/svc.rs +++ b/crates/tor-hsservice/src/svc.rs @@ -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>, 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) +// +// NOTE: This might not need to be parameterized on Runtime; if we can avoid it +// without too much trouble, we should. pub struct OnionService { - /// 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>, +} + +/// Implementation details for an onion service. +struct SvcInner { + /// 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, - /// 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, + + /// 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>, - /// 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>, - /// Object that handles incoming streams from the client. - stream_handler: Arc, + /// 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, + + /// 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, + + /// 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, +} + +/// 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, + + /// 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>, +} + +/// 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 OnionService { /// Create (but do not launch) a new onion service. pub fn new(config: (), netdir_provider: (), circmgr: ()) -> Self {