File size: 3,591 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { randomUUID } from "node:crypto";

/**
 * Trusted peer-IP stamping for the custom Node HTTP servers.
 *
 * The Next.js middleware runtime (proxy.ts β†’ runAuthzPipeline) exposes NO socket
 * or peer IP β€” only request headers, ALL of which are client-controlled. The
 * LOCAL_ONLY route guard (spawn-capable routes) must decide locality from the
 * real TCP peer, never from the spoofable Host header.
 *
 * Our custom servers DO have the real `req.socket.remoteAddress`. They stamp it
 * into PEER_IP_HEADER as `<token>|<ip>`, where <token> is a per-process secret
 * (OMNIROUTE_PEER_STAMP_TOKEN). Any client-supplied value of PEER_IP_HEADER is
 * deleted first, so a remote caller cannot pre-populate it. The middleware
 * (src/server/authz/policies/management.ts β†’ resolveStampedPeer) trusts the IP
 * ONLY when the token matches this process's secret; otherwise it fails closed.
 *
 * Keep PEER_IP_HEADER in sync with PEER_IP_HEADER in
 * src/server/authz/headers.ts (the TS side cannot import this .mjs).
 */
export const PEER_IP_HEADER = "x-omniroute-peer-ip";

/**
 * Companion header to PEER_IP_HEADER: `<token>|1` when the inbound TCP request
 * carried forwarding headers (`x-forwarded-for` / `x-real-ip`), `<token>|0`
 * otherwise. Required so the middleware can tell that a loopback socket is the
 * reverse-proxy hop (nginx / Caddy / Cloudflare Tunnel) and NOT trust it as
 * local β€” without this, a leaked JWT over a public tunnel would reach the
 * LOCAL_ONLY routes that spawn child processes (Hard Rules #15 + #17;
 * port of upstream decolua/9router commit da667836).
 *
 * Keep VIA_PROXY_HEADER in sync with VIA_PROXY_HEADER in
 * src/server/authz/headers.ts (the TS side cannot import this .mjs).
 */
export const VIA_PROXY_HEADER = "x-omniroute-via-proxy";

/** Generate (once) and return the per-process stamp token, persisting it in env
 *  so the middleware running in the same process reads the identical value. */
export function ensurePeerStampToken() {
  process.env.OMNIROUTE_PEER_STAMP_TOKEN ||= randomUUID();
  return process.env.OMNIROUTE_PEER_STAMP_TOKEN;
}

/** Strip any client-supplied PEER_IP_HEADER + VIA_PROXY_HEADER and stamp the
 *  real TCP peer IP plus a token-protected via-proxy marker. Never throws β€” a
 *  stamping failure must not block a request (it degrades to "locality
 *  unknown" β†’ fail closed in the middleware). */
export function stampPeerIp(req) {
  try {
    if (!req || !req.headers) return;
    // Node lowercases incoming header names; delete kills any client value.
    delete req.headers[PEER_IP_HEADER];
    delete req.headers[VIA_PROXY_HEADER];
    const ip = req.socket && req.socket.remoteAddress;
    if (ip) {
      const token = ensurePeerStampToken();
      req.headers[PEER_IP_HEADER] = `${token}|${ip}`;
      // Forwarding headers present = request arrived via a reverse proxy; the
      // loopback socket is the proxy hop, not the end-user, so it must not be
      // trusted as local. Token-prefix the marker so a remote caller cannot
      // forge it (or its absence) on a non-proxied request.
      const viaProxy = !!(req.headers["x-forwarded-for"] || req.headers["x-real-ip"]);
      req.headers[VIA_PROXY_HEADER] = `${token}|${viaProxy ? "1" : "0"}`;
    }
  } catch {
    /* never block a request on peer stamping */
  }
}

/** Wrap a Node request listener so every request is peer-stamped first. */
export function wrapRequestListenerWithPeerStamp(listener) {
  return function peerStampingRequestHandler(req, res) {
    stampPeerIp(req);
    return listener.call(this, req, res);
  };
}