1use std::net::IpAddr;
4#[cfg(any(test, rings_native))]
5use std::net::SocketAddr;
6
7#[cfg(rings_native)]
8use tokio::net::lookup_host;
9
10use crate::error::Error;
11use crate::error::Result;
12
13#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
15pub enum OnionProxyTargetError {
16 #[error("onion proxy target authority must not be empty")]
18 EmptyAuthority,
19 #[error("invalid bracketed IPv6 onion proxy authority")]
21 MissingIpv6Bracket,
22 #[error("onion proxy authority must include a port")]
24 MissingPort,
25 #[error("onion proxy target host must not be empty")]
27 EmptyHost,
28 #[error("onion proxy target host must not contain whitespace")]
30 HostWhitespace,
31 #[error("onion proxy target has an invalid port")]
33 InvalidPort,
34 #[error("onion proxy target port must be non-zero")]
36 ZeroPort,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct OnionProxyTarget {
42 host: String,
43 port: u16,
44}
45
46impl OnionProxyTarget {
47 pub fn parse_authority(authority: &str) -> Result<Self> {
49 let authority = authority.trim();
50 if authority.is_empty() {
51 return Err(OnionProxyTargetError::EmptyAuthority.into());
52 }
53
54 let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
55 let Some((host, rest)) = rest.split_once(']') else {
56 return Err(OnionProxyTargetError::MissingIpv6Bracket.into());
57 };
58 let Some(port) = rest.strip_prefix(':') else {
59 return Err(OnionProxyTargetError::MissingPort.into());
60 };
61 (host, port)
62 } else {
63 authority
64 .rsplit_once(':')
65 .ok_or(OnionProxyTargetError::MissingPort)?
66 };
67
68 let host = normalize_host(host)?;
69 let port = port
70 .parse::<u16>()
71 .map_err(|_| OnionProxyTargetError::InvalidPort)?;
72 if port == 0 {
73 return Err(OnionProxyTargetError::ZeroPort.into());
74 }
75
76 Ok(Self { host, port })
77 }
78
79 pub fn host(&self) -> &str {
81 &self.host
82 }
83
84 pub const fn port(&self) -> u16 {
86 self.port
87 }
88
89 pub fn authority(&self) -> String {
91 if self.host.contains(':') {
92 format!("[{}]:{}", self.host, self.port)
93 } else {
94 format!("{}:{}", self.host, self.port)
95 }
96 }
97}
98
99#[cfg(rings_native)]
104pub(crate) async fn resolve_public_target(target: &OnionProxyTarget) -> Result<Vec<SocketAddr>> {
105 let addresses = resolve_target_addresses(target).await?;
106 match select_public_exit_addresses(addresses) {
107 PublicAddressSelection::Public(addresses) => Ok(addresses),
108 PublicAddressSelection::Denied => Err(Error::NoPermission),
109 PublicAddressSelection::Empty => Err(Error::OnionTargetResolvedEmpty {
110 authority: target.authority(),
111 }),
112 }
113}
114
115#[cfg(rings_native)]
121pub(crate) async fn resolve_target_addresses(target: &OnionProxyTarget) -> Result<Vec<SocketAddr>> {
122 Ok(lookup_host((target.host(), target.port()))
123 .await
124 .map_err(|error| Error::OnionTargetResolve {
125 authority: target.authority(),
126 source: error,
127 })?
128 .collect())
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
132#[cfg(any(test, rings_native))]
133pub(crate) enum PublicAddressSelection {
134 Empty,
135 Denied,
136 Public(Vec<SocketAddr>),
137}
138
139#[cfg(any(test, rings_native))]
141pub(crate) fn select_public_exit_addresses(addresses: Vec<SocketAddr>) -> PublicAddressSelection {
142 if addresses.is_empty() {
143 return PublicAddressSelection::Empty;
144 }
145 let public = addresses
146 .into_iter()
147 .filter(|address| is_public_exit_ip(address.ip()))
148 .fold(Vec::new(), |mut selected, address| {
149 if !selected.contains(&address) {
150 selected.push(address);
151 }
152 selected
153 });
154 if public.is_empty() {
155 PublicAddressSelection::Denied
156 } else {
157 PublicAddressSelection::Public(public)
158 }
159}
160
161#[cfg(rings_browser)]
164pub(crate) fn validate_public_ip_literal(target: &OnionProxyTarget) -> Result<()> {
165 let address = target
166 .host()
167 .parse::<IpAddr>()
168 .map_err(|_| Error::NoPermission)?;
169 if is_public_exit_ip(address) {
170 Ok(())
171 } else {
172 Err(Error::NoPermission)
173 }
174}
175
176const fn is_public_exit_ip(address: IpAddr) -> bool {
183 match address {
184 IpAddr::V4(address) => is_public_exit_ipv4(address.octets()),
185 IpAddr::V6(address) => {
186 let octets = address.octets();
187 if let Some(ipv4) = embedded_ipv4(octets) {
188 return is_public_exit_ipv4(ipv4);
189 }
190 if octets[0] < 0x20 || octets[0] > 0x3f {
194 return false;
195 }
196 !matches!(
197 octets,
198 [0x20, 0x01, 0x00..=0x01, ..]
200 | [0x20, 0x01, 0x0d, 0xb8, ..]
202 | [0x20, 0x02, ..]
204 | [0x3f, 0xf0..=0xff, ..]
206 )
207 }
208 }
209}
210
211const fn embedded_ipv4(octets: [u8; 16]) -> Option<[u8; 4]> {
212 let compatible_prefix = octets[0] == 0
213 && octets[1] == 0
214 && octets[2] == 0
215 && octets[3] == 0
216 && octets[4] == 0
217 && octets[5] == 0
218 && octets[6] == 0
219 && octets[7] == 0
220 && octets[8] == 0
221 && octets[9] == 0;
222 if compatible_prefix
223 && ((octets[10] == 0 && octets[11] == 0) || (octets[10] == 0xff && octets[11] == 0xff))
224 {
225 Some([octets[12], octets[13], octets[14], octets[15]])
226 } else {
227 None
228 }
229}
230
231const fn is_public_exit_ipv4([first, second, third, _fourth]: [u8; 4]) -> bool {
232 !matches!(
233 (first, second, third),
234 (0, _, _)
235 | (10, _, _)
236 | (100, 64..=127, _)
237 | (127, _, _)
238 | (169, 254, _)
239 | (172, 16..=31, _)
240 | (192, 0, 0)
241 | (192, 0, 2)
242 | (192, 88, 99)
243 | (192, 168, _)
244 | (198, 18..=19, _)
245 | (198, 51, 100)
246 | (203, 0, 113)
247 | (224..=255, _, _)
248 )
249}
250
251fn normalize_host(host: &str) -> Result<String> {
252 let host = host.trim().trim_end_matches('.');
253 if host.is_empty() {
254 return Err(OnionProxyTargetError::EmptyHost.into());
255 }
256 if host.chars().any(char::is_whitespace) {
257 return Err(OnionProxyTargetError::HostWhitespace.into());
258 }
259 Ok(host.to_ascii_lowercase())
260}
261
262#[cfg(test)]
263mod tests {
264 use std::net::IpAddr;
265 use std::net::SocketAddr;
266
267 use super::is_public_exit_ip;
268 #[cfg(rings_native)]
269 use super::resolve_public_target;
270 use super::select_public_exit_addresses;
271 #[cfg(rings_native)]
272 use super::OnionProxyTarget;
273 use super::PublicAddressSelection;
274 #[cfg(rings_native)]
275 use crate::error::Error;
276
277 #[test]
278 fn test_public_address_selection_distinguishes_empty_denied_and_deduplicated_public() {
279 let denied: SocketAddr = "127.0.0.1:443".parse().expect("denied address");
280 let public: SocketAddr = "8.8.8.8:443".parse().expect("public address");
281
282 assert_eq!(
283 select_public_exit_addresses(Vec::new()),
284 PublicAddressSelection::Empty
285 );
286 assert_eq!(
287 select_public_exit_addresses(vec![denied]),
288 PublicAddressSelection::Denied
289 );
290 assert_eq!(
291 select_public_exit_addresses(vec![denied, public, public]),
292 PublicAddressSelection::Public(vec![public])
293 );
294 }
295
296 #[test]
297 fn test_exit_address_predicate_rejects_internal_and_special_destinations() {
298 for address in [
299 "0.0.0.0",
300 "10.0.0.1",
301 "100.64.0.1",
302 "127.0.0.1",
303 "169.254.169.254",
304 "172.16.0.1",
305 "192.168.0.1",
306 "198.18.0.1",
307 "224.0.0.1",
308 "::",
309 "::1",
310 "::ffff:127.0.0.1",
311 "64:ff9b::7f00:1",
312 "2001:db8::1",
313 "2002:7f00:1::",
314 "3fff::1",
315 "4000::1",
316 "fc00::1",
317 "fe80::1",
318 "ff02::1",
319 ] {
320 let address = address.parse::<IpAddr>().expect("valid fixture address");
321 assert!(
322 !is_public_exit_ip(address),
323 "accepted special address {address}"
324 );
325 }
326 }
327
328 #[test]
329 fn test_exit_address_predicate_accepts_public_destinations() {
330 for address in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"] {
331 let address = address.parse::<IpAddr>().expect("valid fixture address");
332 assert!(
333 is_public_exit_ip(address),
334 "rejected public address {address}"
335 );
336 }
337 }
338
339 #[cfg(rings_native)]
340 #[tokio::test]
341 async fn test_resolver_rejects_loopback_before_any_exit_connection() {
342 let target =
343 OnionProxyTarget::parse_authority("127.0.0.1:443").expect("valid loopback authority");
344
345 assert!(matches!(
346 resolve_public_target(&target).await,
347 Err(Error::NoPermission)
348 ));
349 }
350}