fix(auth): real client IP rate-limiting + remote default-password guard

- Add custom-server.js: inject unspoofable socket IP, strip client XFF
  (wired into Docker CMD + CLI spawn + build-cli copy)
- loginLimiter: key on trusted x-9r-real-ip, TRUST_PROXY opt-in, global fallback
- Force password change on first remote login while default is in use
- Add /api/auth/reset-password (local-only) so CLI reset writes live SQLite
- CLI settings: reset via API instead of stale db.json
- Fix OAuth modals opening duplicate browser tabs on add-connection
- Add cli:pack / cli:publish scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-08 12:10:02 +07:00
co-authored by Cursor
parent c572c68717
commit 7648c3412b
17 changed files with 185 additions and 44 deletions
+11 -3
View File
@@ -46,7 +46,15 @@ export function recordSuccess(ip) {
}
export function getClientIp(request) {
const xff = request.headers.get("x-forwarded-for");
if (xff) return xff.split(",")[0].trim();
return request.headers.get("x-real-ip") || "unknown";
// Trusted: set from TCP socket by custom-server.js (client cannot spoof).
const realIp = request.headers.get("x-9r-real-ip");
if (realIp) return realIp;
// Behind a trusted reverse proxy that overwrites XFF with the real client IP.
if (process.env.TRUST_PROXY === "true") {
const xff = request.headers.get("x-forwarded-for");
if (xff) return xff.split(",")[0].trim();
}
// Direct exposure without custom-server: single bucket so spoofed XFF
// rotation cannot escape the limiter.
return "unknown";
}
+2
View File
@@ -31,6 +31,8 @@ export {
isTailscaleLoggedIn,
isTailscaleLoggedInStrict,
isSystemDaemonRunning,
isDaemonAlive,
startFunnel,
getTailscaleBin,
installTailscale,
startLogin,
+7 -1
View File
@@ -519,6 +519,11 @@ function isDaemonTunMode() {
} catch { return null; }
}
/** Daemon process alive (independent of funnel state) — mirrors cloudflared PID check semantic. */
export function isDaemonAlive() {
return isDaemonTunMode() !== null;
}
/**
* Start tailscaled.
* - With sudoPassword: TUN mode (root) → Funnel TLS works
@@ -550,8 +555,9 @@ export async function startDaemonWithPassword(sudoPassword) {
return;
}
const wantTun = !!sudoPassword;
const currentMode = isDaemonTunMode(); // true=TUN, false=userspace, null=not running
// No password but a healthy TUN daemon already runs → keep TUN, never downgrade-kill it.
const wantTun = sudoPassword ? true : currentMode === true;
// Daemon already running in correct mode → reuse
if (currentMode !== null && currentMode === wantTun) {