feat(bns): 'p' reverse-proxy record type — mirror an upstream URL under a BNS name

New record kind alongside h / s3 / ip / u: `p` reverse-proxies the
request to a full upstream URL while keeping the BNS host in the
address bar. Unlike `ip` (which pins the upstream to a raw IP + on-chain
TLS fingerprint), `p` uses the upstream's own DNS + public CA cert and
sends `Host:` of the upstream so vhost-based origins answer correctly.

Argus (record-picker.js):
- Placed AFTER `ip` in the apex chain: a name carrying both keeps its
  existing pinned-IP behavior; names with only `p` get honored instead
  of falling through to `u` and 302-ing away.
- Not applied to subdomain inheritance — `p` is single-URL by intent,
  and inheriting through a subdomain would silently mangle the target.

Theseus (main.js serveBns):
- Fetches the upstream (path prefix from the `p` URL is preserved) and
  returns the response body/status/content-type verbatim. Provenance
  label "mirror" appears in the source badge.

Tests: record-picker.test.mjs covers `p`-only, `p`+ip precedence, and
the subdomain-suppression rule.
This commit is contained in:
Local Dev 2026-09-07 00:18:07 +02:00
parent f15752fe4b
commit 408f965629
2 changed files with 50 additions and 1 deletions

View file

@ -26,11 +26,21 @@
/**
* Decide which record wins.
*
* `p` semantics — added 2026-08-31. "Reverse-proxy this name to the URL in
* the `p` record; address bar stays on the BNS name." Complementary to `ip`
* (which pins the upstream to a raw IP + on-chain TLS fingerprint): `p` uses
* the upstream's own DNS + public CA cert. Placed AFTER `ip` in the apex chain
* so a name carrying both keeps its pre-existing pinned-IP behavior; names
* that ONLY carry `p` get honored instead of falling through to `u` and 302-ing
* away. Not applied to subdomain inheritance — `p` is single-URL by intent, so
* inheriting it through a subdomain query would silently mangle the target.
*
* @param {object} records `entry.records` from the resolver
* @param {object} opts
* @param {boolean} opts.isSubdomain true iff the requested name has labels
* above the registered `<label>.<tld>` (so we're serving via inheritance).
* @returns {'ip'|'h'|'s3'|'u'|null} key of the winning record, or null if
* @returns {'ip'|'h'|'s3'|'p'|'u'|null} key of the winning record, or null if
* nothing serviceable is present.
*/
export function pickRecord(records, { isSubdomain } = { isSubdomain: false }) {
@ -39,6 +49,7 @@ export function pickRecord(records, { isSubdomain } = { isSubdomain: false }) {
if (records.h) return "h";
if (records.s3) return "s3";
if (records.ip) return "ip";
if (!isSubdomain && records.p) return "p";
if (records.u) return "u";
return null;
}

View file

@ -113,6 +113,44 @@ test("unrelated fields (tls, r, arbitrary) do not affect the pick", () => {
assert.equal(pickRecord(records, { isSubdomain: true }), "ip");
});
// ---------- pickRecord: the `p` record (reverse proxy), added 2026-08-31 ----------
test("apex with ONLY p → picks p (not fall through to u or null)", () => {
// The reason `p` was added: a name that just wants to mirror an upstream
// via a full URL. Before, this fell through the ordering unnoticed and
// the gateway JSON-dumped the entry.
assert.equal(pickRecord({ p: "https://syskypo.de" }, { isSubdomain: false }), "p");
});
test("apex with s3 + p → s3 wins (owned bucket beats proxy)", () => {
// If the owner uploaded content, they meant that content. `p` is the
// "I don't have static content, mirror this instead" case.
assert.equal(pickRecord({ s3: "bns/x/", p: "https://x" }, { isSubdomain: false }), "s3");
});
test("apex with ip + p → ip wins (backward compat for on-chain-pinned proxies)", () => {
// coinspectrum.bch has both {ip, p, tls}. Its historical behaviour is
// "pin the upstream via chain-committed TLS fingerprint on the raw IP" —
// stricter than public-CA proxying via `p`. Do NOT flip that under it.
assert.equal(pickRecord({ ip: "1.2.3.4", p: "https://x", tls: "abc" }, { isSubdomain: false }), "ip");
});
test("apex with p + u → p wins (mirror beats redirect)", () => {
// p keeps the BNS address bar; u throws the visitor to the upstream URL.
// If the owner has both, they meant "mirror" — u is a fallback for clients
// that can't proxy.
assert.equal(pickRecord({ p: "https://x", u: "https://x" }, { isSubdomain: false }), "p");
});
test("subdomain inheritance does NOT use parent's p — p is single-URL by intent", () => {
// A parent with only `p` proxies its own content to one URL. Silently
// inheriting that for a subdomain query would route the subdomain's traffic
// through an upstream that doesn't know about it — usually the wrong thing.
// If the owner wants subdomain mirroring, they set `ip` (Host-routed).
assert.equal(pickRecord({ p: "https://x" }, { isSubdomain: true }), null);
assert.equal(pickRecord({ p: "https://x", u: "https://u" }, { isSubdomain: true }), "u");
});
// ---------- The invariant that must hold across any future change ----------
test("INVARIANT: for any records with both s3 and ip, the pick differs between apex and subdomain", () => {