Skip to content
Snippets Groups Projects
Commit 38bf85ea authored by Nick Mathewson's avatar Nick Mathewson :fire:
Browse files

Merge branch 'hss_config' into 'main'

Begin working on configuration logic for onion services

See merge request tpo/core/arti!1557
parents 06233adc bbfb9e4c
No related branches found
Tags tor-0.1.0.5-rc
No related merge requests found
......@@ -4922,12 +4922,25 @@ dependencies = [
"tor-hscrypto",
]
[[package]]
name = "tor-hsrproxy"
version = "0.1.0"
dependencies = [
"derive_builder_fork_arti",
"serde",
"serde_json",
"serde_with",
"thiserror",
"tor-config",
]
[[package]]
name = "tor-hsservice"
version = "0.2.4"
dependencies = [
"async-broadcast",
"async-trait",
"base64ct",
"derive-adhoc",
"derive_builder_fork_arti",
"derive_more",
......
......@@ -47,6 +47,7 @@ members = [
"crates/tor-keymgr",
"crates/tor-hsclient",
"crates/tor-hsservice",
"crates/tor-hsrproxy",
"crates/arti-client",
"crates/arti-rpcserver",
"crates/arti-config",
......
[package]
name = "tor-hsrproxy"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
edition = "2021"
rust-version = "1.65"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Reverse proxy to build an onion service that connects to local servers."
keywords = ["tor", "arti", "cryptography"]
categories = ["cryptography"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
publish = false
[features]
default = []
full = []
[dependencies]
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
serde = { version = "1.0.103", features = ["derive"] }
serde_with = "3.0.0"
thiserror = "1"
tor-config = { version = "0.9.3", path = "../tor-config" }
[dev-dependencies]
serde_json = "1.0.50"
# tor-hsrproxy
A "reverse proxy" implementation for onion services.
This crate is used in connection with `tor-hsservice` to crate an
onion service that works by opening connections to local services.
It is a separate crate from `tor-hsservice` because it is only one of
the possible ways to handle incoming onion service streams.
## 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.
//! Configuration logic for onion service reverse proxy.
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, ops::RangeInclusive, path::PathBuf, str::FromStr};
use tor_config::{define_list_builder_accessors, define_list_builder_helper, ConfigBuildError};
/// Configuration for a reverse proxy running for a single onion service.
#[derive(Clone, Debug, Builder)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
pub struct ProxyConfig {
/// A list of rules to apply to incoming requests. If no rule
/// matches, we take the DestroyCircuit action.
#[builder(sub_builder, setter(custom))]
pub(crate) proxy_ports: ProxyRuleList,
}
// ^ TODO HSS: Add validation function to make sure that there are no
// unreachable rules.
define_list_builder_accessors! {
struct ProxyConfigBuilder {
pub proxy_ports: [ProxyRule],
}
}
/// Helper to define builder for ProxyConfig.
type ProxyRuleList = Vec<ProxyRule>;
define_list_builder_helper! {
pub struct ProxyRuleListBuilder {
pub(crate) values: [ProxyRule],
}
built: ProxyRuleList = values;
default = vec![];
item_build: |value| Ok(value.clone());
}
/// A single rule in a `ProxyConfig`.
///
/// Rules take the form of, "When this pattern matches, take this action."
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
// TODO HSS: we might someday want to accept structs here as well, so that
// we can add per-rule fields if we need to. We can make that an option if/when
// it comes up, however.
#[serde(from = "ProxyRuleAsTuple", into = "ProxyRuleAsTuple")]
pub struct ProxyRule {
/// Any connections to a port matching this pattern match this rule.
source: ProxyPattern,
/// When this rule matches, we take this action.
target: ProxyTarget,
}
/// Helper type used to (de)serialize ProxyRule.
type ProxyRuleAsTuple = (ProxyPattern, ProxyTarget);
impl From<ProxyRuleAsTuple> for ProxyRule {
fn from(value: ProxyRuleAsTuple) -> Self {
Self {
source: value.0,
target: value.1,
}
}
}
impl From<ProxyRule> for ProxyRuleAsTuple {
fn from(value: ProxyRule) -> Self {
(value.source, value.target)
}
}
impl ProxyRule {
/// Create a new ProxyRule mapping `source` to `target`.
pub fn new(source: ProxyPattern, target: ProxyTarget) -> Self {
Self { source, target }
}
}
/// A set of ports to use when checking how to handle a port.
#[derive(
Clone, Debug, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, Eq, PartialEq,
)]
pub struct ProxyPattern(
// TODO HSS: Eventually, we will want to allow other patterns, like UDP.
RangeInclusive<u16>,
);
impl FromStr for ProxyPattern {
type Err = ProxyConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use ProxyConfigError as PCE;
if s == "*" {
Ok(Self::all_ports())
} else if let Some((left, right)) = s.split_once('-') {
let left: u16 = left.parse().map_err(PCE::InvalidPort)?;
let right: u16 = right.parse().map_err(PCE::InvalidPort)?;
Self::port_range(left, right)
} else {
let port = s.parse().map_err(PCE::InvalidPort)?;
Self::one_port(port)
}
}
}
impl std::fmt::Display for ProxyPattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.clone().into_inner() {
(start, end) if start == end => write!(f, "{}", start),
(1, 65535) => write!(f, "*"),
(start, end) => write!(f, "{}-{}", start, end),
}
}
}
impl ProxyPattern {
/// Return a pattern matching all ports.
pub fn all_ports() -> Self {
Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
}
/// Return a pattern matching a single port.
///
/// Gives an error if the port is zero.
pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
Self::check(port, port)
}
/// Return a pattern matching all ports between `low` and `high` inclusive.
///
/// Gives an error unless `0 < low <= high`.
pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
Self::check(low, high)
}
/// Return true if this pattern includes `port`.
pub(crate) fn matches_port(&self, port: u16) -> bool {
self.0.contains(&port)
}
/// If start..=end is a valid pattern, wrap it as a ProxyPattern. Otherwise return
/// an error.
fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
use ProxyConfigError as PCE;
match (start, end) {
(_, 0) => Err(PCE::ZeroPort),
(0, n) => Ok(Self(1..=n)),
(low, high) if low > high => Err(PCE::EmptyPortRange),
(low, high) => Ok(Self(low..=high)),
}
}
}
/// An action to take upon receiving an incoming request.
#[derive(
Clone,
Debug,
Default,
serde_with::DeserializeFromStr,
serde_with::SerializeDisplay,
Eq,
PartialEq,
)]
#[non_exhaustive]
pub enum ProxyTarget {
/// Close the circuit immediately with an error.
#[default]
DestroyCircuit,
/// Open a TCP connection to a given address and port.
Tcp(SocketAddr),
/// Open an AF_UNIX connection to a given address.
Unix(PathBuf),
/// Close the stream immediately with an error.
RejectStream,
/// Ignore the stream request.
IgnoreStream,
// TODO HSS: Eventually, we will want to allow other protocols, like
// haproxy. THese might be orthogonal to Tcp vs Unix. Do we want to add
// these as flags to ProxyTarget, or some other thing?
//
// And does the Udp vs Tcp distinction belong here or in ProxyPattern?
//
// See thread at
// https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1557#note_2938349
}
impl FromStr for ProxyTarget {
type Err = ProxyConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use ProxyConfigError as PCE;
/// Return true if 's' looks like an attempted IPv4 or IPv6 socketaddr.
fn looks_like_attempted_addr(s: &str) -> bool {
s.starts_with(|c: char| c.is_ascii_digit())
|| s.strip_prefix('[')
.map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
.unwrap_or(false)
}
if s == "destroy" {
Ok(Self::DestroyCircuit)
} else if s == "reject" {
Ok(Self::RejectStream)
} else if s == "ignore" {
Ok(Self::IgnoreStream)
} else if let Some(path) = s.strip_prefix("unix:") {
Ok(Self::Unix(PathBuf::from(path)))
} else if let Some(addr) = s.strip_prefix("tcp:") {
Ok(Self::Tcp(addr.parse().map_err(PCE::InvalidTargetAddr)?))
} else if looks_like_attempted_addr(s) {
// We check 'looks_like_attempted_addr' before parsing this.
Ok(Self::Tcp(s.parse().map_err(PCE::InvalidTargetAddr)?))
} else {
Err(PCE::UnrecognizedTargetType)
}
}
}
impl std::fmt::Display for ProxyTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProxyTarget::DestroyCircuit => write!(f, "destroy"),
ProxyTarget::Tcp(addr) => write!(f, "tcp:{}", addr),
ProxyTarget::Unix(path) => write!(f, "unix:{}", path.display()),
ProxyTarget::RejectStream => write!(f, "reject"),
ProxyTarget::IgnoreStream => write!(f, "ignore"),
}
}
}
/// An error encountered while parsing or applying a proxy configuration.
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum ProxyConfigError {
/// We encountered a proxy target with an unrecognized type keyword.
#[error("Could not parse proxy target type.")]
UnrecognizedTargetType,
/// A socket address could not be parsed to be invalid.
#[error("Could not parse proxy target address.")]
InvalidTargetAddr(#[source] std::net::AddrParseError),
/// A socket rule had an source port that couldn't be parsed as a `u16`.
#[error("Could not parse proxy source port.")]
InvalidPort(#[source] std::num::ParseIntError),
/// A socket rule had a zero source port.
#[error("Zero is not a valid port.")]
ZeroPort,
/// A socket rule specified an empty port range.
#[error("Port range is empty.")]
EmptyPortRange,
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
#[test]
fn pattern_ok() {
use ProxyPattern as P;
assert_eq!(P::from_str("*").unwrap(), P(1..=65535));
assert_eq!(P::from_str("100").unwrap(), P(100..=100));
assert_eq!(P::from_str("100-200").unwrap(), P(100..=200));
assert_eq!(P::from_str("0-200").unwrap(), P(1..=200));
}
#[test]
fn pattern_display() {
use ProxyPattern as P;
assert_eq!(P::all_ports().to_string(), "*");
assert_eq!(P::one_port(100).unwrap().to_string(), "100");
assert_eq!(P::port_range(100, 200).unwrap().to_string(), "100-200");
}
#[test]
fn pattern_err() {
use ProxyConfigError as PCE;
use ProxyPattern as P;
assert!(matches!(P::from_str("fred"), Err(PCE::InvalidPort(_))));
assert!(matches!(P::from_str("100-fred"), Err(PCE::InvalidPort(_))));
assert!(matches!(P::from_str("100-42"), Err(PCE::EmptyPortRange)));
}
#[test]
fn target_ok() {
use ProxyTarget as T;
assert!(matches!(T::from_str("reject"), Ok(T::RejectStream)));
assert!(matches!(T::from_str("ignore"), Ok(T::IgnoreStream)));
assert!(matches!(T::from_str("destroy"), Ok(T::DestroyCircuit)));
let sa: SocketAddr = "192.168.1.1:50".parse().unwrap();
assert!(matches!(T::from_str("192.168.1.1:50"), Ok(T::Tcp(a)) if a == sa));
assert!(matches!(T::from_str("tcp:192.168.1.1:50"), Ok(T::Tcp(a)) if a == sa));
let sa: SocketAddr = "[::1]:999".parse().unwrap();
assert!(matches!(T::from_str("[::1]:999"), Ok(T::Tcp(a)) if a == sa));
assert!(matches!(T::from_str("tcp:[::1]:999"), Ok(T::Tcp(a)) if a == sa));
let pb = PathBuf::from("/var/run/hs/socket");
assert!(matches!(T::from_str("unix:/var/run/hs/socket"), Ok(T::Unix(p)) if p == pb));
}
#[test]
fn target_display() {
use ProxyTarget as T;
assert_eq!(T::RejectStream.to_string(), "reject");
assert_eq!(T::IgnoreStream.to_string(), "ignore");
assert_eq!(T::DestroyCircuit.to_string(), "destroy");
assert_eq!(
T::Tcp("192.168.1.1:50".parse().unwrap()).to_string(),
"tcp:192.168.1.1:50"
);
assert_eq!(
T::Tcp("[::1]:999".parse().unwrap()).to_string(),
"tcp:[::1]:999"
);
assert_eq!(
T::Unix("/var/run/hs/socket".into()).to_string(),
"unix:/var/run/hs/socket"
);
}
#[test]
fn target_err() {
use ProxyConfigError as PCE;
use ProxyTarget as T;
assert!(matches!(
T::from_str("sdakljf"),
Err(PCE::UnrecognizedTargetType)
));
assert!(matches!(
T::from_str("tcp:hello"),
Err(PCE::InvalidTargetAddr(_))
));
assert!(matches!(
T::from_str("127.1:80"),
Err(PCE::InvalidTargetAddr(_))
));
assert!(matches!(
T::from_str("tcp:127.1:80"),
Err(PCE::InvalidTargetAddr(_))
));
assert!(matches!(
T::from_str("127.1:80"),
Err(PCE::InvalidTargetAddr(_))
));
assert!(matches!(
T::from_str("tcp:2130706433:80"),
Err(PCE::InvalidTargetAddr(_))
));
assert!(matches!(
T::from_str("128.256.cats.and.dogs"),
Err(PCE::InvalidTargetAddr(_))
));
}
#[test]
fn deserialize() {
let ex = r#"{
"proxy_ports": [
[ "443", "127.0.0.1:11443" ],
[ "80", "ignore" ],
[ "*", "destroy" ]
]
}"#;
let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
let cfg = bld.build().unwrap();
assert_eq!(cfg.proxy_ports.len(), 3);
// TODO HSS: test actual values.
}
}
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
// @@ 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))]
#![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(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)] // TODO HSS: remove this.
pub mod config;
......@@ -27,6 +27,7 @@ full = [
[dependencies]
async-broadcast = "0.5.0"
async-trait = "0.1.54"
base64ct = "1.5.1"
derive-adhoc = "0.7.3"
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
derive_more = "0.99.17"
......
//! Define the `Anonymity` type to indicate a level of anonymity.
/// 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.
///
/// Can be represented in a serde-based configuration as `true` or
/// `"anonymous"` (case insensitive).
#[default]
Anonymous,
/// Do not try to keep the location of the onion service private.
///
/// (This is implemented using our "single onion service" design.)
///
/// Can be represented in a serde-based configuration as`"non_anonymous"`
/// (case insensitive).
//
// TODO HSS: We may want to put this behind a feature?
DangerouslyNonAnonymous,
}
/// A string used to represent `Anonymity::Anonymous` in serde.
const ANON_STRING: &str = "anonymous";
/// A string used to represent `Anonymity::DangerouslyNonAnonymous` in serde.
const DANGER_STRING: &str = "not_anonymous";
impl serde::Serialize for Anonymity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Anonymity::Anonymous => serializer.serialize_bool(true),
Anonymity::DangerouslyNonAnonymous => serializer.serialize_str(DANGER_STRING),
}
}
}
impl<'de> serde::Deserialize<'de> for Anonymity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
/// Visitor struct to deserialize an Anonymity object.
struct Vis;
impl<'de> serde::de::Visitor<'de> for Vis {
type Value = Anonymity;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
r#"`true`, `{:?}`, or `{:?}`"#,
ANON_STRING, DANGER_STRING
)
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Ok(Anonymity::Anonymous)
} else {
Err(E::invalid_value(serde::de::Unexpected::Bool(v), &self))
}
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if s.eq_ignore_ascii_case(ANON_STRING) {
Ok(Anonymity::Anonymous)
} else if s.eq_ignore_ascii_case(DANGER_STRING) {
Ok(Anonymity::DangerouslyNonAnonymous)
} else {
Err(E::invalid_value(serde::de::Unexpected::Str(s), &self))
}
}
}
deserializer.deserialize_any(Vis)
}
}
//! Configuration information for onion services.
//
// TODO HSS: We may want rename some of the types and members here!
use base64ct::{Base64Unpadded, Encoding as _};
use derive_builder::Builder;
use std::path::PathBuf;
use tor_config::ConfigBuildError;
use tor_hscrypto::pk::HsClientDescEncKey;
use tor_llcrypto::pk::curve25519;
use crate::HsNickname;
/// Configuration for a single onion service.
#[derive(Debug, Clone)]
/// Configuration for one onion service.
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(error = "ConfigBuildError", validate = "Self::validate"))]
pub struct OnionServiceConfig {
/// The nickname used to look up this service's keys, state, configuration, etc,
//
......@@ -12,16 +22,190 @@ pub struct OnionServiceConfig {
// which the service's configuration is stored. We'll see how the code
// evolves.
// (^ ipt_mgr::IptManager contains a copy of this nickname, that should be fixed too)
nickname: HsNickname,
pub(crate) name: HsNickname,
// TODO HSS: Perhaps this belongs at a higher level.
// enabled: bool,
/// 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,
pub(crate) anonymity: crate::Anonymity,
/// Number of intro points; defaults to 3; max 20.
/// TODO HSS config this Option should be defaulted prior to the value ending up here
pub(crate) 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?
#[builder(default = "3")]
pub(crate) num_intro_points: u8,
/// Limits on rates and concurrency of connections to our service.
#[builder(sub_builder)]
pub(crate) limits: LimitConfig,
/// Configure proof-of-work defense against DoS attacks.
#[builder(sub_builder)]
pub(crate) pow: PowConfig,
/// Configure descriptor-based client authorization.
///
/// When this is enabled, we encrypt our list of introduction point and keys
/// so that only clients holding one of the listed keys can decrypt it.
//
// TODO HSS: we'd like this to be an Option, but that doesn't work well with
// sub_builder. We need to figure out what to do there.
pub(crate) encrypt_descriptor: Option<DescEncryptionConfig>,
//
// TODO HSS: Do we want a "descriptor_lifetime" setting? C tor doesn't have
// one.
}
impl OnionServiceConfigBuilder {
/// Builder helper: check wither the options in this builder are consistent.
fn validate(&self) -> Result<(), ConfigBuildError> {
/// Largest supported number of introduction points
//
// TODO HSS Is this a consensus parameter or anything? What does C tor do?
const MAX_INTRO_POINTS: u8 = 20;
if let Some(ipts) = self.num_intro_points {
if !(1..=MAX_INTRO_POINTS).contains(&ipts) {
return Err(ConfigBuildError::Invalid {
field: "num_intro_points".into(),
problem: "Out of range 1..20".into(),
});
}
}
Ok(())
}
}
/// Configuration for maximum rates and concurrency.
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(error = "ConfigBuildError"))]
pub struct LimitConfig {
/// A rate-limit on the acceptable rate of introduction requests.
///
/// We send this to the send to the introduction point to configure how many
/// introduction requests it sends us.
rate_limit_at_intro: Option<TokenBucketConfig>,
/// How many streams will we allow to be open at once for a single circuit on
/// this service?
#[builder(default = "65535")]
max_concurrent_streams_per_circuit: u32,
}
/// Configuration for proof-of-work defense against DoS attacks.
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(error = "ConfigBuildError"))]
pub struct PowConfig {
/// If true, we will require proof-of-work when we're under heavy load.
enable_pow: bool,
/// Disable the compiled backend for proof-of-work.
disable_pow_compilation: bool,
// TODO HSS: C tor has this, but I don't know if we want it.
//
// TODO HSS: It's possible that we want this to relate, somehow, to our
// rate_limit_at_intro settings.
//
// /// A rate-limit on dispatching requests from the request queue when
// /// our proof-of-work defense is enabled.
// pow_queue_rate: TokenBucketConfig,
// ...
}
/// Configure a token-bucket style limit on some process.
//
// TODO HSS: possibly lower this; it will be used in far more places.
//
// TODO: Do we want to parameterize this, or make it always u32? Do we want to
// specify "per second"?
#[derive(Debug, Clone)]
pub struct TokenBucketConfig {
/// The maximum number of items to process per second.
rate: u32,
/// The maximum number of items to process in a single burst.
burst: u32,
}
impl TokenBucketConfig {
/// Create a new token-bucket configuration to rate-limit some action.
///
/// The "bucket" will have a maximum capacity of `burst`, and will fill at a
/// rate of `rate` per second. New actions are permitted if the bucket is nonempty;
/// each action removes one token from the bucket.
pub fn new(rate: u32, burst: u32) -> Self {
Self { rate, burst }
}
}
/// Configuration for descriptor encryption.
#[derive(Debug, Clone)]
pub struct DescEncryptionConfig {
/// A list of our authorized clients.
///
/// Note that if this list is empty, no clients can connect.
//
// TODO HSS: It might be good to replace this with a trait or something, so that
// we can let callers give us a ClientKeyProvider or some plug-in that reads
// keys from somewhere else. On the other hand, we might have this configure
// our default ClientKeyProvider, and only allow programmatic ClientKeyProviders
authorized_client: Vec<AuthorizedClientConfig>,
}
/// A single client (or a collection of clients) authorized using the descriptor encryption mechanism.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AuthorizedClientConfig {
/// A directory full of authorized public keys.
DirectoryOfKeys(PathBuf),
/// A single authorized public key.
Curve25519Key(HsClientDescEncKey),
}
impl std::fmt::Display for AuthorizedClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DirectoryOfKeys(pb) => write!(f, "dir:{}", pb.display()),
Self::Curve25519Key(key) => write!(
f,
"curve25519:{}",
Base64Unpadded::encode_string(key.as_bytes())
),
}
}
}
/// A problem encountered while parsing an AuthorizedClientConfig.
#[derive(thiserror::Error, Clone, Debug)]
#[non_exhaustive]
pub enum AuthorizedClientParseError {
/// Didn't recognize the type of this [`AuthorizedClientConfig`].
///
/// Recognized types are `dir` and `curve25519`.
#[error("Unrecognized authorized client type")]
InvalidType,
/// Couldn't parse a curve25519 key.
#[error("Invalid curve25519 key")]
InvalidKey,
}
impl std::str::FromStr for AuthorizedClientConfig {
type Err = AuthorizedClientParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((tp, val)) = s.split_once(':') else {
return Err(Self::Err::InvalidType);
};
if tp == "dir" {
Ok(Self::DirectoryOfKeys(val.into()))
} else if tp == "curve25519" {
let bytes: [u8; 32] = Base64Unpadded::decode_vec(val)
.map_err(|_| Self::Err::InvalidKey)?
.try_into()
.map_err(|_| Self::Err::InvalidKey)?;
Ok(Self::Curve25519Key(HsClientDescEncKey::from(
curve25519::PublicKey::from(bytes),
)))
} else {
Err(Self::Err::InvalidType)
}
}
}
......@@ -928,11 +928,7 @@ impl<R: Runtime, M: Mockable<R>> IptManager<R, M> {
/// Target number of intro points
pub(crate) fn target_n_intro_points(&self) -> usize {
self.state
.config
.num_intro_points
.unwrap_or(3 /* TODO HSS should be a const */)
.into()
self.state.config.num_intro_points.into()
}
/// Maximum number of concurrent intro point relays
......
......@@ -42,7 +42,8 @@
#![allow(dead_code, unused_variables)] // TODO hss remove.
mod config;
mod anon_level;
pub mod config;
mod err;
mod ipt_mgr;
mod ipt_set;
......@@ -53,24 +54,10 @@ mod status;
mod svc;
mod timeout_track;
pub use anon_level::Anonymity;
pub use config::OnionServiceConfig;
pub use err::{ClientError, FatalError, StartupError};
pub use nickname::{HsNickname, InvalidNickname};
pub use req::{OnionServiceDataStream, RendRequest, StreamRequest};
pub use status::OnionServiceStatus;
pub use svc::OnionService;
/// 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: We may want to put this behind a feature?
DangerouslyNonAnonymous,
}
......@@ -4,6 +4,11 @@
Here are some notes about high level api/ui design for onion services,
and what it might look like.
2023-08-28
See [!1541](https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1541)
for comments on this note.
# Top CLI level: the `arti` CLI tool.
I'm imagining that the configuration looks something like this:
......@@ -70,6 +75,8 @@ encrypt_descriptor = [
# Note that you can also give a singleton, as in:
# encrypt_descriptor = 'dir:/path/to/dir".
# Set the number of introduction points to try to use for the onion service.
num_intro_points = 3
# This option configures port relaying, which is the only option
# available at the CLI for actually implementing an onion service.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment