1use std::collections::HashMap;
77use std::fmt;
78use std::future::Future;
79use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
80use std::pin::Pin;
81use std::sync::{Arc, Mutex, PoisonError};
82use std::time::Duration;
83
84use reqwest::dns::{Addrs, Name, Resolve, Resolving};
85pub use reqwest::{Method, Request, Response, StatusCode, Url, header};
88use url::Host;
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Allowance {
93 PublicOnly,
96 InternalNetwork,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct Timeouts {
105 pub connect: Duration,
107 pub request: Duration,
111}
112
113impl Timeouts {
114 pub const DEFAULT: Timeouts = Timeouts {
115 connect: Duration::from_secs(10),
116 request: Duration::from_secs(30),
117 };
118}
119
120#[derive(Debug, Clone)]
123pub struct Egress {
124 public: EgressClient,
125 internal: EgressClient,
126}
127
128impl Egress {
129 pub fn new(timeouts: Timeouts) -> anyhow::Result<Egress> {
130 Ok(Egress {
131 public: EgressClient::build(Allowance::PublicOnly, timeouts)?,
132 internal: EgressClient::build(Allowance::InternalNetwork, timeouts)?,
133 })
134 }
135
136 pub fn client(&self, allowance: Allowance) -> EgressClient {
138 match allowance {
139 Allowance::PublicOnly => self.public.clone(),
140 Allowance::InternalNetwork => self.internal.clone(),
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct EgressClient {
148 http: reqwest::Client,
149 plain: Option<reqwest::Client>,
152 allowance: Allowance,
153 timeouts: Timeouts,
154}
155
156impl EgressClient {
157 fn build(allowance: Allowance, timeouts: Timeouts) -> anyhow::Result<EgressClient> {
158 EgressClient::build_with(allowance, timeouts, system_lookup)
159 }
160
161 fn build_with(allowance: Allowance, timeouts: Timeouts, system: SystemLookup) -> anyhow::Result<EgressClient> {
165 let (reach, plain) = match allowance {
166 Allowance::PublicOnly => (Reach::Public, None),
167 Allowance::InternalNetwork => (Reach::Any, Some(http_client(Reach::Internal, timeouts, system)?)),
168 };
169 Ok(EgressClient {
170 http: http_client(reach, timeouts, system)?,
171 plain,
172 allowance,
173 timeouts,
174 })
175 }
176
177 fn for_url(&self, url: &Url) -> &reqwest::Client {
180 match &self.plain {
181 Some(plain) if url.scheme() == "http" => plain,
182 _ => &self.http,
183 }
184 }
185
186 pub fn allowance(&self) -> Allowance {
187 self.allowance
188 }
189
190 pub async fn send(&self, mut request: Request) -> Result<Response, EgressError> {
194 check_url(request.url(), self.allowance)?;
195 if request.timeout().is_none() {
196 *request.timeout_mut() = Some(self.timeouts.request);
197 }
198 self.for_url(request.url()).execute(request).await.map_err(classify)
199 }
200
201 pub async fn send_streaming(&self, mut request: Request) -> Result<Response, EgressError> {
205 check_url(request.url(), self.allowance)?;
206 let head = request.timeout_mut().take().unwrap_or(self.timeouts.request);
207 let http = self.for_url(request.url());
208 match tokio::time::timeout(head, http.execute(request)).await {
209 Ok(sent) => sent.map_err(classify),
210 Err(_) => Err(EgressError::Timeout),
211 }
212 }
213}
214
215fn http_client(reach: Reach, timeouts: Timeouts, system: SystemLookup) -> anyhow::Result<reqwest::Client> {
216 Ok(reqwest::Client::builder()
217 .user_agent(concat!("hennery/", env!("CARGO_PKG_VERSION")))
218 .no_proxy()
219 .redirect(reqwest::redirect::Policy::none())
220 .dns_resolver(Arc::new(CheckedResolver { reach, system }))
221 .connect_timeout(timeouts.connect)
222 .build()?)
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum Refused {
228 Scheme(String),
231 Credentials,
233 NoHost,
235 Address(IpAddr),
237 Resolved { host: String, addr: IpAddr },
240}
241
242impl fmt::Display for Refused {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Refused::Scheme(scheme) => write!(
246 f,
247 "{scheme:?} URLs are refused: https only, or http to loopback or, on an internal network, to an internal address or a name"
248 ),
249 Refused::Credentials => f.write_str("URLs with credentials are refused"),
250 Refused::NoHost => f.write_str("the URL has no host"),
251 Refused::Address(addr) => write!(f, "{addr} is not a public address"),
252 Refused::Resolved { host, addr } => {
253 write!(f, "{host} resolves to {addr}, which this request may not reach")
254 }
255 }
256 }
257}
258
259impl std::error::Error for Refused {}
260
261#[derive(Debug)]
263pub enum EgressError {
264 Refused(Refused),
266 Timeout,
268 Http(reqwest::Error),
270}
271
272impl fmt::Display for EgressError {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match self {
275 EgressError::Refused(refused) => write!(f, "refused by the egress policy: {refused}"),
276 EgressError::Timeout => f.write_str("the request timed out"),
277 EgressError::Http(err) => write!(f, "{err}"),
278 }
279 }
280}
281
282impl std::error::Error for EgressError {
283 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
284 match self {
285 EgressError::Refused(refused) => Some(refused),
286 EgressError::Timeout => None,
287 EgressError::Http(err) => Some(err),
288 }
289 }
290}
291
292impl From<Refused> for EgressError {
293 fn from(refused: Refused) -> EgressError {
294 EgressError::Refused(refused)
295 }
296}
297
298fn classify(err: reqwest::Error) -> EgressError {
300 let mut source: Option<&(dyn std::error::Error + 'static)> = Some(&err);
301 while let Some(cause) = source {
302 if let Some(refused) = cause.downcast_ref::<Refused>() {
303 return EgressError::Refused(refused.clone());
304 }
305 source = cause.source();
306 }
307 if err.is_timeout() {
308 EgressError::Timeout
309 } else {
310 EgressError::Http(err.without_url())
311 }
312}
313
314pub fn check_url(url: &Url, allowance: Allowance) -> Result<(), Refused> {
317 match url.scheme() {
318 "https" => {}
319 "http" if is_loopback_host(url) => {}
320 "http" if allowance == Allowance::InternalNetwork && may_be_internal(url) => {}
321 other => return Err(Refused::Scheme(other.to_owned())),
322 }
323 if !url.username().is_empty() || url.password().is_some() {
324 return Err(Refused::Credentials);
325 }
326 let addr = match url.host() {
327 None => return Err(Refused::NoHost),
328 Some(Host::Domain(_)) => return Ok(()),
329 Some(Host::Ipv4(addr)) => IpAddr::V4(addr),
330 Some(Host::Ipv6(addr)) => IpAddr::V6(addr),
331 };
332 if allowance == Allowance::PublicOnly && !is_public(addr) {
333 return Err(Refused::Address(addr));
334 }
335 Ok(())
336}
337
338pub fn is_https_or_loopback(url: &Url) -> bool {
344 match url.scheme() {
345 "https" => true,
346 "http" => is_loopback_host(url),
347 _ => false,
348 }
349}
350
351#[derive(Debug)]
353pub enum BodyError {
354 TooLarge,
356 Failed(reqwest::Error),
359}
360
361impl fmt::Display for BodyError {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 match self {
364 BodyError::TooLarge => f.write_str("the response body is over its limit"),
365 BodyError::Failed(err) => write!(f, "{err}"),
366 }
367 }
368}
369
370impl std::error::Error for BodyError {}
371
372pub async fn read_capped(mut response: Response, max: usize) -> Result<Vec<u8>, BodyError> {
378 if response.content_length().is_some_and(|len| len > max as u64) {
379 return Err(BodyError::TooLarge);
380 }
381 let mut out = Vec::new();
382 while let Some(chunk) = response
383 .chunk()
384 .await
385 .map_err(|err| BodyError::Failed(err.without_url()))?
386 {
387 if out.len() + chunk.len() > max {
388 return Err(BodyError::TooLarge);
389 }
390 out.extend_from_slice(&chunk);
391 }
392 Ok(out)
393}
394
395fn is_loopback_host(url: &Url) -> bool {
396 match url.host() {
397 Some(Host::Domain(name)) => is_localhost(name),
398 Some(Host::Ipv4(addr)) => addr.is_loopback(),
399 Some(Host::Ipv6(addr)) => addr.is_loopback(),
400 None => false,
401 }
402}
403
404fn may_be_internal(url: &Url) -> bool {
408 match url.host() {
409 Some(Host::Domain(_)) => true,
410 Some(Host::Ipv4(addr)) => is_internal(IpAddr::V4(addr)),
411 Some(Host::Ipv6(addr)) => is_internal(IpAddr::V6(addr)),
412 None => false,
413 }
414}
415
416const V4_INTERNAL: &[(Ipv4Addr, u8)] = &[
419 (Ipv4Addr::new(10, 0, 0, 0), 8), (Ipv4Addr::new(127, 0, 0, 0), 8), (Ipv4Addr::new(172, 16, 0, 0), 12), (Ipv4Addr::new(192, 168, 0, 0), 16), ];
424
425const V6_INTERNAL: &[(Ipv6Addr, u8)] = &[
428 (Ipv6Addr::LOCALHOST, 128), (Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7), ];
431
432const V6_METADATA: Ipv6Addr = Ipv6Addr::new(0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x0254);
436
437fn is_internal(addr: IpAddr) -> bool {
440 match addr {
441 IpAddr::V4(addr) => V4_INTERNAL.iter().any(|&range| in_v4(addr, range)),
442 IpAddr::V6(addr) => addr != V6_METADATA && V6_INTERNAL.iter().any(|&range| in_v6(addr, range)),
443 }
444}
445
446const V4_NOT_PUBLIC: &[(Ipv4Addr, u8)] = &[
448 (Ipv4Addr::new(0, 0, 0, 0), 8), (Ipv4Addr::new(10, 0, 0, 0), 8), (Ipv4Addr::new(100, 64, 0, 0), 10), (Ipv4Addr::new(127, 0, 0, 0), 8), (Ipv4Addr::new(169, 254, 0, 0), 16), (Ipv4Addr::new(172, 16, 0, 0), 12), (Ipv4Addr::new(192, 0, 0, 0), 24), (Ipv4Addr::new(192, 0, 2, 0), 24), (Ipv4Addr::new(192, 88, 99, 0), 24), (Ipv4Addr::new(192, 168, 0, 0), 16), (Ipv4Addr::new(198, 18, 0, 0), 15), (Ipv4Addr::new(198, 51, 100, 0), 24), (Ipv4Addr::new(203, 0, 113, 0), 24), (Ipv4Addr::new(224, 0, 0, 0), 4), (Ipv4Addr::new(240, 0, 0, 0), 4), ];
464
465const V6_GLOBAL: (Ipv6Addr, u8) = (Ipv6Addr::new(0x2000, 0, 0, 0, 0, 0, 0, 0), 3);
467
468const V6_NOT_PUBLIC: &[(Ipv6Addr, u8)] = &[
470 (Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 23), (Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32), (Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16), (Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0), 20), ];
475
476fn in_v4(addr: Ipv4Addr, (net, len): (Ipv4Addr, u8)) -> bool {
477 let mask = u32::MAX.checked_shl(32 - u32::from(len)).unwrap_or(0);
478 u32::from(addr) & mask == u32::from(net) & mask
479}
480
481fn in_v6(addr: Ipv6Addr, (net, len): (Ipv6Addr, u8)) -> bool {
482 let mask = u128::MAX.checked_shl(128 - u32::from(len)).unwrap_or(0);
483 u128::from(addr) & mask == u128::from(net) & mask
484}
485
486pub fn is_public(addr: IpAddr) -> bool {
488 match addr {
489 IpAddr::V4(addr) => !V4_NOT_PUBLIC.iter().any(|&range| in_v4(addr, range)),
490 IpAddr::V6(addr) => in_v6(addr, V6_GLOBAL) && !V6_NOT_PUBLIC.iter().any(|&range| in_v6(addr, range)),
491 }
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496enum Reach {
497 Public,
499 Any,
501 Internal,
503}
504
505fn checked(host: &str, addrs: Vec<SocketAddr>, reach: Reach) -> Result<Vec<SocketAddr>, Refused> {
507 let permitted = |addr: &SocketAddr| match reach {
508 Reach::Public => is_public(addr.ip()),
509 Reach::Any => true,
510 Reach::Internal => is_internal(addr.ip()),
511 };
512 if let Some(addr) = addrs.iter().find(|addr| !permitted(addr)) {
513 return Err(Refused::Resolved {
514 host: host.to_owned(),
515 addr: addr.ip(),
516 });
517 }
518 Ok(addrs)
519}
520
521fn is_localhost(name: &str) -> bool {
523 name.trim_end_matches('.').eq_ignore_ascii_case("localhost")
524}
525
526const LOCALHOST: [SocketAddr; 2] = [
530 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
531 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
532];
533
534type SystemLookup = fn(String) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>>;
536
537fn system_lookup(host: String) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>> {
539 Box::pin(async move { Ok(tokio::net::lookup_host((host.as_str(), 0)).await?.collect()) })
540}
541
542async fn lookup(host: &str, system: SystemLookup) -> std::io::Result<Vec<SocketAddr>> {
545 if is_localhost(host) {
546 return Ok(LOCALHOST.to_vec());
547 }
548 system(host.to_owned()).await
549}
550
551struct CheckedResolver {
553 reach: Reach,
554 system: SystemLookup,
555}
556
557impl Resolve for CheckedResolver {
558 fn resolve(&self, name: Name) -> Resolving {
559 let (reach, system) = (self.reach, self.system);
560 Box::pin(async move {
561 let host = name.as_str().to_owned();
562 let addrs = lookup(&host, system).await?;
563 let addrs = checked(&host, addrs, reach)?;
564 Ok(Box::new(addrs.into_iter()) as Addrs)
565 })
566 }
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub struct Busy;
572
573impl fmt::Display for Busy {
574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575 f.write_str("too many requests in flight for this caller")
576 }
577}
578
579impl std::error::Error for Busy {}
580
581#[derive(Debug, Clone)]
585pub struct Limiter {
586 cap: usize,
587 in_use: Arc<Mutex<HashMap<String, usize>>>,
588}
589
590impl Limiter {
591 pub fn new(cap: usize) -> Limiter {
592 Limiter {
593 cap,
594 in_use: Arc::default(),
595 }
596 }
597
598 pub fn try_acquire(&self, key: &str) -> Result<Permit, Busy> {
601 let mut in_use = self.in_use.lock().unwrap_or_else(PoisonError::into_inner);
602 let held = in_use.get(key).copied().unwrap_or(0);
603 if held >= self.cap {
604 return Err(Busy);
605 }
606 in_use.insert(key.to_owned(), held + 1);
607 Ok(Permit {
608 key: key.to_owned(),
609 in_use: self.in_use.clone(),
610 })
611 }
612}
613
614#[derive(Debug)]
616#[must_use = "the permit is released when dropped"]
617pub struct Permit {
618 key: String,
619 in_use: Arc<Mutex<HashMap<String, usize>>>,
620}
621
622impl Drop for Permit {
623 fn drop(&mut self) {
624 let mut in_use = self.in_use.lock().unwrap_or_else(PoisonError::into_inner);
625 if let Some(held) = in_use.get_mut(&self.key) {
626 *held -= 1;
627 if *held == 0 {
628 in_use.remove(&self.key);
629 }
630 }
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 fn refused(url: &str) -> bool {
639 let url = Url::parse(url).unwrap();
640 let public = check_url(&url, Allowance::PublicOnly);
641 assert_eq!(check_url(&url, Allowance::InternalNetwork), Ok(()), "{url}");
642 matches!(public, Err(Refused::Address(_)))
643 }
644
645 #[test]
648 fn non_public_literals_are_refused() {
649 for url in [
650 "https://0.0.0.0/",
651 "https://0.1.2.3/",
652 "https://10.0.0.1/",
653 "https://100.64.0.1/",
654 "https://100.127.255.254/",
655 "https://127.0.0.1/",
656 "https://127.255.255.254/",
657 "https://169.254.169.254/",
658 "https://172.16.0.1/",
659 "https://172.31.255.254/",
660 "https://192.0.0.1/",
661 "https://192.0.2.1/",
662 "https://192.88.99.1/",
663 "https://192.168.1.1/",
664 "https://198.18.0.1/",
665 "https://198.19.255.254/",
666 "https://198.51.100.1/",
667 "https://203.0.113.1/",
668 "https://224.0.0.1/",
669 "https://239.255.255.250/",
670 "https://240.0.0.1/",
671 "https://255.255.255.255/",
672 "https://[::]/",
673 "https://[::1]/",
674 "https://[::ffff:127.0.0.1]/",
675 "https://[::ffff:10.0.0.1]/",
676 "https://[::ffff:8.8.8.8]/",
677 "https://[::127.0.0.1]/",
678 "https://[64:ff9b::7f00:1]/",
679 "https://[64:ff9b::808:808]/",
680 "https://[64:ff9b:1::1]/",
681 "https://[100::1]/",
682 "https://[2001::1]/",
683 "https://[2001:0:4136:e378:8000:63bf:3fff:fdd2]/",
684 "https://[2001:2::1]/",
685 "https://[2001:db8::1]/",
686 "https://[2002:c0a8:101::1]/",
687 "https://[2002:808:808::1]/",
688 "https://[3fff::1]/",
689 "https://[5f00::1]/",
690 "https://[fc00::1]/",
691 "https://[fd12:3456::1]/",
692 "https://[fe80::1]/",
693 "https://[fec0::1]/",
694 "https://[ff02::1]/",
695 ] {
696 assert!(refused(url), "{url} was not refused");
697 }
698 }
699
700 #[test]
703 fn odd_ipv4_spellings_are_refused_as_the_address_they_name() {
704 for url in [
705 "http://2130706433/",
706 "http://0x7f000001/",
707 "http://0x7f.1/",
708 "http://0177.0.0.1/",
709 "http://127.1/",
710 "http://127.0.0.1./",
711 "https://0/",
712 ] {
713 let parsed = Url::parse(url).unwrap();
714 assert!(
715 matches!(parsed.host(), Some(Host::Ipv4(_))),
716 "{url} -> {:?}",
717 parsed.host()
718 );
719 assert!(refused(url), "{url} was not refused");
720 }
721 assert_eq!(
722 Url::parse("http://2130706433/").unwrap().host(),
723 Some(Host::Ipv4(Ipv4Addr::LOCALHOST))
724 );
725 }
726
727 #[test]
731 fn public_literals_and_names_pass() {
732 for url in [
733 "https://8.8.8.8/",
734 "https://1.1.1.1/",
735 "https://100.63.255.255/",
736 "https://100.128.0.0/",
737 "https://172.15.255.255/",
738 "https://172.32.0.0/",
739 "https://198.17.255.255/",
740 "https://198.20.0.0/",
741 "https://223.255.255.255/",
742 "https://[2606:4700:4700::1111]/",
743 "https://[2001:200::1]/",
744 "https://[2a00:1450:4001::1]/",
745 "https://example.com/",
746 ] {
747 let parsed = Url::parse(url).unwrap();
748 assert_eq!(check_url(&parsed, Allowance::PublicOnly), Ok(()), "{url}");
749 }
750 }
751
752 #[test]
755 fn plain_http_only_to_loopback() {
756 for url in [
757 "http://127.0.0.1/",
758 "http://[::1]/",
759 "http://localhost/",
760 "http://LOCALHOST./",
761 ] {
762 let parsed = Url::parse(url).unwrap();
763 assert_eq!(check_url(&parsed, Allowance::InternalNetwork), Ok(()), "{url}");
764 assert!(
767 !matches!(check_url(&parsed, Allowance::PublicOnly), Err(Refused::Scheme(_))),
768 "{url}"
769 );
770 }
771 for url in [
772 "http://example.com/",
773 "http://10.0.0.1/",
774 "http://localhost.example.com/",
775 "http://8.8.8.8/",
776 ] {
777 let parsed = Url::parse(url).unwrap();
778 assert!(
779 matches!(check_url(&parsed, Allowance::PublicOnly), Err(Refused::Scheme(_))),
780 "{url}"
781 );
782 }
783 for url in ["ws://localhost/", "ws://10.0.0.1/", "file:///etc/passwd"] {
784 let parsed = Url::parse(url).unwrap();
785 for allowance in [Allowance::PublicOnly, Allowance::InternalNetwork] {
786 assert!(
787 matches!(check_url(&parsed, allowance), Err(Refused::Scheme(_))),
788 "{url} {allowance:?}"
789 );
790 }
791 }
792 }
793
794 #[test]
800 fn plain_http_under_internal_network_only_to_internal_addresses() {
801 for url in [
802 "http://10.0.0.1/",
803 "http://10.255.255.254/",
804 "http://127.0.0.2/",
805 "http://127.255.255.254/",
806 "http://172.16.0.1/",
807 "http://172.31.255.254/",
808 "http://192.168.0.1/",
809 "http://192.168.255.254/",
810 "http://[::1]/",
811 "http://[fc00::1]/",
812 "http://[fdff:ffff::1]/",
813 "http://[fd00:ec2::253]/",
814 "http://[fd00:ec2::255]/",
815 "http://nas.lan/",
816 "http://example.com/",
817 ] {
818 let parsed = Url::parse(url).unwrap();
819 assert_eq!(check_url(&parsed, Allowance::InternalNetwork), Ok(()), "{url}");
820 if !is_loopback_host(&parsed) {
821 assert_eq!(
822 check_url(&parsed, Allowance::PublicOnly),
823 Err(Refused::Scheme("http".into())),
824 "{url}"
825 );
826 }
827 }
828 for url in [
829 "http://8.8.8.8/",
830 "http://9.255.255.255/",
831 "http://11.0.0.0/",
832 "http://126.255.255.255/",
833 "http://128.0.0.0/",
834 "http://100.64.0.1/",
835 "http://100.100.100.200/",
836 "http://169.254.169.254/",
837 "http://169.254.0.1/",
838 "http://[fd00:ec2::254]/",
839 "http://172.15.255.255/",
840 "http://172.32.0.0/",
841 "http://192.167.255.255/",
842 "http://192.169.0.0/",
843 "http://0.0.0.0/",
844 "http://192.0.2.1/",
845 "http://198.18.0.1/",
846 "http://224.0.0.1/",
847 "http://255.255.255.255/",
848 "http://[2606:4700:4700::1111]/",
849 "http://[::]/",
850 "http://[::2]/",
851 "http://[fbff:ffff::1]/",
852 "http://[fe00::]/",
853 "http://[fe80::1]/",
854 "http://[fec0::1]/",
855 "http://[ff02::1]/",
856 "http://[::ffff:10.0.0.1]/",
857 "http://[::ffff:8.8.8.8]/",
858 "http://[64:ff9b::a00:1]/",
859 "http://[2002:a00:1::1]/",
860 "http://[2001:0:4136:e378:8000:63bf:3fff:fdd2]/",
861 ] {
862 let parsed = Url::parse(url).unwrap();
863 for allowance in [Allowance::PublicOnly, Allowance::InternalNetwork] {
864 assert_eq!(
865 check_url(&parsed, allowance),
866 Err(Refused::Scheme("http".into())),
867 "{url} {allowance:?}"
868 );
869 }
870 }
871 for (url, refusal) in [
873 ("http://user@10.0.0.1/", Refused::Credentials),
874 ("http://user:secret@nas.lan/", Refused::Credentials),
875 ("ws://10.0.0.1/", Refused::Scheme("ws".into())),
876 ("ftp://nas.lan/", Refused::Scheme("ftp".into())),
877 ] {
878 let parsed = Url::parse(url).unwrap();
879 assert_eq!(check_url(&parsed, Allowance::InternalNetwork), Err(refusal), "{url}");
880 }
881 }
882
883 #[test]
886 fn a_name_for_plain_http_needs_every_address_internal() {
887 let lan: SocketAddr = "192.168.1.10:0".parse().unwrap();
888 let ula: SocketAddr = "[fd12:3456::1]:0".parse().unwrap();
889 let public: SocketAddr = "93.184.215.14:0".parse().unwrap();
890 let doc: SocketAddr = "192.0.2.1:0".parse().unwrap();
891 let mapped: SocketAddr = "[::ffff:10.0.0.1]:0".parse().unwrap();
892 let metadata: SocketAddr = "[fd00:ec2::254]:0".parse().unwrap();
893 assert_eq!(checked("nas.lan", vec![lan, ula], Reach::Internal), Ok(vec![lan, ula]));
894 for addrs in [
895 vec![public],
896 vec![lan, public],
897 vec![public, lan],
898 vec![lan, doc],
899 vec![lan, mapped],
900 vec![ula, metadata],
901 vec![metadata],
902 ] {
903 let Err(Refused::Resolved { host, addr }) = checked("nas.lan", addrs.clone(), Reach::Internal) else {
904 panic!("{addrs:?} was not refused");
905 };
906 assert_eq!(host, "nas.lan");
907 assert!(!is_internal(addr), "{addr}");
908 }
909 }
910
911 fn table(host: String) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>> {
913 Box::pin(async move {
914 let addrs: &[&str] = match host.as_str() {
915 "lan.test" => &["127.0.0.1:0"],
916 "mixed.test" => &["127.0.0.1:0", "192.0.2.1:0"],
917 "public.test" => &["192.0.2.1:0"],
918 _ => return Err(std::io::Error::other("no such name")),
919 };
920 Ok(addrs.iter().map(|addr| addr.parse().unwrap()).collect())
921 })
922 }
923
924 #[tokio::test]
931 async fn plain_http_to_a_name_reaches_only_internal_addresses() {
932 use std::sync::atomic::{AtomicUsize, Ordering};
933 use tokio::io::{AsyncReadExt, AsyncWriteExt};
934 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
935 let port = listener.local_addr().unwrap().port();
936 let accepted = Arc::new(AtomicUsize::new(0));
937 let count = accepted.clone();
938 tokio::spawn(async move {
939 while let Ok((mut stream, _)) = listener.accept().await {
940 count.fetch_add(1, Ordering::SeqCst);
941 tokio::spawn(async move {
942 let mut buf = [0u8; 1024];
943 let _ = stream.read(&mut buf).await;
944 let _ = stream
945 .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\nconnection: close\r\n\r\nok")
946 .await;
947 });
948 }
949 });
950 let timeouts = Timeouts {
951 connect: Duration::from_secs(2),
952 request: Duration::from_secs(5),
953 };
954 let client = EgressClient::build_with(Allowance::InternalNetwork, timeouts, table).unwrap();
955 let get = |host: &str| Request::new(Method::GET, Url::parse(&format!("http://{host}:{port}/x")).unwrap());
956 let response = client.send(get("lan.test")).await.unwrap();
957 assert_eq!(response.text().await.unwrap(), "ok");
958 assert_eq!(accepted.load(Ordering::SeqCst), 1);
959 for host in ["mixed.test", "public.test"] {
960 for streaming in [false, true] {
961 let sent = if streaming {
962 client.send_streaming(get(host)).await
963 } else {
964 client.send(get(host)).await
965 };
966 let Err(EgressError::Refused(Refused::Resolved { addr, .. })) = sent else {
967 panic!("{host}: {sent:?}");
968 };
969 assert_eq!(addr, "192.0.2.1".parse::<IpAddr>().unwrap());
970 }
971 }
972 assert_eq!(accepted.load(Ordering::SeqCst), 1, "a refused name connected");
973 let https = Request::new(
977 Method::GET,
978 Url::parse(&format!("https://mixed.test:{port}/x")).unwrap(),
979 );
980 let sent = client.send(https).await;
981 assert!(matches!(sent, Err(EgressError::Http(_))), "{sent:?}");
982 assert_eq!(accepted.load(Ordering::SeqCst), 2, "https did not connect");
983 }
984
985 #[test]
987 fn a_name_with_any_non_public_address_is_refused() {
988 let public: SocketAddr = "93.184.215.14:0".parse().unwrap();
989 let private: SocketAddr = "10.0.0.1:0".parse().unwrap();
990 let mapped: SocketAddr = "[::ffff:127.0.0.1]:0".parse().unwrap();
991 assert_eq!(checked("example.com", vec![public], Reach::Public), Ok(vec![public]));
992 for addrs in [vec![public, private], vec![private, public], vec![public, mapped]] {
993 let Err(Refused::Resolved { host, addr }) = checked("example.com", addrs.clone(), Reach::Public) else {
994 panic!("{addrs:?} was not refused");
995 };
996 assert_eq!(host, "example.com");
997 assert!(!is_public(addr));
998 assert_eq!(checked("example.com", addrs.clone(), Reach::Any), Ok(addrs));
999 }
1000 }
1001
1002 #[tokio::test]
1006 async fn localhost_is_loopback_without_a_lookup() {
1007 fn no_lookup(host: String) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>> {
1008 panic!("{host} was looked up")
1009 }
1010 for reach in [Reach::Any, Reach::Internal, Reach::Public] {
1011 let resolver = CheckedResolver {
1012 reach,
1013 system: no_lookup,
1014 };
1015 for name in ["localhost", "localhost.", "LocalHost."] {
1016 match resolver.resolve(name.parse().unwrap()).await {
1017 Ok(addrs) => {
1018 assert_ne!(reach, Reach::Public, "{name}");
1019 assert_eq!(addrs.collect::<Vec<_>>(), LOCALHOST.to_vec(), "{name}");
1020 }
1021 Err(err) => {
1022 assert_eq!(reach, Reach::Public, "{name}: {err}");
1023 assert!(err.downcast_ref::<Refused>().is_some(), "{name}: {err}");
1024 }
1025 }
1026 }
1027 }
1028 assert!(!is_localhost("localhost.example.com"));
1029 assert!(!is_localhost("foo.localhost"));
1030 let Err(Refused::Resolved { .. }) = checked("localhost", LOCALHOST.to_vec(), Reach::Public) else {
1032 panic!("localhost was not refused");
1033 };
1034 }
1035
1036 const _: fn() = || {
1039 fn send_sync<T: Send + Sync + 'static>() {}
1040 send_sync::<Egress>();
1041 send_sync::<EgressClient>();
1042 send_sync::<Limiter>();
1043 send_sync::<Permit>();
1044 send_sync::<EgressError>();
1045 };
1046
1047 #[test]
1048 fn a_limiter_refuses_past_its_cap_per_key_and_forgets_released_keys() {
1049 let limiter = Limiter::new(2);
1050 let a1 = limiter.try_acquire("a").unwrap();
1051 let a2 = limiter.try_acquire("a").unwrap();
1052 assert_eq!(limiter.try_acquire("a").unwrap_err(), Busy);
1053 let b1 = limiter.clone().try_acquire("b").unwrap();
1055 assert_eq!(limiter.clone().try_acquire("a").unwrap_err(), Busy);
1056 drop(a1);
1057 let a3 = limiter.try_acquire("a").unwrap();
1058 assert_eq!(limiter.try_acquire("a").unwrap_err(), Busy);
1059 drop((a2, a3, b1));
1060 assert!(limiter.in_use.lock().unwrap().is_empty(), "released keys are forgotten");
1061 let none = Limiter::new(0);
1063 assert_eq!(none.try_acquire("a").unwrap_err(), Busy);
1064 assert!(none.in_use.lock().unwrap().is_empty());
1065 }
1066}