1use rings_core::dht::Did;
9
10use crate::error::Error;
11use crate::error::Result;
12use crate::onion::OnionExitDescriptor;
13use crate::onion::OnionExitService;
14use crate::onion::OnionExitTransport;
15pub use crate::onion::OnionProxyTarget;
16use crate::onion::OnionRoute;
17use crate::onion::OnionServiceName;
18use crate::online::OnlineNodeType;
19
20#[cfg(rings_native)]
21pub mod http;
22
23pub const ONION_PROXY_TCP_SERVICE: &str = "tcp";
25
26pub const ONION_PROXY_HTTPS_SERVICE: &str = "https";
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum OnionProxyProtocol {
32 TcpConnect,
34 HttpsProxy,
36}
37
38impl OnionProxyProtocol {
39 pub const fn exit_service(self) -> &'static str {
41 match self {
42 Self::TcpConnect => ONION_PROXY_TCP_SERVICE,
43 Self::HttpsProxy => ONION_PROXY_HTTPS_SERVICE,
44 }
45 }
46
47 pub const fn exit_transport(self) -> OnionExitTransport {
49 match self {
50 Self::TcpConnect => OnionExitTransport::Tcp,
51 Self::HttpsProxy => OnionExitTransport::Tcp,
52 }
53 }
54
55 pub const fn label(self) -> &'static str {
57 match self {
58 Self::TcpConnect => "tcp-connect",
59 Self::HttpsProxy => "https-proxy",
60 }
61 }
62
63 fn default_exit_service_name(self) -> OnionServiceName {
64 match self {
65 Self::TcpConnect => OnionServiceName::tcp(),
66 Self::HttpsProxy => OnionServiceName::https(),
67 }
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct OnionProxyConfig {
77 pub protocol: OnionProxyProtocol,
79 service: OnionServiceName,
80 pub hop_count: usize,
82 pub allow_short_paths: bool,
84}
85
86impl OnionProxyConfig {
87 pub fn new(protocol: OnionProxyProtocol, hop_count: usize, allow_short_paths: bool) -> Self {
89 Self {
90 protocol,
91 service: protocol.default_exit_service_name(),
92 hop_count,
93 allow_short_paths,
94 }
95 }
96
97 pub fn with_service(
99 protocol: OnionProxyProtocol,
100 service: OnionServiceName,
101 hop_count: usize,
102 allow_short_paths: bool,
103 ) -> Result<Self> {
104 validate_proxy_service(protocol, &service)?;
105 Ok(Self {
106 protocol,
107 service,
108 hop_count,
109 allow_short_paths,
110 })
111 }
112
113 pub fn tcp_connect(hop_count: usize, allow_short_paths: bool) -> Self {
115 Self::new(OnionProxyProtocol::TcpConnect, hop_count, allow_short_paths)
116 }
117
118 pub fn tcp_connect_service(
120 service: OnionServiceName,
121 hop_count: usize,
122 allow_short_paths: bool,
123 ) -> Result<Self> {
124 Self::with_service(
125 OnionProxyProtocol::TcpConnect,
126 service,
127 hop_count,
128 allow_short_paths,
129 )
130 }
131
132 pub fn https_proxy(hop_count: usize, allow_short_paths: bool) -> Self {
134 Self::new(OnionProxyProtocol::HttpsProxy, hop_count, allow_short_paths)
135 }
136
137 pub fn exit_service(&self) -> &str {
139 self.service.as_str()
140 }
141
142 pub fn exit_service_name(&self) -> &OnionServiceName {
144 &self.service
145 }
146
147 pub fn exit_transport(&self) -> OnionExitTransport {
149 self.protocol.exit_transport()
150 }
151
152 pub(crate) fn accepts_exit_descriptor(&self, descriptor: &OnionExitDescriptor) -> bool {
153 match self.protocol {
154 OnionProxyProtocol::TcpConnect => {
155 matches!(
156 descriptor.node_type,
157 OnlineNodeType::Native | OnlineNodeType::Ffi
158 ) && descriptor
159 .service
160 .matches(self.service.as_str(), OnionExitTransport::Tcp)
161 }
162 OnionProxyProtocol::HttpsProxy => {
163 self.service == OnionServiceName::https()
164 && descriptor
165 .offers_service_transport(self.service.as_str(), OnionExitTransport::Tcp)
166 }
167 }
168 }
169}
170
171fn validate_proxy_service(protocol: OnionProxyProtocol, service: &OnionServiceName) -> Result<()> {
172 if protocol == OnionProxyProtocol::HttpsProxy && service != &OnionServiceName::https() {
173 return Err(Error::InvalidConfig(format!(
174 "onion HTTPS proxy requires service {:?}",
175 OnionServiceName::https().as_str()
176 )));
177 }
178 if let Some(expected) = OnionExitService::reserved_transport(service.as_str()) {
179 if expected != protocol.exit_transport() {
180 return Err(Error::InvalidConfig(format!(
181 "onion proxy service {:?} requires {:?} transport, got {:?}",
182 service.as_str(),
183 expected,
184 protocol.exit_transport()
185 )));
186 }
187 }
188 Ok(())
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct OnionProxyRoute {
194 pub protocol: OnionProxyProtocol,
196 pub target: OnionProxyTarget,
198 pub route: OnionRoute,
200}
201
202impl OnionProxyRoute {
203 pub fn exit_did(&self) -> Did {
205 self.route.exit_did()
206 }
207
208 pub fn exit_service(&self) -> &str {
210 self.route.service()
211 }
212
213 pub const fn exit_transport(&self) -> OnionExitTransport {
215 self.protocol.exit_transport()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::error::Error;
223 use crate::error::Result;
224
225 #[test]
226 fn test_proxy_protocol_maps_to_exit_service() {
227 assert_eq!(OnionProxyProtocol::TcpConnect.exit_service(), "tcp");
228 assert_eq!(OnionProxyProtocol::HttpsProxy.exit_service(), "https");
229 assert_eq!(
230 OnionProxyProtocol::TcpConnect.exit_transport(),
231 OnionExitTransport::Tcp
232 );
233 assert_eq!(
234 OnionProxyProtocol::HttpsProxy.exit_transport(),
235 OnionExitTransport::Tcp
236 );
237 }
238
239 #[test]
240 fn test_proxy_config_is_target_agnostic() {
241 let proxy = OnionProxyConfig::https_proxy(3, false);
242
243 assert_eq!(proxy.exit_service(), "https");
244 assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
245 assert_eq!(proxy.hop_count, 3);
246 assert!(!proxy.allow_short_paths);
247 }
248
249 #[test]
250 fn test_tcp_proxy_config_accepts_custom_tcp_service() -> Result<()> {
251 let service = OnionServiceName::parse("web")?;
252 let proxy = OnionProxyConfig::tcp_connect_service(service, 2, true)?;
253
254 assert_eq!(proxy.exit_service(), "web");
255 assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
256 assert_eq!(proxy.hop_count, 2);
257 assert!(proxy.allow_short_paths);
258 Ok(())
259 }
260
261 #[test]
262 fn test_tcp_proxy_config_accepts_https_tcp_service() -> Result<()> {
263 let proxy = OnionProxyConfig::tcp_connect_service(OnionServiceName::https(), 1, false)?;
264
265 assert_eq!(proxy.exit_service(), "https");
266 assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
267 Ok(())
268 }
269
270 #[test]
271 fn test_target_authority_parses_domain_targets() -> Result<()> {
272 let target = OnionProxyTarget::parse_authority("Example.COM.:443")?;
273
274 assert_eq!(target.host(), "example.com");
275 assert_eq!(target.port(), 443);
276 assert_eq!(target.authority(), "example.com:443");
277 Ok(())
278 }
279
280 #[test]
281 fn test_target_authority_parses_ipv6_targets() -> Result<()> {
282 let target = OnionProxyTarget::parse_authority("[2001:db8::1]:8443")?;
283
284 assert_eq!(target.host(), "2001:db8::1");
285 assert_eq!(target.port(), 8443);
286 assert_eq!(target.authority(), "[2001:db8::1]:8443");
287 Ok(())
288 }
289
290 #[test]
291 fn test_target_authority_rejects_missing_port() {
292 assert!(matches!(
293 OnionProxyTarget::parse_authority("example.com"),
294 Err(Error::OnionProxyTarget(
295 crate::onion::OnionProxyTargetError::MissingPort
296 ))
297 ));
298 }
299}