1use crate::{
6 Layer, Service,
7 cli::ForwardKind,
8 combinators::Either,
9 combinators::Either7,
10 error::{BoxError, OpaqueError},
11 extensions::{ExtensionsMut, ExtensionsRef},
12 http::{
13 Request, Response, StatusCode,
14 headers::exotic::XClacksOverhead,
15 headers::forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
16 headers::{Accept, HeaderMapExt},
17 layer::{
18 forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
19 set_header::SetResponseHeaderLayer, trace::TraceLayer,
20 },
21 mime,
22 server::HttpServer,
23 service::web::response::{Html, IntoResponse, Json, Redirect},
24 },
25 layer::limit::policy::UnlimitedPolicy,
26 layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
27 net::forwarded::Forwarded,
28 net::stream::{SocketInfo, layer::http::BodyLimitLayer},
29 proxy::haproxy::server::HaProxyLayer,
30 rt::Executor,
31 stream::Stream,
32 tcp::TcpStream,
33 telemetry::tracing,
34};
35
36#[cfg(all(feature = "rustls", not(feature = "boring")))]
37use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
38
39#[cfg(any(feature = "rustls", feature = "boring"))]
40use crate::http::headers::StrictTransportSecurity;
41
42#[cfg(feature = "boring")]
43use crate::{
44 net::tls::server::ServerConfig,
45 tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
46};
47
48#[cfg(feature = "boring")]
49type TlsConfig = ServerConfig;
50
51#[cfg(all(feature = "rustls", not(feature = "boring")))]
52type TlsConfig = TlsAcceptorData;
53
54use std::{convert::Infallible, marker::PhantomData, net::IpAddr, time::Duration};
55use tokio::io::AsyncWriteExt;
56
57#[derive(Debug, Clone)]
58pub struct IpServiceBuilder<M> {
61 #[cfg(any(feature = "rustls", feature = "boring"))]
62 tls_server_config: Option<TlsConfig>,
63 concurrent_limit: usize,
64 timeout: Duration,
65 forward: Option<ForwardKind>,
66 _mode: PhantomData<fn(M)>,
67}
68
69impl IpServiceBuilder<mode::Http> {
70 #[must_use]
72 pub fn http() -> Self {
73 Self {
74 #[cfg(any(feature = "rustls", feature = "boring"))]
75 tls_server_config: None,
76 concurrent_limit: 0,
77 timeout: Duration::ZERO,
78 forward: None,
79 _mode: PhantomData,
80 }
81 }
82}
83
84impl IpServiceBuilder<mode::Transport> {
85 #[must_use]
87 pub fn tcp() -> Self {
88 Self {
89 #[cfg(any(feature = "rustls", feature = "boring"))]
90 tls_server_config: None,
91 concurrent_limit: 0,
92 timeout: Duration::ZERO,
93 forward: None,
94 _mode: PhantomData,
95 }
96 }
97}
98
99impl<M> IpServiceBuilder<M> {
100 crate::utils::macros::generate_set_and_with! {
101 #[must_use]
103 pub fn concurrent(mut self, limit: usize) -> Self {
104 self.concurrent_limit = limit;
105 self
106 }
107 }
108
109 crate::utils::macros::generate_set_and_with! {
110 #[must_use]
112 pub fn timeout(mut self, timeout: Duration) -> Self {
113 self.timeout = timeout;
114 self
115 }
116 }
117
118 crate::utils::macros::generate_set_and_with! {
119 #[must_use]
131 pub fn forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
132 self.forward = maybe_kind;
133 self
134 }
135 }
136
137 crate::utils::macros::generate_set_and_with! {
138 #[cfg(any(feature = "rustls", feature = "boring"))]
139 pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
142 self.tls_server_config = cfg;
143 self
144 }
145 }
146}
147
148impl IpServiceBuilder<mode::Http> {
149 #[allow(unused_mut)]
150 #[inline]
151 pub fn build(
153 mut self,
154 executor: Executor,
155 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
156 #[cfg(all(feature = "rustls", not(feature = "boring")))]
157 let tls_cfg = self.tls_server_config.take();
158
159 #[cfg(feature = "boring")]
160 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
161 Some(cfg) => Some(cfg.try_into()?),
162 None => None,
163 };
164
165 #[cfg(any(feature = "rustls", feature = "boring"))]
166 {
167 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
168 self.build_http(executor, maybe_tls_acceptor_layer)
169 }
170
171 #[cfg(not(any(feature = "rustls", feature = "boring")))]
172 self.build_http(executor)
173 }
174}
175
176#[derive(Debug, Clone)]
177#[non_exhaustive]
178struct HttpIpService;
180
181impl Service<Request> for HttpIpService {
182 type Output = Response;
183 type Error = BoxError;
184
185 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
186 let norm_req_path = req.uri().path().trim_matches('/');
187 if !norm_req_path.is_empty() {
188 tracing::debug!("unexpected request path '{norm_req_path}', redirect to root");
189 return Ok(Redirect::permanent("/").into_response());
190 }
191
192 let peer_ip = req
193 .extensions()
194 .get::<Forwarded>()
195 .and_then(|f| f.client_ip())
196 .or_else(|| {
197 req.extensions()
198 .get::<SocketInfo>()
199 .map(|s| s.peer_addr().ip())
200 });
201
202 Ok(match peer_ip {
203 Some(ip) => match HttpBodyContentFormat::derive_from_req(&req) {
204 HttpBodyContentFormat::Txt => ip.to_string().into_response(),
205 HttpBodyContentFormat::Html => format_html_page(ip).into_response(),
206 HttpBodyContentFormat::Json => Json(serde_json::json!({
207 "ip": ip,
208 }))
209 .into_response(),
210 },
211 None => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
212 })
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
217enum HttpBodyContentFormat {
218 #[default]
219 Txt,
220 Html,
221 Json,
222}
223
224impl HttpBodyContentFormat {
225 fn derive_from_req(req: &Request) -> Self {
226 let Some(accept) = req.headers().typed_get::<Accept>() else {
227 return Self::default();
228 };
229 accept
230 .0
231 .iter()
232 .find_map(|qv| {
233 let r#type = qv.value.subtype();
234 if r#type == mime::JSON {
235 Some(Self::Json)
236 } else if r#type == mime::HTML {
237 Some(Self::Html)
238 } else if r#type == mime::TEXT {
239 Some(Self::Txt)
240 } else {
241 None
242 }
243 })
244 .unwrap_or_default()
245 }
246}
247
248#[derive(Debug, Clone)]
249#[non_exhaustive]
250struct TcpIpService;
252
253impl<Input> Service<Input> for TcpIpService
254where
255 Input: Stream + Unpin + ExtensionsRef,
256{
257 type Output = ();
258 type Error = BoxError;
259
260 async fn serve(&self, stream: Input) -> Result<Self::Output, Self::Error> {
261 tracing::info!("connection received");
262 let peer_ip = stream
263 .extensions()
264 .get::<Forwarded>()
265 .and_then(|f| f.client_ip())
266 .or_else(|| {
267 stream
268 .extensions()
269 .get::<SocketInfo>()
270 .map(|s| s.peer_addr().ip())
271 });
272 let Some(peer_ip) = peer_ip else {
273 tracing::error!("missing peer information");
274 return Ok(());
275 };
276
277 let mut stream = std::pin::pin!(stream);
278
279 match peer_ip {
280 std::net::IpAddr::V4(ip) => {
281 if let Err(err) = stream.write_all(&ip.octets()).await {
282 tracing::error!("error writing IPv4 of peer to peer: {}", err);
283 }
284 }
285 std::net::IpAddr::V6(ip) => {
286 if let Err(err) = stream.write_all(&ip.octets()).await {
287 tracing::error!("error writing IPv6 of peer to peer: {}", err);
288 }
289 }
290 };
291
292 Ok(())
293 }
294}
295
296impl IpServiceBuilder<mode::Transport> {
297 #[allow(unused_mut)]
298 #[inline]
299 pub fn build(
301 mut self,
302 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
303 #[cfg(all(feature = "rustls", not(feature = "boring")))]
304 let tls_cfg = self.tls_server_config.take();
305
306 #[cfg(feature = "boring")]
307 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config.take() {
308 Some(cfg) => Some(cfg.try_into()?),
309 None => None,
310 };
311
312 #[cfg(any(feature = "rustls", feature = "boring"))]
313 {
314 let maybe_tls_acceptor_layer = tls_cfg.map(TlsAcceptorLayer::new);
315 self.build_tcp(maybe_tls_acceptor_layer)
316 }
317
318 #[cfg(not(any(feature = "rustls", feature = "boring")))]
319 self.build_tcp()
320 }
321}
322
323impl<M> IpServiceBuilder<M> {
324 fn build_tcp<S: Stream + ExtensionsMut + Unpin + Send + Sync + 'static>(
325 self,
326 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
327 TlsAcceptorLayer,
328 >,
329 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
330 let tcp_forwarded_layer = match &self.forward {
331 None => None,
332 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
333 Some(other) => {
334 return Err(OpaqueError::from_display(format!(
335 "invalid forward kind for Transport mode: {other:?}"
336 ))
337 .into());
338 }
339 };
340
341 let tcp_service_builder = (
342 ConsumeErrLayer::trace(tracing::Level::DEBUG),
343 LimitLayer::new(if self.concurrent_limit > 0 {
344 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
345 } else {
346 Either::B(UnlimitedPolicy::new())
347 }),
348 if !self.timeout.is_zero() {
349 TimeoutLayer::new(self.timeout)
350 } else {
351 TimeoutLayer::never()
352 },
353 tcp_forwarded_layer,
354 #[cfg(any(feature = "rustls", feature = "boring"))]
355 maybe_tls_accept_layer,
356 );
357
358 Ok(tcp_service_builder.into_layer(TcpIpService))
359 }
360
361 fn build_http<S: Stream + Unpin + Send + Sync + ExtensionsMut + 'static>(
362 self,
363 executor: Executor,
364 #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option<
365 TlsAcceptorLayer,
366 >,
367 ) -> Result<impl Service<S, Output = (), Error = Infallible>, BoxError> {
368 let (tcp_forwarded_layer, http_forwarded_layer) = match &self.forward {
369 None => (None, None),
370 Some(ForwardKind::Forwarded) => {
371 (None, Some(Either7::A(GetForwardedHeaderLayer::forwarded())))
372 }
373 Some(ForwardKind::XForwardedFor) => (
374 None,
375 Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for())),
376 ),
377 Some(ForwardKind::XClientIp) => (
378 None,
379 Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new())),
380 ),
381 Some(ForwardKind::ClientIp) => (
382 None,
383 Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new())),
384 ),
385 Some(ForwardKind::XRealIp) => (
386 None,
387 Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new())),
388 ),
389 Some(ForwardKind::CFConnectingIp) => (
390 None,
391 Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new())),
392 ),
393 Some(ForwardKind::TrueClientIp) => (
394 None,
395 Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new())),
396 ),
397 Some(ForwardKind::HaProxy) => (Some(HaProxyLayer::default()), None),
398 };
399
400 #[cfg(any(feature = "rustls", feature = "boring"))]
401 let hsts_layer = maybe_tls_accept_layer.is_some().then(|| {
402 SetResponseHeaderLayer::if_not_present_typed(
403 StrictTransportSecurity::excluding_subdomains_for_max_seconds(31536000),
404 )
405 });
406
407 let tcp_service_builder = (
408 ConsumeErrLayer::trace(tracing::Level::DEBUG),
409 (self.concurrent_limit > 0)
410 .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit))),
411 (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout)),
412 tcp_forwarded_layer,
413 BodyLimitLayer::request_only(1024 * 1024),
415 #[cfg(any(feature = "rustls", feature = "boring"))]
416 maybe_tls_accept_layer,
417 );
418
419 let http_service = (
420 TraceLayer::new_for_http(),
421 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
422 AddRequiredResponseHeadersLayer::default(),
423 ConsumeErrLayer::default(),
424 #[cfg(any(feature = "rustls", feature = "boring"))]
425 hsts_layer,
426 http_forwarded_layer,
427 )
428 .into_layer(HttpIpService);
429
430 Ok(tcp_service_builder.into_layer(HttpServer::auto(executor).service(http_service)))
431 }
432}
433
434pub mod mode {
435 #[derive(Debug, Clone)]
438 #[non_exhaustive]
439 pub struct Http;
441
442 #[derive(Debug, Clone)]
443 #[non_exhaustive]
444 pub struct Transport;
446}
447
448fn format_html_page(ip: IpAddr) -> Html<String> {
449 Html(format!(
450 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>"##,
451 ))
452}