diff --git a/Cargo.lock b/Cargo.lock index 8a2877af7..8a79a0b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4913,10 +4913,13 @@ name = "tor-hsservice" version = "0.2.3" dependencies = [ "async-trait", + "futures", "rand_core 0.6.4", "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 ae2c6e7c4..d5822b7c9 100644 --- a/crates/tor-hsservice/Cargo.toml +++ b/crates/tor-hsservice/Cargo.toml @@ -26,10 +26,13 @@ full = [ [dependencies] async-trait = "0.1.54" +futures = "0.3.14" rand_core = "0.6.2" thiserror = "1" tor-circmgr = { version = "0.10.0", 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.3", path = "../tor-netdir" } tor-proto = { version = "0.12.0", 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..2df68533b --- /dev/null +++ b/crates/tor-hsservice/src/req.rs @@ -0,0 +1,141 @@ +//! 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 futures::{channel::mpsc, stream::Stream}; +use std::net::SocketAddr; + +use tor_linkspec::OwnedChanTarget; +use tor_llcrypto::pk::curve25519; +use tor_proto::stream::DataStream; + +use crate::Result; + +/// 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, +} + +impl RendRequest { + /// Mark this request as accepted, and try to connect to the client's + /// provided rendezvous point. + /// + /// TODO HSS: Should this really be async? It might be nicer if it weren't. + pub async fn accept(self) -> Result> { + let r: Result>; + todo!(); + #[allow(unreachable_code)] + r + } + /// Reject this request. (The client will receive no notification.) + /// + /// TODO HSS: Should this really be async? It might be nicer if it weren't. + pub async fn reject(self) -> Result<()> { + todo!() + } + // + // TODO HSS: also add various accessors +} + +impl StreamRequest { + /// Accept this request and send the client a `CONNECTED` message. + pub async fn accept(self) -> Result { + todo!() + } + /// Reject this request, and send the client an `END` message. + pub async fn reject(self) -> Result<()> { + todo!() + } + /// Reject this request and close the rendezvous circuit entirely, + /// along with all other streams attached to the circuit. + pub fn shutdown_circuit(self) -> Result<()> { + todo!() + } + // TODO HSS various accessors, including for circuit. +} 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..337faf010 100644 --- a/crates/tor-hsservice/src/svc.rs +++ b/crates/tor-hsservice/src/svc.rs @@ -1,49 +1,134 @@ -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; use crate::{OnionServiceStatus, Result}; +mod ipt_establish; +mod publish; + /// 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 { @@ -72,6 +157,8 @@ impl OnionService { // changes? Or use a publish-based watcher? /// Tell this onion service to begin running. + // + // TODO HSS: Probably return an `impl Stream`. pub fn launch(&self) -> Result<()> { todo!() // TODO hss diff --git a/crates/tor-hsservice/src/svc/ipt_establish.rs b/crates/tor-hsservice/src/svc/ipt_establish.rs new file mode 100644 index 000000000..db7af27ca --- /dev/null +++ b/crates/tor-hsservice/src/svc/ipt_establish.rs @@ -0,0 +1,114 @@ +//! IPT Establisher +//! +//! Responsible for maintaining and establishing one introduction point. +//! +//! TODO HSS: move docs from `hssvc-ipt-algorithm.md` + +#![allow(clippy::needless_pass_by_value)] // TODO HSS remove + +use std::sync::Arc; + +use futures::channel::mpsc; +use tor_circmgr::hspool::HsCircPool; +use tor_netdir::{NetDirProvider, Relay}; +use tor_rtcompat::Runtime; + +use crate::RendRequest; + +/// Handle onto the task which is establishing and maintaining one IPT +pub(crate) struct IptEstablisher {} + +/// When the `IptEstablisher` is dropped it is torn down +/// +/// Synchronously +/// +/// * No rendezvous requests will be accepted +/// that arrived after `Drop::drop` returns. +/// +/// Asynchronously +/// +/// * Circuits constructed for this IPT are torn down +/// * The `rend_reqs` sink is closed (dropped) +/// * `IptStatusStatus::Faulty` will be indicated +impl Drop for IptEstablisher { + fn drop(&mut self) { + todo!() + } +} + +/// An error from trying to create in introduction point establisher. +/// +/// TODO HSS: This is probably too narrow a definition; do something else +/// instead. +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum IptError {} + +impl IptEstablisher { + /// Try to set up, and maintain, an IPT at `Relay` + /// + /// Rendezvous requests will be rejected + pub(crate) fn new( + circ_pool: Arc>, + dirprovider: Arc, + 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, + // TODO HSS: this needs to take some configuration + ) -> Result { + todo!() + } + + /// Begin accepting connections from this introduction point. + // + // TODO HSS: Perhaps we want to provide rend_reqs as part of the + // new() API instead. If we do, we must make sure there's a way to + // turn requests on and off, so that we can say "now we have advertised this + // so requests are okay." + pub(crate) fn start_accepting(&self, rend_reqs: mpsc::Sender) { + todo!() + } +} + +/// The current status of an introduction point, as defined in +/// `hssvc-ipt-algorithms.md`. +/// +/// TODO HSS Make that file unneeded. +#[derive(Clone, Debug)] +pub(crate) enum IptStatusStatus { + /// We are (re)establishing our connection to the IPT + /// + /// But we don't think there's anything wrong with it. + Establishing, + + /// The IPT is established and ready to accept rendezvous requests + Good, + + /// We don't have the IPT and it looks like it was the IPT's fault + Faulty, +} + +/// `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)] +pub(crate) struct IptWantsToRetire; + +/// The current status of an introduction point. +#[derive(Clone, Debug)] +pub(crate) struct IptStatus { + /// The current state of this introduction point as defined by + /// `hssvc-ipt-algorithms.md`. + /// + /// TODO HSS Make that file unneeded. + pub(crate) status: IptStatusStatus, + + /// 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>, +} diff --git a/crates/tor-hsservice/src/svc/publish.rs b/crates/tor-hsservice/src/svc/publish.rs new file mode 100644 index 000000000..624f575a7 --- /dev/null +++ b/crates/tor-hsservice/src/svc/publish.rs @@ -0,0 +1,98 @@ +//! Publish and maintain onion service descriptors + +#![allow(clippy::needless_pass_by_value)] // TODO HSS REMOVE. + +use std::sync::Arc; + +use tor_netdir::NetDirProvider; +use tor_rtcompat::Runtime; + +/// A handle for the Hsdir Publisher for an onion service. +/// +/// This handle represents a set of tasks that identify the hsdirs for each +/// releavant time period, construct descriptors, publish them, and keep them +/// up-to-date. +pub(crate) struct Publisher { + // TODO HSS: Write the contents here. + // + // I'm assuming that each Publisher knows its current keys, keeps track of + // the current relevant time periods, and knows the current + // status for uploading to each HsDir. + // + // Some of these contents may actually wind up belonging to a reactor + // task. + // + /// A source for new network directories that we use to determine + /// our HsDirs. + dir_provider: Arc, +} + +/// An error from creating or talkign with a Publisher. +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum PublisherError {} + +impl Publisher { + /// Create and launch a new publisher. + /// + /// When it launches, it will know no keys or introduction points, + /// and will therefore not upload any descriptors. + /// + #[allow(clippy::unnecessary_wraps)] // TODO HSS REMOVE + pub(crate) fn new( + runtime: R, + dir_provider: Arc, + ) -> Result { + // TODO: Do we really want to launch now, or later? + Ok(Self { dir_provider }) + } + + /// Inform this publisher that its set of keys has changed. + /// + /// TODO HSS: Either this needs to take new keys as an argument, or there + /// needs to be a source of keys (including public keys) in Publisher. + pub(crate) fn new_hs_keys(&self, keys: ()) { + todo!() + } + + /// Inform this publisher that the set of introduction points has changed. + /// + /// TODO HSS: Either this needs to take new intropoints as an argument, + /// or there needs to be a source of intro points in the Publisher. + pub(crate) fn new_intro_points(&self, ipts: ()) { + todo!() + } + + /// Return our current status. + // + // TODO HSS: There should also be a postage::Watcher -based stream of status + // change events. + pub(crate) fn status(&self) -> PublisherStatus { + todo!() + } + + // TODO HSS: We may also need to update descriptors based on configuration + // or authentication changes. +} + +/// Current status of our attempts to publish an onion service descriptor. +#[derive(Debug, Clone)] +pub(crate) struct PublisherStatus { + // TODO HSS add fields +} + +// +// Our main loop has to look something like: + +// Whenever time period or keys or netdir changes: Check whether our list of +// HsDirs has changed. If it is, add and/or remove hsdirs as needed. + +// "when learning about new keys, new intro points, or new configurations, +// or whenever the time period changes: Mark descriptors dirty." + +// Whenever descriptors are dirty, we have enough info to generate +// descriptors, and we aren't upload-rate-limited: Generate new descriptors +// and mark descriptors clean. Mark all hsdirs as needing new versions of +// this descriptor. + +// While any hsdir does not have the latest version of its any descriptor: +// upload it. Retry with usual timeouts on failure."