feat: /myip command + /v1/whoami endpoint

Server:
- GET /v1/whoami returns client IP, IPv4/IPv6 classification
- Detects proxy via X-Forwarded-For, X-Real-IP, Via headers
- Shows proxy details when behind reverse proxy (Caddy etc)
- ConnectInfo enabled for direct socket address

Web client:
- /myip, /whatsmyip, /ip — shows your IP + proxy info
- Useful for testing IPv4/IPv6 connectivity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-30 11:11:06 +04:00
parent 11133cf968
commit f272a82faf
3 changed files with 74 additions and 3 deletions

View File

@@ -248,7 +248,7 @@ async fn main() -> anyhow::Result<()> {
let listener = tokio::net::TcpListener::bind(&cli.bind).await?;
tracing::info!("Listening on {}", cli.bind);
axum::serve(listener, app).await?;
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()).await?;
Ok(())
}

View File

@@ -1,12 +1,66 @@
use axum::{routing::get, Json, Router};
use axum::{extract::ConnectInfo, http::HeaderMap, routing::get, Json, Router};
use serde_json::json;
use std::net::SocketAddr;
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new().route("/health", get(health))
Router::new()
.route("/health", get(health))
.route("/whoami", get(whoami))
}
async fn health() -> Json<serde_json::Value> {
Json(json!({ "status": "ok", "version": env!("CARGO_PKG_VERSION") }))
}
async fn whoami(
headers: HeaderMap,
connect_info: Option<ConnectInfo<SocketAddr>>,
) -> Json<serde_json::Value> {
// Prefer X-Forwarded-For (set by Caddy/reverse proxy), then X-Real-Ip, then direct
let forwarded = headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.map(|v| v.split(',').next().unwrap_or("").trim().to_string());
let real_ip = headers
.get("x-real-ip")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let direct = connect_info.map(|ci| ci.0.ip().to_string());
let ip = forwarded.clone()
.or(real_ip.clone())
.or(direct.clone())
.unwrap_or_else(|| "unknown".to_string());
// Classify as IPv4 or IPv6
let is_v6 = ip.contains(':');
let via = headers.get("via").and_then(|v| v.to_str().ok()).map(|s| s.to_string());
let proto = headers.get("x-forwarded-proto").and_then(|v| v.to_str().ok()).map(|s| s.to_string());
let host = headers.get("x-forwarded-host").and_then(|v| v.to_str().ok()).map(|s| s.to_string());
let behind_proxy = forwarded.is_some() || real_ip.is_some() || via.is_some();
let mut result = json!({
"ip": ip,
"version": if is_v6 { "IPv6" } else { "IPv4" },
"direct": direct,
"behind_proxy": behind_proxy,
});
if behind_proxy {
let proxy = json!({
"x_forwarded_for": forwarded,
"x_real_ip": real_ip,
"x_forwarded_proto": proto,
"x_forwarded_host": host,
"via": via,
});
result.as_object_mut().unwrap().insert("proxy".to_string(), proxy);
}
Json(result)
}

View File

@@ -1945,6 +1945,23 @@ async function doSend() {
return;
}
if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); return; }
if (text === '/myip' || text === '/whatsmyip' || text === '/ip') {
try {
const resp = await fetch(SERVER + '/v1/whoami');
const data = await resp.json();
addSys('Your IP: ' + data.ip + ' (' + data.version + ')');
if (data.behind_proxy && data.proxy) {
addSys(' Behind proxy: yes');
if (data.proxy.x_forwarded_for) addSys(' X-Forwarded-For: ' + data.proxy.x_forwarded_for);
if (data.proxy.x_real_ip) addSys(' X-Real-IP: ' + data.proxy.x_real_ip);
if (data.proxy.x_forwarded_proto) addSys(' Proto: ' + data.proxy.x_forwarded_proto);
if (data.proxy.x_forwarded_host) addSys(' Host: ' + data.proxy.x_forwarded_host);
if (data.proxy.via) addSys(' Via: ' + data.proxy.via);
}
if (data.direct) addSys(' Direct connection: ' + data.direct);
} catch(e) { addSys('Error: ' + e.message); }
return;
}
if (text === '/call') { startCall(); return; }
if (text === '/hangup' || text === '/end') { hangupCall(); return; }
if (text === '/accept') { acceptCall(); return; }