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 crate::{
11 Layer, Service,
12 cli::ForwardKind,
13 combinators::Either,
14 combinators::Either7,
15 error::BoxError,
16 extensions::ExtensionsRef,
17 http::{
18 Request, Response, StatusCode,
19 headers::exotic::XClacksOverhead,
20 headers::forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
21 headers::{Accept, HeaderMapExt},
22 layer::{
23 forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
24 set_header::SetResponseHeaderLayer, trace::TraceLayer,
25 },
26 mime,
27 server::HttpServer,
28 service::web::response::{Html, IntoResponse, Json, Redirect},
29 },
30 io::Io,
31 layer::limit::policy::UnlimitedPolicy,
32 layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
33 net::forwarded::Forwarded,
34 net::stream::{SocketInfo, layer::http::BodyLimitLayer},
35 proxy::haproxy::server::HaProxyLayer,
36 rt::Executor,
37 tcp::TcpStream,
38 telemetry::tracing,
39};
40
41#[cfg(all(feature = "rustls", not(feature = "boring")))]
42use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
43
44#[cfg(any(feature = "rustls", feature = "boring"))]
45use crate::http::headers::StrictTransportSecurity;
46
47#[cfg(feature = "boring")]
48use crate::{
49 net::tls::server::ServerConfig,
50 tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
51};
52
53#[cfg(feature = "boring")]
54type TlsConfig = ServerConfig;
55
56#[cfg(all(feature = "rustls", not(feature = "boring")))]
57type TlsConfig = TlsAcceptorData;
58
59use rama_core::error::{ErrorExt as _, extra::OpaqueError};
60use std::{convert::Infallible, marker::PhantomData, net::IpAddr, time::Duration};
61use tokio::io::AsyncWriteExt;
62
63#[derive(Debug, Clone)]
64pub struct IpServiceBuilder<M> {
67 #[cfg(any(feature = "rustls", feature = "boring"))]
68 tls_server_config: Option<TlsConfig>,
69 concurrent_limit: usize,
70 timeout: Duration,
71 forward: Option<ForwardKind>,
72 _mode: PhantomData<fn(M)>,
73}
74
75impl IpServiceBuilder<mode::Http> {
76 #[must_use]
78 pub fn http() -> Self {
79 Self {
80 #[cfg(any(feature = "rustls", feature = "boring"))]
81 tls_server_config: None,
82 concurrent_limit: 0,
83 timeout: Duration::ZERO,
84 forward: None,
85 _mode: PhantomData,
86 }
87 }
88}
89
90impl IpServiceBuilder<mode::Transport> {
91 #[must_use]
93 pub fn tcp() -> Self {
94 Self {
95 #[cfg(any(feature = "rustls", feature = "boring"))]
96 tls_server_config: None,
97 concurrent_limit: 0,
98 timeout: Duration::ZERO,
99 forward: None,
100 _mode: PhantomData,
101 }
102 }
103}
104
105impl<M> IpServiceBuilder<M> {
106 crate::utils::macros::generate_set_and_with! {
107 #[must_use]
109 pub fn concurrent(mut self, limit: usize) -> Self {
110 self.concurrent_limit = limit;
111 self
112 }
113 }
114
115 crate::utils::macros::generate_set_and_with! {
116 #[must_use]
118 pub fn timeout(mut self, timeout: Duration) -> Self {
119 self.timeout = timeout;
120 self
121 }
122 }
123
124 crate::utils::macros::generate_set_and_with! {
125 #[must_use]
137 pub fn forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
138 self.forward = maybe_kind;
139 self
140 }
141 }
142
143 crate::utils::macros::generate_set_and_with! {
144 #[cfg(any(feature = "rustls", feature = "boring"))]
145 pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
148 self.tls_server_config = cfg;
149 self
150 }
151 }
152}
153
154impl IpServiceBuilder<mode::Http> {
155 #[allow(unused_mut)]
156 #[inline]
157 pub fn build(
159 mut self,
160 executor: Executor,
161 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
162 #[cfg(all(feature = "rustls", not(feature = "boring")))]
163 let tls_cfg = self.tls_server_config.take();
164
165 #[cfg(feature = "boring")]
166 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
167 Some(cfg) => Some(cfg.try_into()?),
168 None => None,
169 };
170
171 #[cfg(any(feature = "rustls", feature = "boring"))]
172 {
173 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
174 self.build_http(executor, maybe_tls_acceptor_layer)
175 }
176
177 #[cfg(not(any(feature = "rustls", feature = "boring")))]
178 self.build_http(executor)
179 }
180}
181
182#[derive(Debug, Clone)]
183#[non_exhaustive]
184struct HttpIpService;
186
187impl Service<Request> for HttpIpService {
188 type Output = Response;
189 type Error = BoxError;
190
191 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
192 let norm_req_path = req.uri().path().trim_matches('/');
193 if !norm_req_path.is_empty() {
194 tracing::debug!("unexpected request path '{norm_req_path}', redirect to root");
195 return Ok(Redirect::permanent("/").into_response());
196 }
197
198 let peer_ip = req
199 .extensions()
200 .get_ref::<Forwarded>()
201 .and_then(|f| f.client_ip())
202 .or_else(|| {
203 req.extensions()
204 .get_ref::<SocketInfo>()
205 .map(|s| s.peer_addr().ip_addr)
206 });
207
208 Ok(match peer_ip {
209 Some(ip) => match HttpBodyContentFormat::derive_from_req(&req) {
210 HttpBodyContentFormat::Txt => ip.to_string().into_response(),
211 HttpBodyContentFormat::Html => format_html_page(ip).into_response(),
212 HttpBodyContentFormat::Json => Json(serde_json::json!({
213 "ip": ip,
214 }))
215 .into_response(),
216 },
217 None => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
218 })
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223enum HttpBodyContentFormat {
224 #[default]
225 Txt,
226 Html,
227 Json,
228}
229
230impl HttpBodyContentFormat {
231 fn derive_from_req(req: &Request) -> Self {
232 let Some(accept) = req.headers().typed_get::<Accept>() else {
233 return Self::default();
234 };
235 accept
236 .0
237 .iter()
238 .find_map(|qv| {
239 let r#type = qv.value.subtype();
240 if r#type == mime::JSON {
241 Some(Self::Json)
242 } else if r#type == mime::HTML {
243 Some(Self::Html)
244 } else if r#type == mime::TEXT {
245 Some(Self::Txt)
246 } else {
247 None
248 }
249 })
250 .unwrap_or_default()
251 }
252}
253
254#[derive(Debug, Clone)]
255#[non_exhaustive]
256struct TcpIpService;
258
259impl<Input> Service<Input> for TcpIpService
260where
261 Input: Io + Unpin + ExtensionsRef,
262{
263 type Output = ();
264 type Error = BoxError;
265
266 async fn serve(&self, stream: Input) -> Result<Self::Output, Self::Error> {
267 tracing::info!("connection received");
268 let peer_ip = stream
269 .extensions()
270 .get_ref::<Forwarded>()
271 .and_then(|f| f.client_ip())
272 .or_else(|| {
273 stream
274 .extensions()
275 .get_ref::<SocketInfo>()
276 .map(|s| s.peer_addr().ip_addr)
277 });
278 let Some(peer_ip) = peer_ip else {
279 tracing::error!("missing peer information");
280 return Ok(());
281 };
282
283 let mut stream = std::pin::pin!(stream);
284
285 match peer_ip {
286 std::net::IpAddr::V4(ip) => {
287 if let Err(err) = stream.write_all(&ip.octets()).await {
288 tracing::error!("error writing IPv4 of peer to peer: {}", err);
289 }
290 }
291 std::net::IpAddr::V6(ip) => {
292 if let Err(err) = stream.write_all(&ip.octets()).await {
293 tracing::error!("error writing IPv6 of peer to peer: {}", err);
294 }
295 }
296 };
297
298 Ok(())
299 }
300}
301
302impl IpServiceBuilder<mode::Transport> {
303 #[allow(unused_mut)]
304 #[inline]
305 pub fn build(
307 mut self,
308 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
309 #[cfg(all(feature = "rustls", not(feature = "boring")))]
310 let tls_cfg = self.tls_server_config.take();
311
312 #[cfg(feature = "boring")]
313 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
314 Some(cfg) => Some(cfg.try_into()?),
315 None => None,
316 };
317
318 #[cfg(any(feature = "rustls", feature = "boring"))]
319 {
320 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
321 self.build_tcp(maybe_tls_acceptor_layer)
322 }
323
324 #[cfg(not(any(feature = "rustls", feature = "boring")))]
325 self.build_tcp()
326 }
327}
328
329impl<M> IpServiceBuilder<M> {
330 fn build_tcp<S: Io + ExtensionsRef + Unpin + Sync>(
331 self,
332 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
333 TlsAcceptorLayer,
334 >,
335 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
336 let tcp_forwarded_layer = match &self.forward {
337 None => None,
338 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
339 Some(other) => {
340 return Err(OpaqueError::from_static_str(
341 "invalid forward kind for Transport mode",
342 )
343 .with_context_debug_field("kind", || other.clone()));
344 }
345 };
346
347 let tcp_service_builder = (
348 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
349 LimitLayer::new(if self.concurrent_limit > 0 {
350 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
351 } else {
352 Either::B(UnlimitedPolicy::new())
353 }),
354 if !self.timeout.is_zero() {
355 TimeoutLayer::new(self.timeout)
356 } else {
357 TimeoutLayer::never()
358 },
359 tcp_forwarded_layer,
360 #[cfg(any(feature = "rustls", feature = "boring"))]
361 maybe_tls_accept_layer,
362 );
363
364 Ok(tcp_service_builder.into_layer(TcpIpService))
365 }
366
367 fn build_http<S: Io + Unpin + Sync + ExtensionsRef>(
368 self,
369 executor: Executor,
370 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
371 TlsAcceptorLayer,
372 >,
373 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
374 let (tcp_forwarded_layer, http_forwarded_layer) = match &self.forward {
375 None => (None, None),
376 Some(ForwardKind::Forwarded) => {
377 (None, Some(Either7::A(GetForwardedHeaderLayer::forwarded())))
378 }
379 Some(ForwardKind::XForwardedFor) => (
380 None,
381 Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for())),
382 ),
383 Some(ForwardKind::XClientIp) => (
384 None,
385 Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new())),
386 ),
387 Some(ForwardKind::ClientIp) => (
388 None,
389 Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new())),
390 ),
391 Some(ForwardKind::XRealIp) => (
392 None,
393 Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new())),
394 ),
395 Some(ForwardKind::CFConnectingIp) => (
396 None,
397 Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new())),
398 ),
399 Some(ForwardKind::TrueClientIp) => (
400 None,
401 Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new())),
402 ),
403 Some(ForwardKind::HaProxy) => (Some(HaProxyLayer::default()), None),
404 };
405
406 #[cfg(any(feature = "rustls", feature = "boring"))]
407 let hsts_layer = maybe_tls_accept_layer.is_some().then(|| {
408 SetResponseHeaderLayer::if_not_present_typed(
409 StrictTransportSecurity::excluding_subdomains_for_max_seconds(31536000),
410 )
411 });
412
413 let tcp_service_builder = (
414 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
415 (self.concurrent_limit > 0)
416 .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit))),
417 (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout)),
418 tcp_forwarded_layer,
419 BodyLimitLayer::request_only(1024 * 1024),
421 #[cfg(any(feature = "rustls", feature = "boring"))]
422 maybe_tls_accept_layer,
423 );
424
425 let http_service = (
426 TraceLayer::new_for_http(),
427 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
428 AddRequiredResponseHeadersLayer::default(),
429 ConsumeErrLayer::default(),
430 #[cfg(any(feature = "rustls", feature = "boring"))]
431 hsts_layer,
432 http_forwarded_layer,
433 )
434 .into_layer(HttpIpService);
435
436 Ok(tcp_service_builder.into_layer(HttpServer::auto(executor).service(http_service)))
437 }
438}
439
440pub mod mode {
441 #[derive(Debug, Clone)]
444 #[non_exhaustive]
445 pub struct Http;
447
448 #[derive(Debug, Clone)]
449 #[non_exhaustive]
450 pub struct Transport;
452}
453
454fn format_html_page(ip: IpAddr) -> Html<String> {
455 Html(format!(
456 r##"<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🦙</text></svg>" /> <title>Rama IP</title> <style> *, *::before, *::after {{ box-sizing: border-box; }} :root{{ --bg:#000; --panel:#0f0f0f; --green:#45d23a; --muted:#bfbfbf; }} html,body{{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;}} body{{ background:var(--bg); color:var(--muted); display:flex; align-items:center; justify-content:center; padding:2.8rem; }} .card{{ text-align:center; }} .logo{{ display:flex; align-items:center; justify-content:center; gap:0.8rem; margin-bottom:1.1rem; }} .logo, .logo a, .logo a:hover {{ color:var(--green); font-weight:700; font-size:2rem; letter-spacing:0.4rem; }} .logo a {{ text-decoration: none; }} .logo a:hover {{ text-decoration: underline; }} .subtitle{{ font-size:1.1rem; margin:0.3rem 0 2rem 0; color:var(--muted); }} .panel{{ background:linear-gradient(180deg,#0b0b0b 0%, #111 100%); border-radius:0.8rem; padding:2rem; box-shadow:0 0.3rem 2rem rgba(0,0,0,0.7), inset 0 0.05rem 0 rgba(255,255,255,0.02); border:0.1rem solid rgba(69,210,58,0.06); }} .ip{{ background:transparent; border-radius:0.6rem; padding:1rem 1.1rem; font-family: ui-monospace,SFMono-Regular,Menlo,monospace; font-size:1.1rem; color:#fff139; margin:0.6rem auto 1.1rem auto; word-break:break-all; border:0.05rem solid rgba(69,210,58,0.12); }} .muted{{ color:var(--muted); font-size:1rem; margin-bottom:0.9rem; }} .controls{{display:flex;gap:0.8rem;justify-content:center;flex-wrap:wrap;}} button{{ background:transparent; color:var(--green); padding:0.8rem 1.1rem; border-radius:0.6rem; font-weight:700; border:0.1rem solid rgba(69,210,58,0.9); cursor:pointer; }} button.primary{{ background:var(--green); color:#032; box-shadow:0 0.4rem 1.2rem rgba(69,210,58,0.08); }} .note{{font-size:0.95rem;color:#9aa; margin-top:1rem;}} .small{{font-size:0.9rem;color:#808080;margin-top:0.7rem}} </style> </head> <body> <div class="card"> <div class="logo"> <div>🦙</div> <div><a href="https://ramaproxy.org">ラマ</a></div> </div> <div class="panel" role="region" aria-label="ip panel"> <div class="muted">Your public ip</div><div id="ip" class="ip"> <code>{ip}</code> </div> <div class="controls"> <button id="copyBtn" class="primary" title="Copy ip to clipboard">📋 Copy IP</button></div> </div> <script> (async function(){{ const ipEl = document.getElementById('ip'); const copyBtn = document.getElementById('copyBtn'); copyBtn.addEventListener('click', async ()=>{{ const txt = ipEl.textContent.trim(); try{{ await navigator.clipboard.writeText(txt); copyBtn.textContent = 'Copied'; setTimeout(()=> copyBtn.textContent = 'Copy IP', 1400); }}catch(e){{ const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta); ta.select(); try{{ document.execCommand('copy'); copyBtn.textContent = 'Copied'; }} catch(e){{ alert('Copy failed. Select and copy manually.'); }} ta.remove(); setTimeout(()=> copyBtn.textContent = 'Copy IP', 1400); }} }}); }})(); </script> </body> </html>"##,
457 ))
458}