Skip to main content

rings_node/onion/tcp/
config.rs

1//! Native TCP onion-exit service configuration.
2
3use std::collections::BTreeSet;
4
5use reqwest::Url;
6
7use crate::error::Error;
8use crate::error::Result;
9use crate::onion::OnionExitPolicy;
10use crate::onion::OnionExitService;
11use crate::onion::OnionExitTransport;
12use crate::onion::OnionServiceName;
13
14/// Native TCP exit capabilities installed into the onion circuit data plane.
15///
16/// Invariant: `services` is non-empty and every name was derived from an advertised
17/// [`OnionExitService`] whose transport is [`OnionExitTransport::Tcp`].
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct NativeOnionTcpExitConfig {
20    services: Vec<OnionServiceName>,
21    policy: OnionExitPolicy,
22    https_proxy: Option<String>,
23}
24
25impl NativeOnionTcpExitConfig {
26    /// Build a native TCP exit config from advertised registry services.
27    pub fn new(
28        services: impl IntoIterator<Item = OnionExitService>,
29        policy: OnionExitPolicy,
30    ) -> Result<Self> {
31        let mut service_names = BTreeSet::new();
32        for service in services {
33            if service.transport != OnionExitTransport::Tcp {
34                return Err(Error::InvalidConfig(format!(
35                    "native onion TCP exit cannot serve {:?} over {:?}",
36                    service.name, service.transport
37                )));
38            }
39            service_names.insert(service.name);
40        }
41        if service_names.is_empty() {
42            return Err(Error::InvalidConfig(
43                "native onion TCP exit requires at least one TCP service".to_string(),
44            ));
45        }
46        Ok(Self {
47            services: service_names.into_iter().collect(),
48            policy,
49            https_proxy: None,
50        })
51    }
52
53    /// Build a native TCP exit config for the reserved `tcp` service.
54    pub fn tcp(policy: OnionExitPolicy) -> Self {
55        Self {
56            services: vec![OnionServiceName::tcp()],
57            policy,
58            https_proxy: None,
59        }
60    }
61
62    /// Explicitly delegate eligible synthetic-DNS HTTPS targets to this operator proxy.
63    ///
64    /// Ambient process proxy variables are intentionally ignored by onion exits: enabling a new
65    /// egress trust boundary must be an explicit node capability.
66    pub fn with_https_proxy(mut self, proxy: impl AsRef<str>) -> Result<Self> {
67        let proxy = proxy.as_ref().trim();
68        let parsed = Url::parse(proxy).map_err(|_| {
69            Error::InvalidConfig("native onion HTTPS proxy must be an absolute URL".to_string())
70        })?;
71        if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
72            return Err(Error::InvalidConfig(
73                "native onion HTTPS proxy must use http or https with a host".to_string(),
74            ));
75        }
76        self.https_proxy = Some(proxy.to_string());
77        Ok(self)
78    }
79
80    /// Return whether this exit may execute TCP payloads for `service`.
81    pub fn allows_service(&self, service: &OnionServiceName) -> bool {
82        self.services.iter().any(|candidate| candidate == service)
83    }
84
85    pub(super) fn policy(&self) -> &OnionExitPolicy {
86        &self.policy
87    }
88
89    pub(super) fn https_proxy(&self) -> Option<&str> {
90        self.https_proxy.as_deref()
91    }
92}