Initial notes about onion service structs and APIs

This commit is contained in:
Nick Mathewson 2023-07-19 16:34:06 -04:00
parent 43481d1797
commit 9330c70ebe
1 changed files with 263 additions and 0 deletions

View File

@ -0,0 +1,263 @@
# A few notes about onion services
## A higher level vs a lower level.
As I'm working through all of the design elements below, I'm realizing
that we have the possibility for both a high-level API and a low-level
API. The low-level API wouldn't need to handle persistence or making
outbound connections; it would just provide `DataStreams` and let the
caller load and save things as needed. The higher level API would
behave more like C tor, providing a 'reverse proxy' and opening
stream connections to local applications.
## What we need to save on disk
Right now C tor stores this information:
`DIR/hostname`: The hostname of the onion service, with trailing
.onion. Tor writes this but does not read it.
`DIR/private_key`: The top level identity key (`KS_hs_id`) for the
onion service.
`DIR/client_keys`: A list of authorized clients; optional. Tor reads
this but does not write it.
`DIR/onion_service_non_anonymous`: present if this is a non-anonymous
service.
It's absolutely necessary to support making `KS_hs_id` persistent, or
we can't create the same onion service over time. The other stuff can
be read or written in other ways, though maintaining the current interface
_would_ allow compatibility with C installations.
## Notes about configuration
Right now C tor has these options. They're all prefixed with
`HiddenService` but let's just ignore that.
### High-level options.
Here we talk about the higher-level APIs. These are all APIs that we
would or could implemen _on top of_ the lower-level API I'm going to
be discussing below.
These are **must-have** **high-level** options. (They are must-have
_in some form_, and the form can be quite different.)
`Dir`: Location on disk to store info for this onion service.
`Port`: mapping from virtual port received in begin cell
to local port where we should send streams. AF_UNIX addresses are supported
Client authorization file: Stored on disk. Is a directory full of files
with the contents: `<auth-type>:<key-type>:<base32-encoded-public-key>`.
These are **nice-to-have** **high-level** options:
`ExportCircuitId`: Special protocol to use in exposing a global
circuit ID for whatever circuit originated each stream. Right now
`haproxy` is supported.
`DirGroupReadable`: Allow the directory to be group-readable.
`OnionBalanceInstance`: Exposes extra data used by `OnionBalance`.
(TODO: find out what this does?)
I suggest that we do not provide these **high-level** options:
`AllowUnknownPorts`: Do not close the circuit when we get a request
for a port we don't recognize. (This is 0 by default to avoid port-scanning)
### Low-level options
Here are the lower-level options. We'd need to port these to work with
our lower-level APIs.
These are **should-have** **low-level** options:
`NumIntroductionPoints`: How many introduction points to
try to have. (This turns out to be important for tuning, IIUC)
These are **nice-to-have** **low-level** options:
`SingleHopMode`, `NonAnonymousMode`: Makes services non-anonymous. Global.
I **do now know** how necessary this option is:
`PublishHidServDescriptor`: Do not publish any
descriptors. Global. Only useful if you're having something else
publish for you.
I **do not know** whether this is high-level or low-level, or how
necessary it is:
`MaxStreams`: Limit simultaneous connections on a rendezvous circuit.
`MaxStreamsCloseCircuit`: Whether to close the circuit if the number of
streams tries to exceed the limit.
I suggest that we do not build this** low-level** option:
`Version`: Always 3.
## Projected data structures and APIs
```
enum Anonymity {
Anonymous,
DangerouslyNonAnonymous,
}
struct ServiceConfig {
/// An arbitrary identifier used to look up this service's keys
/// and distinguish them from other keys. Perhaps we don't need this?
///
/// Alternatively, maybe each service gets its own keymgr instance
/// (or a some kind of a keymgr view), and uses that to look up keys
/// with a global name?
keyid: 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: Anonymity,
/// Number of intro points; defaults to 3; max 20.
num_intro_points: Option<u8>,
/// Not sure if client encryption belongs as a configuration item, or
/// as a directory like C tor does?
}
pub struct Service {
inner: Mutex<Inner>
}
struct ServiceInner {
/// Configuration of this service
config: ServiceConfig,
/// Used to look up our keys
keymgr: Arc<KeyMgr>,
/// Used to decide whom to encrypt descriptor to.
desc_encryption_auth: Option<DescEncryptionAuth>,
current_keys: {
// Possibly we cache keys here that we've loaded?
// Possibly we store keys here that don't go in the keymgr?
// we probably want to keep certs around.
},
/// Possibly a generational arena , so we can make IntroPointId
/// into a generational index?
intro_point_state: Vec<IntroPointState>,
desc_upload_history: DescUploadHistory,
}
struct DescUploadHistory {
/// We can have multiple simultaneous variants of our
/// descriptor of there are different time periods active now,
/// I think?
///
/// Possibly we should just have multiple instances of DescUploadHistory?
///
/// Possibly we should (eventually, not v1) try to decorrelate uploads
/// where the BlindIdKey is different
descriptors_last_rebuilt: Instant,
descriptors: HashMap<HsBlindIdKey, (String, HsDe)>,
/// Status with uploading the latest version of each descriptor to
/// each relevant hsdir.
upload_targets: HashMap<RelayIds, (HsBlindIdKey, RetryState)>,
}
struct DescEncryptionAuth {
Vec<x25519::PublicKey>,
}
struct IntroPointState {
/// TODO diziet is writing this, I think.
}
impl Service {
/// At some point you can launch a service and get a stream
/// of all the requests to rendezvous.
pub fn launch(...) -> Result<mpsc::Receiver<RendRequest>>;
}
pub enum IntroAuth {
/// Deliberately empty; nothing is implemented here.
}
// TODO: Use Beth's api insted.
enum ProofOfWork {
EquixV1 { effort_level: usize }
}
/// We create one of these whenever we get a well-formed INTRODUCE2
/// message, based on this, we the caller decides whether to send a
/// RENDEZVOUS2 message.
pub struct RendRequest {
from_intro_point: IntroPointId,
client_auth_provided: Option<ClientAuth>,
proof_of_work_provided: Option<ProofOfWork>,
rend_circuit: Arc<ClientCirc>,
info_needed_to_send_rendezvous2 : (
OwnedChanTarget, // The location of the rendezvous point.
x25519::PublicKey, // The ntor key for the rendezvous point.
HandshakeState, // Not a real type; used to finish the handshake.
),
}
// TODO: Not sure that the functions here and below need to be
// async, or if they should send messages on one-shots.
impl RendRequest {
pub async fn accept(self) -> Result<mpsc::Receiver<StreamRequest>>;
pub async fn reject(self) -> Result<()>;
// also various accessors
}
pub struct StreamRequest {
stream: DataStream, // Or possibly some other type that can turn
// into a DataStream.
target: SocketAddr,
// doesn't need to include a ClientCirc, since the DataStream
// can give you its circuit.
}
pub struct ServiceDataStream {
inner: DataStream,
}
impl StreamRequest
pub async fn accept(self) -> Result<ServiceDataStream>;
pub async fn reject(self) -> Result<()>;
pub fn shutdown_circuit(self) -> Result<()>;
// various accessors, including for circuit.
}
```
# Tickets to open
- API to inquire about number of current open streams on a circuit.