# frp-mtls.conf — nginx :8470 TLS-termination + device-CA mTLS + WS-upgrade proxy to frps vhost.
#
# Native mTLS tunnel · V4. Install target: /etc/nginx/conf.d/frp-mtls.conf
# This is a conf.d snippet (safe to ADD — it only introduces a NEW loopback :8470 server + one map;
# it does NOT touch the existing stream :443 map, which is edited separately per stream-sni-additions.md).
#
# Data-path security gate: `ssl_verify_client on` against the DEVICE CA (with CRL) is the ONE gate for
# the tunnelled base app. `on`, NEVER `optional` (PLAN P-D / R2). The public :8470 SERVER cert is the
# LE wildcard (V3); the CLIENT trust anchor is the private device CA (V2) — two different chains.
#
# nginx 1.24: there is NO standalone `http2` directive here and NO `http2 off;` — HTTP/2 is off by
# default on this listen and `http2 off;` is INVALID on 1.24 (would fail `nginx -t`, PLAN R7).
#
# A3 / FIX C-native-3 — cert→Host BINDING (the SINGLE load-bearing tenant-isolation control). On top
# of `ssl_verify_client on` (which only proves the cert is device-CA-signed), njs parses the leaf's
# dNSName SAN and a `map` requires its leftmost label == the leftmost label of the requested SNI,
# `return 403` otherwise. A cert stamped `alice.terminal.yaojia.wang` may ONLY reach `alice.*`; a
# tenant-A cert presented to tenant-B's host is denied. Rule lives in the `map` (never scattered
# `if`s); exactly one `if` guards the single `return 403`.
#
# PREREQ (VPS main nginx.conf, MAIN context — NOT here): `load_module modules/ngx_http_js_module.so;`
# (Debian nginx: `apt-get install nginx-module-njs`). Install the module file to /etc/nginx/njs/
# getCertSub.js so the relative `js_import` below resolves against the /etc/nginx prefix.

js_import certsub from njs/getCertSub.js;
js_set    $cert_sub certsub.getCertSub;   # leftmost dNSName SAN label of the verified client cert ('' on any parse failure)

# Allow iff the requested SNI is EXACTLY `<cert_sub>.terminal.yaojia.wang` — the cert's leftmost
# dNSName label equals the requested subdomain, anchored to the FULL zone. Anchoring the zone here
# (not just `<label>.`) makes this map SELF-SUFFICIENT: it does not depend on the `server_name` regex
# (line ~46) or the stream SNI routing to constrain the zone — belt-and-suspenders for the one control.
# `(?P=s)` is the PCRE named backreference to `s`. An empty $cert_sub (fail-closed njs output) can
# never satisfy `[^:]+`, so it always falls through to `default 0` → denied. Case-sensitive (~):
# our subdomains and stamped SANs are lowercase.
map "$cert_sub:$ssl_server_name" $cert_host_ok {
    "~^(?<s>[^:]+):(?P=s)\.terminal\.yaojia\.wang$"  1;
    default                                          0;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 127.0.0.1:8470 ssl;
    server_name ~^.+\.terminal\.yaojia\.wang$;

    ssl_certificate     /etc/relay/frp-tls/fullchain.pem;   # LE wildcard *.terminal.yaojia.wang (V3)
    ssl_certificate_key /etc/relay/frp-tls/privkey.pem;

    # --- the data-path security gate ---
    ssl_verify_client on;                                   # ON, never `optional`
    ssl_client_certificate /etc/relay/device-ca/device-ca.cert.pem;
    ssl_verify_depth 1;
    ssl_crl /etc/relay/device-ca/crl.pem;                   # revocation from day one (V2)

    # --- A3 cert→Host binding gate (runs AFTER ssl_verify_client) ---
    # The one `if` guarding the single `return`; the actual rule is in the $cert_host_ok map above.
    # Denies before ANY location is reached (protects /, /hook*, /live-sessions/:id/preview, …).
    if ($cert_host_ok = 0) { return 403; }

    location / {
        proxy_pass http://127.0.0.1:7080;                   # frps vhost — routes by Host → subdomain
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Origin https://$host;              # needed for Android/curl; harmless for iOS
        proxy_set_header X-Client-Cert-CN $ssl_client_s_dn;
        proxy_buffering off;
        proxy_read_timeout 3600s;                           # prevents idle-WS proxy kill (default 60s, R6)
        proxy_send_timeout 3600s;
    }
}
