Merge branch 'onion-api-highlevel' into 'main'

Onion-service APIs: circmgr, hsclient, hsservice

See merge request tpo/core/arti!972
This commit is contained in:
Nick Mathewson 2023-01-24 18:19:42 +00:00
commit 3b2848f904
18 changed files with 565 additions and 2 deletions

31
Cargo.lock generated
View File

@ -3673,6 +3673,7 @@ dependencies = [
"tor-config", "tor-config",
"tor-error", "tor-error",
"tor-guardmgr", "tor-guardmgr",
"tor-hscrypto",
"tor-linkspec", "tor-linkspec",
"tor-llcrypto", "tor-llcrypto",
"tor-netdir", "tor-netdir",
@ -3883,6 +3884,21 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tor-hsclient"
version = "0.1.0"
dependencies = [
"async-trait",
"rand_core 0.6.4",
"thiserror",
"tor-circmgr",
"tor-hscrypto",
"tor-llcrypto",
"tor-netdir",
"tor-proto",
"tor-rtcompat",
]
[[package]] [[package]]
name = "tor-hscrypto" name = "tor-hscrypto"
version = "0.1.0" version = "0.1.0"
@ -3892,6 +3908,21 @@ dependencies = [
"tor-llcrypto", "tor-llcrypto",
] ]
[[package]]
name = "tor-hsservice"
version = "0.1.0"
dependencies = [
"async-trait",
"rand_core 0.6.4",
"thiserror",
"tor-circmgr",
"tor-hscrypto",
"tor-llcrypto",
"tor-netdir",
"tor-proto",
"tor-rtcompat",
]
[[package]] [[package]]
name = "tor-linkspec" name = "tor-linkspec"
version = "0.6.0" version = "0.6.0"

View File

@ -38,6 +38,8 @@ members = [
"crates/tor-circmgr", "crates/tor-circmgr",
"crates/tor-dirclient", "crates/tor-dirclient",
"crates/tor-dirmgr", "crates/tor-dirmgr",
"crates/tor-hsclient",
"crates/tor-hsservice",
"crates/arti-client", "crates/arti-client",
"crates/arti-config", "crates/arti-config",
"crates/arti-hyper", "crates/arti-hyper",

View File

@ -21,8 +21,11 @@ specific-relay = []
# #
# These APIs are not covered by semantic versioning. Using this # These APIs are not covered by semantic versioning. Using this
# feature voids your "semver warrantee". # feature voids your "semver warrantee".
experimental = ["experimental-api"] experimental = ["experimental-api", "onion-client", "onion-service"]
experimental-api = [] experimental-api = []
onion-client = ["onion-common"]
onion-service = ["onion-common"]
onion-common = ["tor-hscrypto"]
[dependencies] [dependencies]
async-trait = "0.1.2" async-trait = "0.1.2"
@ -47,6 +50,7 @@ tor-chanmgr = { path = "../tor-chanmgr", version = "0.8.0" }
tor-config = { path = "../tor-config", version = "0.7.0" } tor-config = { path = "../tor-config", version = "0.7.0" }
tor-error = { path = "../tor-error", version = "0.4.0" } tor-error = { path = "../tor-error", version = "0.4.0" }
tor-guardmgr = { path = "../tor-guardmgr", version = "0.8.0" } tor-guardmgr = { path = "../tor-guardmgr", version = "0.8.0" }
tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true }
tor-linkspec = { path = "../tor-linkspec", version = "0.6.0" } tor-linkspec = { path = "../tor-linkspec", version = "0.6.0" }
tor-netdir = { path = "../tor-netdir", version = "0.7.0" } tor-netdir = { path = "../tor-netdir", version = "0.7.0" }
tor-netdoc = { path = "../tor-netdoc", version = "0.6.0" } tor-netdoc = { path = "../tor-netdoc", version = "0.6.0" }

View File

@ -60,6 +60,8 @@ mod err;
mod impls; mod impls;
pub mod isolation; pub mod isolation;
mod mgr; mod mgr;
#[cfg(feature = "onion-client")]
mod onion_connector;
pub mod path; pub mod path;
mod preemptive; mod preemptive;
mod timeouts; mod timeouts;
@ -67,6 +69,9 @@ mod usage;
pub use err::Error; pub use err::Error;
pub use isolation::IsolationToken; pub use isolation::IsolationToken;
#[cfg(feature = "onion-client")]
#[cfg_attr(docsrs, doc(cfg(feature = "onion-client")))]
pub use onion_connector::{OnionConnectError, OnionServiceConnector};
use tor_guardmgr::fallback::FallbackList; use tor_guardmgr::fallback::FallbackList;
pub use tor_guardmgr::{ClockSkewEvents, GuardMgrConfig, SkewEstimate}; pub use tor_guardmgr::{ClockSkewEvents, GuardMgrConfig, SkewEstimate};
pub use usage::{TargetPort, TargetPorts}; pub use usage::{TargetPort, TargetPorts};
@ -206,6 +211,20 @@ impl<R: Runtime> CircMgr<R> {
Ok(circmgr) Ok(circmgr)
} }
/// Install a given [`OnionServiceConnector`] object to be used when making
/// connections to an onion service.
///
/// (This cannot be done at construction time, since the
/// OnionServiceConnector will have to keep a reference to this `CircMgr`.)
#[cfg(feature = "onion-client")]
#[allow(unused_variables, clippy::missing_panics_doc)]
pub fn install_onion_service_connector(
&self,
connector: &Arc<dyn OnionServiceConnector>,
) -> Result<()> {
todo!() // TODO hs
}
/// Launch the periodic daemon tasks required by the manager to function properly. /// Launch the periodic daemon tasks required by the manager to function properly.
/// ///
/// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks. /// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks.
@ -410,7 +429,35 @@ impl<R: Runtime> CircMgr<R> {
self.mgr.get_or_launch(&usage, netdir).await.map(|(c, _)| c) self.mgr.get_or_launch(&usage, netdir).await.map(|(c, _)| c)
} }
/// Return a circuit to a specific relay, suitable for using for directory downloads. /// Try to connect to an onion service via this circuit manager.
///
/// If `using_keys` is provided, then we will use those keys, in addition to
/// any configured in our `OnionServiceConnector`, to connect to the
/// service.
///
/// If we already have an existing circuit with the appropriate isolation,
/// we will return that circuit regardless of the content of `using_keys`.
///
/// Requires that an `OnionServiceConnector` has been installed. If it
/// hasn't, then we return an error.
#[cfg(feature = "onion-client")]
#[allow(clippy::missing_panics_doc, unused_variables)]
pub async fn get_or_launch_onion_client(
&self,
service_id: tor_hscrypto::pk::OnionId,
using_keys: Option<tor_hscrypto::pk::ClientSecretKeys>,
isolation: StreamIsolation,
) -> Result<ClientCirc> {
todo!() // TODO hs
// The implementation should look up whether we have an appropriate
// connected rendezvous circuit built or in progress in our CircMgr. If
// we do, we should return it or wait for it. Otherwise we should
// delegate to our OnionServiceConnector to build it.
}
/// Return a circuit to a specific relay, suitable for using for direct
/// (one-hop) directory downloads.
/// ///
/// This could be used, for example, to download a descriptor for a bridge. /// This could be used, for example, to download a descriptor for a bridge.
#[cfg_attr(docsrs, doc(cfg(feature = "specific-relay")))] #[cfg_attr(docsrs, doc(cfg(feature = "specific-relay")))]
@ -427,6 +474,28 @@ impl<R: Runtime> CircMgr<R> {
.map(|(c, _)| c) .map(|(c, _)| c)
} }
/// Create and return a new (typically anonymous) circuit whose last hop is
/// `target`.
///
/// This circuit is guaranteed not to have been used for any traffic
/// previously, and it will not be given out for any other requests in the
/// future unless explicitly re-registered with a circuit manager.
///
/// Used to implement onion service clients and services.
#[cfg(feature = "onion-common")]
#[allow(unused_variables, clippy::missing_panics_doc)]
pub async fn launch_specific_isolated(
&self,
target: tor_linkspec::OwnedCircTarget,
// TODO hs: this should at least be an enum to define what kind of
// circuit we want, in case we have different rules for different types.
// It might also need to include a "anonymous?" flag for supporting
// single onion services.
preferences: (),
) -> Result<ClientCirc> {
todo!() // TODO hs implement.
}
/// Launch circuits preemptively, using the preemptive circuit predictor's /// Launch circuits preemptively, using the preemptive circuit predictor's
/// predictions. /// predictions.
/// ///

View File

@ -0,0 +1,28 @@
//! Declare the `OnionServiceConnector` trait.
use async_trait::async_trait;
use thiserror::Error;
use tor_proto::circuit::ClientCirc;
/// A trait representing the ability to make a connection to an onion service.
///
/// This is defined in `tor-circmgr`, since `tor-circmgr` uses an instance of
/// this object to connect to onion services.
#[async_trait]
pub trait OnionServiceConnector {
/// Try to launch a connection to a given onion service.
async fn create_connection(
&self,
service_id: tor_hscrypto::pk::OnionId,
using_keys: Option<tor_hscrypto::pk::ClientSecretKeys>,
// TODO hs: If we want to support cache isolation, we may need to pass
// an additional argument here.
) -> Result<ClientCirc, OnionConnectError>;
}
/// An error returned when constructing an onion service.
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum OnionConnectError {
// TODO hs add variants.
}

View File

@ -0,0 +1,30 @@
[package]
name = "tor-hsclient"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
edition = "2021"
rust-version = "1.60"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Arti's implementation of an onion service client"
keywords = ["tor", "arti", "cryptography"]
categories = ["cryptography"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
publish = false
[features]
default = []
[dependencies]
async-trait = "0.1.2"
rand_core = "0.6.2"
thiserror = "1"
tor-circmgr = { version = "0.7.0", path = "../tor-circmgr", features = ["onion-client"] }
tor-hscrypto = { version = "0.1.0", path = "../tor-hscrypto" }
tor-llcrypto = { version = "0.4.0", path = "../tor-llcrypto" }
tor-netdir = { version = "0.7.0", path = "../tor-netdir" }
tor-proto = { version = "0.8.0", path = "../tor-proto" }
tor-rtcompat = { version = "0.8.0", path = "../tor-rtcompat" }
[dev-dependencies]

View File

@ -0,0 +1,17 @@
# tor-hsclient
Core implementation for onion services client.
## EXPERIMENTAL DRAFT
This crate is a work in progress; it is not the least bit complete.
Right now, it does not even work: it's only here so that we can prototype
our APIs.
## ARCHITECTURAL NOTE
This crate creates circuits to onion circuits, but does not remember them: it is
the circmgr's job to remember circuits. The tor-circmgr crate uses this module
indirectly, via a trait that it defines.

View File

@ -0,0 +1,11 @@
//! Manage a set of private keys for a client to authenticate to one or more
//! onion services.
use std::{collections::HashMap, sync::Mutex};
use tor_hscrypto::pk::{ClientSecretKeys, OnionId};
pub(crate) struct Keys {
/// The
keys: Mutex<HashMap<OnionId, ClientSecretKeys>>,
}

View File

@ -0,0 +1,80 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
// TODO hs: Add complete suite of warnings here.
#![allow(dead_code, unused_variables)] // TODO hs remove.
mod keys;
mod state;
use async_trait::async_trait;
use std::sync::Arc;
use tor_hscrypto::pk::{ClientSecretKeys, OnionId};
use tor_proto::circuit::ClientCirc;
use tor_circmgr::{CircMgr, OnionConnectError, OnionServiceConnector};
use tor_netdir::NetDirProvider;
use tor_rtcompat::Runtime;
/// An object that negotiates connections with onion services
pub struct HsClientConnector<R: Runtime> {
/// A [`CircMgr`] that we use to build circuits to HsDirs, introduction
/// points, and rendezvous points.
//
// TODO hs: currently this is a circular set of Arc, since the CircMgr will
// have to hold an Arc<OnionServiceConnector>. We should make one Weak.
//
// TODO hs: 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>>,
/// A [`NetDirProvider`] that we use to pick rendezvous points.
//
// TODO hs: Should this be weak too?
netdir_provider: Arc<dyn NetDirProvider>,
/// Information we are remembering about different onion services.
//
// TODO hs: if we implement cache isolation or state isolation, we might
// need multiple instances of this.
state: state::StateMap,
/// A collection of private keys to be used with various onion services.
//
// TODO hs: we might even want multiple instances of this, depending on how
// we decide to do isolation.
keys: keys::Keys,
}
impl<R: Runtime> HsClientConnector<R> {
// TODO hs: Need a way to manage the set of keys.
// TODO hs: need a constructor here.
// TODO hs: need a function to clear our StateMap, or to create a new
// isolated StateMap.
//
// TODO hs: Also, we need to expose that function from `TorClient`, possibly
// in the existing isolation API, possibly in something new.
}
#[async_trait]
impl<R: Runtime> OnionServiceConnector for HsClientConnector<R> {
async fn create_connection(
&self,
service_id: OnionId,
using_keys: Option<ClientSecretKeys>,
) -> Result<ClientCirc, OnionConnectError> {
todo!() // TODO hs
// This function must do the following, retrying as appropriate.
// - Look up the onion descriptor in the state.
// - Download the onion descriptor if one isn't there.
// - In parallel:
// - Pick a rendezvous point from the netdirprovider and launch a
// rendezvous circuit to it. Then send ESTABLISH_INTRO.
// - Pick a number of introduction points (1 or more) and try to
// launch circuits to them.
// - On a circuit to an introduction point, send an INTRODUCE1 cell.
// - Wait for a RENDEZVOUS2 cell on the rendezvous circuit
// - Add a virtual hop to the rendezvous circuit.
// - Return the rendezvous circuit.
}
}

View File

@ -0,0 +1,39 @@
//! Implement a cache for onion descriptors and the facility to remember a bit
//! about onion service history.
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::SystemTime;
use tor_hscrypto::pk::BlindedOnionId;
/// Information about onion services and our history of connecting to them.
pub(crate) struct StateMap {
/// A map from blinded onion identity to information about an onion service.
///
/// If the map is to `None`, then a download is in progress for that state's
/// descriptor.
members: Mutex<HashMap<BlindedOnionId, Option<State>>>,
}
/// Information about our history of connecting to an onion service.
//
// TODO hs: We might need this to be an enum, if we want to represent "fetch
// pending" as something with a RetryDelay. We might even want a RetryDelay
// associated with each HsDir for the service as well!
pub(crate) struct State {
/// A time when we should check whether this descriptor is still the latest.
desc_fresh_until: SystemTime,
/// A time when we should expire this entry completely.
expires: SystemTime,
/// The latest known onion service descriptor for this service.
desc: (), // TODO hs: use actual onion service descriptor type.
/// Information about the latest status of trying to connect to this service
/// through each of its introduction points.
///
ipts: (), // TODO hs: make this type real, use `RetryDelay`, etc.
}
impl StateMap {
// TODO hs: we need a way to make the entries here expire over time.
}

View File

@ -127,7 +127,16 @@ pub struct ClientDescAuthKey(curve25519::PublicKey);
// The names should be something like these: // The names should be something like these:
pub struct OnionIdSecretKey(ed25519::SecretKey); pub struct OnionIdSecretKey(ed25519::SecretKey);
pub struct ClientDescAuthSecretKey(curve25519::StaticSecret); pub struct ClientDescAuthSecretKey(curve25519::StaticSecret);
pub struct ClientIntroAuthSecretKey(ed25519::SecretKey);
// ... and so on. // ... and so on.
// //
// NOTE: We'll have to use ExpandedSecretKey as the secret key // NOTE: We'll have to use ExpandedSecretKey as the secret key
// for BlindedOnionIdSecretKey. // for BlindedOnionIdSecretKey.
/// A set of keys to tell the client to use when connecting to an onion service.
//
// TODO hs
pub struct ClientSecretKeys {
desc_auth: Option<ClientDescAuthSecretKey>,
intro_auth: Option<ClientIntroAuthSecretKey>,
}

View File

@ -0,0 +1,30 @@
[package]
name = "tor-hsservice"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
edition = "2021"
rust-version = "1.60"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Arti's implementation of an onion service provider"
keywords = ["tor", "arti", "cryptography"]
categories = ["cryptography"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
publish = false
[features]
default = []
[dependencies]
async-trait = "0.1.2"
rand_core = "0.6.2"
thiserror = "1"
tor-circmgr = { version = "0.7.0", path = "../tor-circmgr", features = ["onion-client"] }
tor-hscrypto = { version = "0.1.0", path = "../tor-hscrypto" }
tor-llcrypto = { version = "0.4.0", path = "../tor-llcrypto" }
tor-netdir = { version = "0.7.0", path = "../tor-netdir" }
tor-proto = { version = "0.8.0", path = "../tor-proto" }
tor-rtcompat = { version = "0.8.0", path = "../tor-rtcompat" }
[dev-dependencies]

View File

@ -0,0 +1,11 @@
# tor-hsclient
Core implementation for onion services in arti.
## EXPERIMENTAL DRAFT
This crate is a work in progress; it is not the least bit complete.
Right now, it does not even work: it's only here so that we can prototype
our APIs.

View File

@ -0,0 +1,8 @@
//! Declare an error type for the `tor-hsservice` crate.
use thiserror::Error;
/// An error affecting the operation of an onion service.
#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum Error {}

View File

@ -0,0 +1,45 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
// TODO hs: Add complete suite of warnings here.
#![allow(dead_code, unused_variables)] // TODO hs remove.
mod err;
mod status;
mod streamproxy;
mod svc;
use async_trait::async_trait;
pub use err::Error;
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.
//
// TODO hs: 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 hs: 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 hs: 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 hs: 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?
}

View File

@ -0,0 +1,4 @@
pub struct OnionServiceStatus {
// TODO hs Should say how many intro points are active, how many descriptors
// are updated, whether we're "healthy", etc.
}

View File

@ -0,0 +1,35 @@
//! 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 hs 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?)
}
#[async_trait]
impl StreamHandler for StreamProxy {
async fn handle_request(&self, circinfo: &(), stream: ()) {
todo!() // TODO hs: 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

@ -0,0 +1,110 @@
use std::sync::Arc;
use tor_circmgr::CircMgr;
use tor_netdir::NetDirProvider;
use tor_rtcompat::Runtime;
use crate::{OnionServiceStatus, Result};
/// A handle to an instance of an onion service.
//
// TODO hs: 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.
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.
///
/// Authorized client public keys might be here, or they might be in a
/// separate structure.
config: (),
/// 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,
/// HsDirs, and rendezvous points.
// TODO hs: 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 hs: This will need heavy refactoring.
///
/// TODO hs: 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: (),
/// Object that handles incoming streams from the client.
stream_handler: Arc<dyn crate::StreamHandler>,
}
impl<R: Runtime> OnionService<R> {
/// Create (but do not launch) a new onion service.
pub fn new(config: (), netdir_provider: (), circmgr: ()) -> Self {
todo!(); // TODO hs
}
/// Change the configuration of this onion service.
///
/// (Not everything can be changed here. At the very least we'll need to say
/// that the identity of a service is fixed. We might want to make the
/// storage backing this, and the anonymity status, unchangeable.)
pub fn reconfigure(&self, new_config: ()) -> Result<()> {
todo!() // TODO hs
}
/// Tell this onion service about some new short-term keys it can use.
pub fn add_keys(&self, keys: ()) -> Result<()> {
todo!() // TODO hs
}
/// Return the current status of this onion service.
pub fn status(&self) -> OnionServiceStatus {
todo!() // TODO hs
}
// TODO hs let's also have a function that gives you a stream of Status
// changes? Or use a publish-based watcher?
/// Tell this onion service to begin running.
pub fn launch(&self) -> Result<()> {
todo!() // TODO hs
// This needs to launch at least the following tasks:
//
// - If we decide to use separate disk-based key provisioning, a task to
// monitor our keys directory.
// - If we own our identity key, a task to generate per-period sub-keys as
// needed.
// - A task to make sure that we have enough introduction point circuits
// at all times, and launch new ones as needed.
// - A task to see whether we have an up-to-date descriptor uploaded for
// each supported time period to every HsDir listed for us in the
// current directory, and if not, regenerate and upload our descriptor
// as needed.
// - A task to receive introduction requests from our introduction
// points, decide whether to answer them, and if so launch a new
// rendezvous task to:
// - finish the cryptographic handshake
// - build a circuit to the rendezvous point
// - Send the RENDEZVOUS1 reply
// - Add a virtual hop to the rendezvous circuit
// - Launch a new task to handle BEGIN requests on the rendezvous
// circuit, using our StreamHandler.
}
/// Tell this onion service to stop running.
///
/// It can be restarted with launch().
///
/// You can also shut down an onion service completely by dropping the last
/// Clone of it.
pub fn stop(&self) -> Result<()> {
todo!() // TODO hs
}
}