Files
podsteadr-player/njs/fips.js
ssmithxandClaude Opus 5.5 1da8e186f5 fix(gateway): relay this node's own npub through the host
Under podman's pasta networking the container gets copies of the host's
addresses, including its FIPS address, so relaying to the node's own npub
connected back into the container and was refused (502). When the computed
address is one of the container's own, send the request to the host's
mapped address (host.containers.internal) instead. Other npubs still go
straight over the mesh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:19:30 +00:00

113 lines
3.9 KiB
JavaScript

// FIPS mesh address of an <npub>.fips host, for the /fips/ gateway in nginx.conf.
//
// A FIPS node's address is fd00::/8 followed by the first 15 bytes of
// sha256(its 32-byte x-only public key). Computing it here means the gateway
// needs no DNS — the container can't reach the host's fips DNS resolver, and
// only needs IPv6 egress to the mesh (podman `pasta` networking).
const crypto = require('crypto');
const fs = require('fs');
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
function polymod(values) {
let chk = 1;
for (let n = 0; n < values.length; n++) {
const v = values[n];
const top = chk >>> 25;
chk = (((chk & 0x1ffffff) << 5) ^ v) >>> 0;
for (let i = 0; i < 5; i++) {
if ((top >>> i) & 1) chk = (chk ^ GENERATORS[i]) >>> 0;
}
}
return chk;
}
// The 32-byte public key of a bech32 `npub1…`, or null if it isn't a valid npub.
function npubToPubkey(npub) {
if (npub.length !== 63 || npub.slice(0, 5) !== 'npub1') return null;
const data = [];
for (let n = 5; n < npub.length; n++) {
const v = CHARSET.indexOf(npub[n]);
if (v < 0) return null;
data.push(v);
}
// hrp "npub" expanded per BIP-173: each char's high bits, 0, each char's low bits.
const hrp = [3, 3, 3, 3, 0, 14, 16, 21, 2];
if (polymod(hrp.concat(data)) !== 1) return null;
const bytes = [];
let acc = 0;
let bits = 0;
// (indexed loops throughout: njs has no for...of)
for (let n = 0; n < data.length - 6; n++) {
acc = ((acc << 5) | data[n]) & 0xfff;
bits += 5;
if (bits >= 8) {
bits -= 8;
bytes.push((acc >> bits) & 0xff);
}
}
return bytes.length === 32 ? Buffer.from(bytes) : null;
}
// True when `hex` (32 hex digits) is one of this network namespace's own IPv6
// addresses. With podman's pasta networking the container gets copies of the
// host's addresses, so connecting to the host's own FIPS address stays inside
// the container and is refused.
function isLocalAddress(hex) {
let lines;
try {
lines = fs.readFileSync('/proc/net/if_inet6', 'utf8').split('\n');
} catch (e) {
return false;
}
for (let i = 0; i < lines.length; i++) {
if (lines[i].slice(0, 32) === hex) return true;
}
return false;
}
// The host's address as podman maps it into the container
// (host.containers.internal in /etc/hosts), or null when not under podman.
function hostAddress() {
let lines;
try {
lines = fs.readFileSync('/etc/hosts', 'utf8').split('\n');
} catch (e) {
return null;
}
for (let i = 0; i < lines.length; i++) {
const fields = lines[i].trim().split(/\s+/);
if (fields[0].charAt(0) === '#') continue;
if (fields.slice(1).indexOf('host.containers.internal') >= 0) return fields[0];
}
return null;
}
// js_set handler: the upstream address for $fips_host — "[fdxx:xxxx:…]", or
// the host's mapped address when the npub is this node's own — or "" when it
// isn't a valid npub host.
function address(r) {
const host = r.variables.fips_host || '';
const pubkey = npubToPubkey(host.replace(/\.fips$/, ''));
if (!pubkey) return '';
const digest = crypto.createHash('sha256').update(pubkey).digest();
const addr = [0xfd];
for (let i = 0; i < 15; i++) addr.push(digest[i]);
let hex = '';
for (let i = 0; i < 16; i++) hex += (addr[i] < 16 ? '0' : '') + addr[i].toString(16);
if (isLocalAddress(hex)) {
const local = hostAddress();
if (local) return local.indexOf(':') >= 0 ? '[' + local + ']' : local;
}
const groups = [];
for (let i = 0; i < 16; i += 2) groups.push(((addr[i] << 8) | addr[i + 1]).toString(16));
return '[' + groups.join(':') + ']';
}
export default { address };