1#![expect(
6 clippy::allow_attributes,
7 reason = "feature-gated `mut self` consumed by some cfg branches but not others β `#[allow(unused_mut)]` would warn unfulfilled in the cfg arm where it IS used"
8)]
9
10use std::{convert::Infallible, marker::PhantomData, net::IpAddr, sync::Arc, time::Duration};
11
12use tokio::io::AsyncWriteExt;
13
14use crate::{
15 Layer, Service,
16 cli::ForwardKind,
17 combinators::{Either, Either3, Either7},
18 error::{BoxError, BoxErrorExt, ErrorExt as _},
19 extensions::ExtensionsRef,
20 http::{
21 BodyLimitLayer, Request, Response, StatusCode, Version,
22 headers::{
23 Accept, AltSvc, HeaderMapExt,
24 exotic::XClacksOverhead,
25 forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
26 },
27 layer::{
28 forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
29 set_header::SetResponseHeaderLayer, trace::TraceLayer,
30 },
31 mime,
32 server::HttpServer,
33 service::web::response::{Css, IntoResponse, Json, Redirect, Script},
34 },
35 io::Io,
36 layer::{
37 ConsumeErrLayer, LimitLayer, TimeoutLayer,
38 limit::policy::{ConcurrentPolicy, RateLimitReached, RatePolicy, UnlimitedPolicy},
39 },
40 net::{
41 address::ip::geo::{GeoLocation, IpGeoDb, IpGeoInfo},
42 stream::layer::{ThrottleLayer, ThrottleMode},
43 },
44 proxy::haproxy::server::HaProxyLayer,
45 quic,
46 rt::Executor,
47 tcp::TcpStream,
48 telemetry::tracing,
49 utils::{octets::mib, rate::Rate},
50};
51
52core::cfg_select! {
53 feature = "boring" => {
54 use crate::tls::boring::server::TlsAcceptorLayer;
55 }
56 feature = "rustls" => {
57 use crate::tls::rustls::server::TlsAcceptorLayer;
58 }
59 _ => {}
60}
61
62#[cfg(any(feature = "rustls", feature = "boring"))]
63use crate::{http::headers::StrictTransportSecurity, tls::server::TlsServerConfig};
64
65#[derive(Debug, Clone)]
66pub struct IpServiceBuilder<M> {
69 #[cfg(any(feature = "rustls", feature = "boring"))]
70 tls_server_config: Option<TlsServerConfig>,
71 concurrent_limit: usize,
72 rate_limit: Option<Rate>,
73 throttle: Option<Rate>,
74 timeout: Duration,
75 forward: Option<ForwardKind>,
76 geo_db: Option<Arc<IpGeoDb>>,
77 http_version: Option<Version>,
78 alt_svc: Option<AltSvc>,
79 _mode: PhantomData<fn(M)>,
80}
81
82impl IpServiceBuilder<mode::Http> {
83 #[must_use]
85 pub fn http() -> Self {
86 Self {
87 #[cfg(any(feature = "rustls", feature = "boring"))]
88 tls_server_config: None,
89 concurrent_limit: 0,
90 rate_limit: None,
91 throttle: None,
92 timeout: Duration::ZERO,
93 forward: None,
94 geo_db: None,
95 http_version: None,
96 alt_svc: None,
97 _mode: PhantomData,
98 }
99 }
100}
101
102impl IpServiceBuilder<mode::Transport> {
103 #[must_use]
105 pub fn tcp() -> Self {
106 Self {
107 #[cfg(any(feature = "rustls", feature = "boring"))]
108 tls_server_config: None,
109 concurrent_limit: 0,
110 rate_limit: None,
111 throttle: None,
112 timeout: Duration::ZERO,
113 forward: None,
114 geo_db: None,
115 http_version: None,
116 alt_svc: None,
117 _mode: PhantomData,
118 }
119 }
120}
121
122impl<M> IpServiceBuilder<M> {
123 crate::utils::macros::generate_set_and_with! {
124 #[must_use]
126 pub fn concurrent(mut self, limit: usize) -> Self {
127 self.concurrent_limit = limit;
128 self
129 }
130 }
131
132 crate::utils::macros::generate_set_and_with! {
133 #[must_use]
137 pub fn rate_limit(mut self, rate: Option<Rate>) -> Self {
138 self.rate_limit = rate;
139 self
140 }
141 }
142
143 crate::utils::macros::generate_set_and_with! {
144 #[must_use]
147 pub fn throttle(mut self, rate: Option<Rate>) -> Self {
148 self.throttle = rate;
149 self
150 }
151 }
152
153 crate::utils::macros::generate_set_and_with! {
154 #[must_use]
156 pub fn timeout(mut self, timeout: Duration) -> Self {
157 self.timeout = timeout;
158 self
159 }
160 }
161
162 crate::utils::macros::generate_set_and_with! {
163 #[must_use]
175 pub fn forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
176 self.forward = maybe_kind;
177 self
178 }
179 }
180
181 crate::utils::macros::generate_set_and_with! {
182 #[must_use]
185 pub fn geo_db(mut self, db: Option<Arc<IpGeoDb>>) -> Self {
186 self.geo_db = db;
187 self
188 }
189 }
190
191 crate::utils::macros::generate_set_and_with! {
192 #[cfg(any(feature = "rustls", feature = "boring"))]
193 pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
196 self.tls_server_config = cfg;
197 self
198 }
199 }
200}
201
202impl IpServiceBuilder<mode::Http> {
203 crate::utils::macros::generate_set_and_with! {
204 #[must_use]
206 pub fn http_version(mut self, version: Option<Version>) -> Self {
207 self.http_version = version;
208 self
209 }
210 }
211
212 crate::utils::macros::generate_set_and_with! {
213 #[must_use]
215 pub fn alt_svc(mut self, alt_svc: Option<AltSvc>) -> Self {
216 self.alt_svc = alt_svc;
217 self
218 }
219 }
220
221 #[inline]
222 pub fn build(
224 self,
225 executor: Executor,
226 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
227 self.build_with_http3(executor).map(|(tcp, _)| tcp)
228 }
229
230 pub fn build_with_http3(
237 self,
238 executor: Executor,
239 ) -> Result<
240 (
241 impl Service<TcpStream, Output = (), Error = Infallible>,
242 impl Service<quic::Connection, Output = (), Error = Infallible>,
243 ),
244 BoxError,
245 > {
246 #[cfg(any(feature = "rustls", feature = "boring"))]
247 let maybe_tls_accept_layer = self.tls_server_config.map(TlsAcceptorLayer::new);
248
249 let (tcp_forwarded_layer, http_forwarded_layer) = match &self.forward {
250 None => (None, None),
251 Some(ForwardKind::Forwarded) => {
252 (None, Some(Either7::A(GetForwardedHeaderLayer::forwarded())))
253 }
254 Some(ForwardKind::XForwardedFor) => (
255 None,
256 Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for())),
257 ),
258 Some(ForwardKind::XClientIp) => (
259 None,
260 Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new())),
261 ),
262 Some(ForwardKind::ClientIp) => (
263 None,
264 Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new())),
265 ),
266 Some(ForwardKind::XRealIp) => (
267 None,
268 Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new())),
269 ),
270 Some(ForwardKind::CFConnectingIp) => (
271 None,
272 Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new())),
273 ),
274 Some(ForwardKind::TrueClientIp) => (
275 None,
276 Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new())),
277 ),
278 Some(ForwardKind::HaProxy) => (Some(HaProxyLayer::default()), None),
279 };
280
281 #[cfg(any(feature = "rustls", feature = "boring"))]
282 let hsts_layer = maybe_tls_accept_layer.is_some().then(|| {
283 SetResponseHeaderLayer::if_not_present_typed(
284 StrictTransportSecurity::excluding_subdomains_for_max_seconds(31536000),
285 )
286 });
287
288 let connection_limit = (self.concurrent_limit > 0)
289 .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit)));
290 let connection_timeout = (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout));
291 let throttle = self
293 .throttle
294 .map(|rate| ThrottleLayer::symmetric(ThrottleMode::per_conn(rate)));
295 let tcp_service_builder = (
296 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
297 connection_limit.clone(),
298 connection_timeout.clone(),
299 throttle.clone(),
300 tcp_forwarded_layer,
301 BodyLimitLayer::request_only(mib(1)),
303 #[cfg(any(feature = "rustls", feature = "boring"))]
304 maybe_tls_accept_layer,
305 );
306
307 let (csp_layer, nosniff_layer, referrer_layer, frame_layer) =
315 crate::cli::service::http_security::defence_in_depth_layer(
316 crate::cli::service::http_security::rama_html_csp(),
317 );
318
319 let geo_attribution = self.geo_db.as_ref().and_then(|db| {
321 let notices: Vec<_> = db.attributions().collect();
322 (!notices.is_empty()).then(|| crate::cli::service::geo::geo_attribution_layer(notices))
323 });
324
325 let router = crate::http::service::web::Router::new()
329 .with_get(
330 "/",
331 HttpIpService {
332 geo_db: self.geo_db,
333 },
334 )
335 .with_get("/style/ip.css", Css(IP_STYLE_CSS))
336 .with_get("/script/ip.js", Script(IP_SCRIPT_JS))
337 .with_not_found(async || Redirect::permanent("/"));
338
339 let http_service = (
340 TraceLayer::new_for_http(),
341 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
342 self.alt_svc
343 .map(SetResponseHeaderLayer::if_not_present_typed),
344 AddRequiredResponseHeadersLayer::default(),
345 self.rate_limit.map(|rate| {
346 LimitLayer::new(RatePolicy::abort(rate)).with_error_into_response_fn(
347 |err: RateLimitReached| Ok::<_, Infallible>(err.into_response()),
348 )
349 }),
350 geo_attribution,
351 csp_layer,
352 nosniff_layer,
353 referrer_layer,
354 frame_layer,
355 ConsumeErrLayer::default(),
356 #[cfg(any(feature = "rustls", feature = "boring"))]
357 hsts_layer,
358 http_forwarded_layer,
359 )
360 .into_layer(router);
361
362 let http_service = Arc::new(http_service);
366 let http_transport_service = match self.http_version {
367 None => Either3::A(HttpServer::auto(executor.clone()).service(http_service.clone())),
368 Some(Version::HTTP_2) => {
369 Either3::B(HttpServer::new_h2(executor.clone()).service(http_service.clone()))
370 }
371 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
372 Either3::C(HttpServer::new_http1(executor.clone()).service(http_service.clone()))
373 }
374 Some(version) => {
375 return Err(BoxError::from_static_str("unsupported http version")
376 .context_debug_field("version", version));
377 }
378 };
379 let http3_service = (
380 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
381 connection_limit,
382 connection_timeout,
383 throttle,
384 BodyLimitLayer::request_only(mib(1)),
385 )
386 .into_layer(HttpServer::new_http3(executor).service(http_service));
387
388 Ok((
389 tcp_service_builder.into_layer(http_transport_service),
390 http3_service,
391 ))
392 }
393}
394
395#[derive(Debug, Clone)]
396struct HttpIpService {
401 geo_db: Option<Arc<IpGeoDb>>,
404}
405
406impl Service<Request> for HttpIpService {
407 type Output = Response;
408 type Error = Infallible;
409
410 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
411 let peer_ip = crate::net::client_ip::client_ip(&req);
412
413 Ok(match peer_ip {
414 Some(ip) => match HttpBodyContentFormat::derive_from_req(&req) {
415 HttpBodyContentFormat::Txt => ip.to_string().into_response(),
416 HttpBodyContentFormat::Html => {
417 let geo = self.geo_db.as_ref().and_then(|db| db.resolve(ip));
418 let attributions: Vec<_> = self
419 .geo_db
420 .as_ref()
421 .map(|db| db.attributions().collect())
422 .unwrap_or_default();
423 render_html_page(ip, geo.as_ref(), &attributions).into_response()
424 }
425 HttpBodyContentFormat::Json => {
426 let geo = self.geo_db.as_ref().and_then(|db| db.resolve(ip));
427 let mut body = serde_json::json!({ "ip": ip });
428 if let Some(info) = geo {
429 body["geo"] = serde_json::to_value(&info).unwrap_or_default();
431 }
432 Json(body).into_response()
433 }
434 },
435 None => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
436 })
437 }
438}
439
440const IP_STYLE_CSS: &str = include_str!("ip.css");
444
445const IP_SCRIPT_JS: &str = include_str!("ip.js");
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
450enum HttpBodyContentFormat {
451 #[default]
452 Txt,
453 Html,
454 Json,
455}
456
457impl HttpBodyContentFormat {
458 fn derive_from_req(req: &Request) -> Self {
459 let Some(accept) = req.headers().typed_get::<Accept>() else {
460 return Self::default();
461 };
462 let mut entries: Vec<_> = accept.0.iter().collect();
465 entries.sort_by_key(|qv| std::cmp::Reverse(qv.quality));
466 entries
467 .into_iter()
468 .find_map(|qv| {
469 let r#type = qv.value.subtype();
470 if r#type == mime::JSON {
471 Some(Self::Json)
472 } else if r#type == mime::HTML {
473 Some(Self::Html)
474 } else if r#type == mime::TEXT {
475 Some(Self::Txt)
476 } else {
477 None
478 }
479 })
480 .unwrap_or_default()
481 }
482}
483
484#[derive(Debug, Clone)]
485#[non_exhaustive]
486struct TcpIpService;
488
489impl<Input> Service<Input> for TcpIpService
490where
491 Input: Io + Unpin + ExtensionsRef,
492{
493 type Output = ();
494 type Error = BoxError;
495
496 async fn serve(&self, stream: Input) -> Result<Self::Output, Self::Error> {
497 tracing::info!("connection received");
498 let peer_ip = crate::net::client_ip::client_ip(&stream);
499 let Some(peer_ip) = peer_ip else {
500 tracing::error!("missing peer information");
501 return Ok(());
502 };
503
504 let mut stream = std::pin::pin!(stream);
505
506 match peer_ip {
507 std::net::IpAddr::V4(ip) => {
508 if let Err(err) = stream.write_all(&ip.octets()).await {
509 tracing::error!("error writing IPv4 of peer to peer: {}", err);
510 }
511 }
512 std::net::IpAddr::V6(ip) => {
513 if let Err(err) = stream.write_all(&ip.octets()).await {
514 tracing::error!("error writing IPv6 of peer to peer: {}", err);
515 }
516 }
517 };
518
519 Ok(())
520 }
521}
522
523impl IpServiceBuilder<mode::Transport> {
524 #[allow(unused_mut)]
525 #[inline]
526 pub fn build(
528 mut self,
529 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
530 #[cfg(any(feature = "rustls", feature = "boring"))]
531 {
532 let maybe_tls_acceptor_layer = self.tls_server_config.take().map(TlsAcceptorLayer::new);
533 self.build_tcp(maybe_tls_acceptor_layer)
534 }
535
536 #[cfg(not(any(feature = "rustls", feature = "boring")))]
537 self.build_tcp()
538 }
539}
540
541impl<M> IpServiceBuilder<M> {
542 fn build_tcp<S: Io + ExtensionsRef + Unpin + Sync>(
543 self,
544 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
545 TlsAcceptorLayer,
546 >,
547 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
548 let tcp_forwarded_layer = match &self.forward {
549 None => None,
550 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
551 Some(other) => {
552 return Err(
553 BoxError::from_static_str("invalid forward kind for Transport mode")
554 .with_context_debug_field("kind", || other.clone()),
555 );
556 }
557 };
558
559 let tcp_service_builder = (
560 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
561 self.rate_limit
562 .map(|rate| LimitLayer::new(RatePolicy::abort(rate))),
563 LimitLayer::new(if self.concurrent_limit > 0 {
564 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
565 } else {
566 Either::B(UnlimitedPolicy::new())
567 }),
568 if !self.timeout.is_zero() {
569 TimeoutLayer::new(self.timeout)
570 } else {
571 TimeoutLayer::never()
572 },
573 self.throttle
574 .map(|rate| ThrottleLayer::symmetric(ThrottleMode::per_conn(rate))),
575 tcp_forwarded_layer,
576 #[cfg(any(feature = "rustls", feature = "boring"))]
577 maybe_tls_accept_layer,
578 );
579
580 Ok(tcp_service_builder.into_layer(TcpIpService))
581 }
582}
583
584pub mod mode {
585 #[derive(Debug, Clone)]
588 #[non_exhaustive]
589 pub struct Http;
591
592 #[derive(Debug, Clone)]
593 #[non_exhaustive]
594 pub struct Transport;
596}
597
598fn render_html_page(
599 ip: IpAddr,
600 geo: Option<&IpGeoInfo>,
601 attributions: &[&str],
602) -> impl crate::http::protocols::html::IntoHtml + IntoResponse {
603 use crate::http::protocols::html::*;
604
605 let geo_comment =
607 crate::cli::service::geo::geo_attribution_html_comment(attributions).map(PreEscaped);
608 let geo_panel = geo.map(|info| {
609 let rows = |loc: &GeoLocation| {
610 crate::cli::service::geo::geo_location_rows(loc)
611 .into_iter()
612 .map(|(k, v)| div!(class = "georow", div!(class = "muted", k), div!(code!(v))))
613 .collect::<Vec<_>>()
614 };
615 let card = |label: String, loc: &GeoLocation| {
617 div!(
618 class = "panel geo-card",
619 div!(class = "muted geo-source", label),
620 rows(loc),
621 )
622 };
623 let mut cards = vec![card("merged".to_owned(), &info.location)];
624 cards.extend(
625 info.by_source
626 .iter()
627 .map(|src| card(src.label.to_string(), &src.location)),
628 );
629 div!(
630 class = "geo-section",
631 role = "region",
632 "aria-label" = "geo panel",
633 div!(class = "muted geo-title", "Geolocation"),
634 div!(class = "geo-grid", cards),
635 )
636 });
637
638 html!(
639 lang = "en",
640 head!(
641 meta!(charset = "utf-8"),
642 meta!(
643 name = "viewport",
644 content = "width=device-width,initial-scale=1"
645 ),
646 link!(
647 rel = "icon",
648 href = PreEscaped(
649 "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>\
650 <text y='0.9em' font-size='90'>π¦</text></svg>"
651 ),
652 ),
653 title!("Rama IP"),
654 link!(
655 rel = "stylesheet",
656 r#type = "text/css",
657 href = "/style/ip.css"
658 ),
659 ),
660 body!(
661 geo_comment,
662 div!(
663 class = "card",
664 div!(
665 class = "logo",
666 div!("π¦"),
667 div!(a!(href = "https://ramaproxy.org", "γ©γ")),
668 ),
669 div!(
670 class = "panel",
671 role = "region",
672 "aria-label" = "ip panel",
673 div!(class = "muted", "Your public ip"),
674 div!(id = "ip", class = "ip", code!(ip.to_string())),
675 div!(
676 class = "controls",
677 button!(
678 id = "copyBtn",
679 class = "primary",
680 title = "Copy ip to clipboard",
681 "π Copy IP",
682 ),
683 ),
684 ),
685 geo_panel,
686 script!(src = "/script/ip.js"),
687 )
688 ),
689 )
690}
691
692#[cfg(test)]
693mod render_html_page_tests {
694 use std::net::Ipv4Addr;
695
696 use super::*;
697 use crate::http::protocols::html::IntoHtml as _;
698
699 #[test]
704 fn render_html_page_embeds_ip_safely() {
705 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
706 let out = render_html_page(ip, None, &[]).into_string();
707 assert!(out.starts_with("<!DOCTYPE html><html lang=\"en\">"));
708 assert!(out.contains("<title>Rama IP</title>"));
709 assert!(out.contains(r#"<div id="ip" class="ip"><code>127.0.0.1</code></div>"#));
710 assert!(out.contains(r#"id="copyBtn""#));
712 }
713
714 #[test]
717 fn render_html_page_emits_aria_label() {
718 let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1));
719 let out = render_html_page(ip, None, &[]).into_string();
720 assert!(out.contains(r#"aria-label="ip panel""#));
721 }
722
723 #[test]
729 fn render_html_page_uses_external_assets() {
730 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
731 let out = render_html_page(ip, None, &[]).into_string();
732 assert!(
733 !out.contains("<style>") && !out.contains("<style "),
734 "IP page must not embed inline <style>; CSP blocks it"
735 );
736 assert!(
739 !out.contains("<script>"),
740 "IP page must not embed inline <script>; CSP blocks it"
741 );
742 assert!(
743 out.contains(r#"<link rel="stylesheet" type="text/css" href="/style/ip.css">"#),
744 "IP page must link to /style/ip.css",
745 );
746 assert!(
747 out.contains(r#"<script src="/script/ip.js">"#),
748 "IP page must source /script/ip.js",
749 );
750 }
751
752 #[test]
755 fn render_html_page_renders_geo_panel() {
756 use crate::{
757 geo::Country,
758 net::address::ip::geo::{GeoLocation, IpGeoInfo, IpGeoSourceResult},
759 };
760 let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
761 let loc = GeoLocation {
762 country: Some(Country::Belgium),
763 ..Default::default()
764 };
765 let info = IpGeoInfo {
766 ip,
767 location: loc.clone(),
768 by_source: vec![IpGeoSourceResult {
769 label: "geolite2".into(),
770 location: loc,
771 }],
772 };
773 let notices = ["This product includes GeoLite2 data created by MaxMind"];
774 let out = render_html_page(ip, Some(&info), ¬ices).into_string();
775 assert!(out.contains("Geolocation"), "geo panel title missing");
776 assert!(out.contains("Belgium"), "resolved country missing");
777 assert!(out.contains("geolite2"), "per-source label missing");
778 assert!(
780 out.contains("<!-- This product includes GeoLite2"),
781 "attribution comment missing"
782 );
783
784 let plain = render_html_page(ip, None, &[]).into_string();
786 assert!(!plain.contains("Geolocation"));
787 assert!(!plain.contains("<!--"));
788 }
789}