// ── Trust proxy configuration ────────────────────────────────────────────── // // Real request path for this deployment: // // client → Pangolin → newt tunnel agent ("ptero", separate VM) // → this app (its own VM), over the LAN // // The hop that actually opens the TCP connection to this app is ptero, so from // Express's point of view ptero is THE trusted proxy and its LAN IP is the // right-most/most-recently-added entry to reconcile against. The real client IP // arrives in X-Forwarded-For. req.ip / req.secure must reflect the real client // because the rate limiter, exponential backoff, bot-scoring ban, and activity // log all key on req.ip — so this is a prerequisite for every other control. // // Recommended production value: pin TRUST_PROXY to ptero's LAN IP exactly. That // is stricter than a hop count: Express will only honour XFF on connections that // actually come from ptero, so nothing else on the LAN can inject a forwarded // header. See TRUST_PROXY in .env.example for how to set it. // // IMPORTANT ASSUMPTION: pinning ptero's IP assumes ptero holds a STATIC IP // (a DHCP reservation in Omada). If that reservation does not exist, a lease // change would silently move ptero to a new IP and every XFF would stop being // trusted — req.ip would collapse to ptero's (new) address for all clients, // breaking rate limiting/bans. Verify the reservation before relying on this, // and use DEBUG_TRUST_PROXY (below) to re-check the observed proxy IP without a // code redeploy if it ever needs to change. // // We deliberately DO NOT use a blanket `true`. `true` trusts every hop and takes // the left-most (client-supplied, spoofable) XFF entry, letting an attacker forge // their apparent IP to dodge rate limits / bans. // // TRUST_PROXY accepts: // - unset / '' -> 1 (single proxy hop fallback) // - an integer -> that many trusted hops (e.g. "2") // - "false" -> false (no proxy; direct connections only) // - "loopback" etc. -> the express preset string, passed through // - a CSV of IPs / CIDRs -> that list (e.g. ptero's LAN IP: "10.0.0.42") // // `true` is intentionally rejected (coerced to 1) with a warning, so it can't be // set by accident. const log = require('./logger')('trustproxy') function truthyEnv(v) { return ['1', 'true', 'yes', 'on'].includes(String(v || '').trim().toLowerCase()) } const PRESETS = new Set(['loopback', 'linklocal', 'uniquelocal']) function parseTrustProxy(raw = process.env.TRUST_PROXY) { const val = (raw == null ? '' : String(raw)).trim() if (val === '') return 1 // default: one hop (Pangolin) if (val.toLowerCase() === 'false') return false if (val.toLowerCase() === 'true') { log.warn('TRUST_PROXY=true is unsafe (trusts spoofable client XFF); using 1 hop instead') return 1 } // Pure integer → hop count. if (/^\d+$/.test(val)) return Number(val) // Single express preset keyword. if (PRESETS.has(val.toLowerCase())) return val.toLowerCase() // Otherwise treat as a comma-separated list of trusted IPs / CIDRs (and/or // preset keywords), which Express accepts as an array. const list = val .split(',') .map((s) => s.trim()) .filter(Boolean) return list.length === 1 ? list[0] : list } // Apply the setting to an Express app and log what was chosen. function applyTrustProxy(app, raw = process.env.TRUST_PROXY) { const setting = parseTrustProxy(raw) app.set('trust proxy', setting) log.info('trust proxy configured', { setting: Array.isArray(setting) ? setting.join(',') : setting }) return setting } // Temporary diagnostic middleware, OFF by default. Set DEBUG_TRUST_PROXY=1 to // log, per request, the raw peer address and forwarded header alongside the IP // Express resolved — so the real proxy IP (ptero) can be re-verified in-place // without a code change if it ever moves. Mounted before the bot guard so it // still fires for scanner/junk requests (whose source IPs are what we want to // see). Turn it back off once verified; it is noisy. function trustProxyDebug(req, res, next) { if (truthyEnv(process.env.DEBUG_TRUST_PROXY)) { log.info('trust-proxy debug', { remoteAddress: req.socket && req.socket.remoteAddress, xForwardedFor: req.headers['x-forwarded-for'] || null, resolvedIp: req.ip, method: req.method, path: req.originalUrl, }) } return next() } module.exports = { parseTrustProxy, applyTrustProxy, trustProxyDebug }