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:
parent
f15752fe4b
commit
408f965629
2 changed files with 50 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue