underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
1,178 lines • 50.8 kB
JavaScript
'use strict';
import { expect } from 'chai';
import http from 'node:http';
import net from 'node:net';
import {
FORWARD_PROXY,
fetchViaForwardProxy,
forwardProxyAuthorizedFactory,
forwardProxyCommandFactory,
forwardProxyConfigFactory,
forwardProxyConnectHandlerFactory,
forwardProxyHeadersFactory,
forwardProxyNodeCandidatesFactory,
forwardProxyNodeProbeCommandFactory,
forwardProxyRequestHandlerFactory,
forwardProxyServiceCommandsFactory,
forwardProxyStartProbeCommandFactory,
forwardProxyTargetFactory,
forwardProxyTunnelTargetFactory,
forwardProxyUnitFactory,
} from '../src/server/forward-proxy.js';
import { homeDirectoryPathFactory } from '../src/server/systemd.js';
import {
UNDERPOST_EDGE,
allowedIpsConflictsFactory,
backendNameFactory,
defaultPeerFactory,
deployListFactory,
edgeRouteTableFactory,
edgeStateFactory,
firewallCommandsFactory,
haproxyConfFactory,
haproxyMapsFactory,
hostProxyEntriesFactory,
instanceProxyEntriesFactory,
mergeRouteTablesFactory,
peerFactory,
quicForwardCommandsFactory,
redirectHostFactory,
tunnelAddressFactory,
tunnelNetworkCidrFactory,
wireguardClientConfFactory,
wireguardClientSettingsFactory,
wireguardServerConfFactory,
wireguardStatusFactory,
} from '../src/cli/wireguard.js';
// The conf shape the PRD names: two published sites, each fronted by a bare
// domain that only redirects to its `www` host.
const CONF_SERVER = {
'dogmadual.com': {
'/': {
client: null,
runtime: 'nodejs',
apis: [],
origins: [],
proxy: [80, 443],
redirect: 'https://www.dogmadual.com',
},
},
'www.dogmadual.com': {
'/': { client: 'dogmadual', runtime: 'nodejs', apis: ['user', 'file'], ws: 'core', peer: true, proxy: [80, 443] },
},
'nexodev.org': {
'/': {
client: null,
runtime: 'nodejs',
apis: [],
origins: [],
proxy: [80, 443],
redirect: 'https://www.nexodev.org',
},
},
'www.nexodev.org': {
'/': { client: 'nexodev', runtime: 'nodejs', apis: ['default', 'user'], ws: 'core', peer: true, proxy: [80, 443] },
},
};
const INSTANCES = [
{ id: 'mmo-server', templateId: 'mmo-server', host: 'server.cyberiaonline.com', path: '/' },
{ id: 'mmo-server-forest', templateId: 'mmo-server', host: 'server.cyberiaonline.com', path: '/FOREST' },
{ id: 'mmo-client', templateId: 'mmo-client', host: 'client.cyberiaonline.com', path: '/' },
];
const PEERS = [
{
id: 'homelab-a',
address: '10.0.0.2',
publicKey: 'AAA=',
hosts: ['www.dogmadual.com'],
allowedIPs: ['10.0.0.2/32', '192.168.10.0/24'],
},
{ id: 'homelab-b', address: '10.0.0.3', publicKey: 'BBB=', hosts: ['www.nexodev.org'], instances: ['mmo-server'] },
];
describe('edge hub routing', () => {
describe('hostProxyEntriesFactory', () => {
it('extracts every domain that declares proxy ports, with its redirect target', () => {
expect(hostProxyEntriesFactory({ confServer: CONF_SERVER })).to.deep.equal([
{ host: 'dogmadual.com', ports: [80, 443], redirects: ['www.dogmadual.com'] },
{ host: 'nexodev.org', ports: [80, 443], redirects: ['www.nexodev.org'] },
{ host: 'www.dogmadual.com', ports: [80, 443], redirects: [] },
{ host: 'www.nexodev.org', ports: [80, 443], redirects: [] },
]);
});
// The `proxy` array is the declaration that a hostname is reachable from
// outside; without one the host is internal and must not reach the edge.
it('skips a host whose sub-paths declare no proxy ports', () => {
const entries = hostProxyEntriesFactory({
confServer: { 'internal.test': { '/': { client: 'x' } }, 'public.test': { '/': { proxy: [443] } } },
});
expect(entries.map((entry) => entry.host)).to.deep.equal(['public.test']);
});
it('unions ports across sub-paths, because the edge routes a hostname rather than a path', () => {
const [entry] = hostProxyEntriesFactory({
confServer: { 'app.test': { '/': { proxy: [443] }, '/api': { proxy: [80, 443] } } },
});
expect(entry.ports).to.deep.equal([80, 443]);
});
it('reads a redirect target whether it is a URL or a bare hostname', () => {
expect(redirectHostFactory('https://www.nexodev.org/path')).to.equal('www.nexodev.org');
expect(redirectHostFactory('www.nexodev.org')).to.equal('www.nexodev.org');
expect(redirectHostFactory('')).to.equal('');
expect(redirectHostFactory('::not a url::')).to.equal('');
});
});
describe('instanceProxyEntriesFactory', () => {
it('collapses a variant family onto one hostname reachable by id and by template id', () => {
expect(instanceProxyEntriesFactory({ instances: INSTANCES })).to.deep.equal([
{ host: 'client.cyberiaonline.com', ports: [80, 443], instances: ['mmo-client'] },
{
host: 'server.cyberiaonline.com',
ports: [80, 443],
instances: ['mmo-server', 'mmo-server-forest'],
},
]);
});
});
describe('edgeRouteTableFactory', () => {
it('resolves each published hostname to the spoke its binding names, redirects included', () => {
const { routes, unresolved } = edgeRouteTableFactory({ confServer: CONF_SERVER, peers: PEERS });
expect(unresolved).to.deep.equal([]);
expect(routes).to.deep.equal([
{ host: 'dogmadual.com', ports: [80, 443], peerId: 'homelab-a', address: '10.0.0.2', via: 'redirect' },
{ host: 'nexodev.org', ports: [80, 443], peerId: 'homelab-b', address: '10.0.0.3', via: 'redirect' },
{ host: 'www.dogmadual.com', ports: [80, 443], peerId: 'homelab-a', address: '10.0.0.2', via: 'host' },
{ host: 'www.nexodev.org', ports: [80, 443], peerId: 'homelab-b', address: '10.0.0.3', via: 'host' },
]);
});
it('resolves instance hostnames through the template id, covering every variant', () => {
const { routes } = edgeRouteTableFactory({ instances: INSTANCES, peers: PEERS });
const server = routes.find((route) => route.host === 'server.cyberiaonline.com');
expect(server).to.include({ peerId: 'homelab-b', via: 'instance' });
});
it('prefers an explicit hostname binding over the instance binding', () => {
const { routes } = edgeRouteTableFactory({
instances: INSTANCES,
peers: [
...PEERS,
{ id: 'homelab-c', address: '10.0.0.4', publicKey: 'CCC=', hosts: ['server.cyberiaonline.com'] },
],
});
expect(routes.find((route) => route.host === 'server.cyberiaonline.com')).to.include({
peerId: 'homelab-c',
via: 'host',
});
});
it('sends an unmatched hostname to the default spoke', () => {
const { routes, unresolved } = edgeRouteTableFactory({
confServer: { 'orphan.test': { '/': { proxy: [443] } } },
peers: [...PEERS, { id: 'fallback', address: '10.0.0.9', publicKey: 'DDD=', default: true }],
});
expect(unresolved).to.deep.equal([]);
expect(routes).to.deep.equal([
{ host: 'orphan.test', ports: [443], peerId: 'fallback', address: '10.0.0.9', via: 'default' },
]);
});
it('treats a lone spoke as its own fallback', () => {
const { routes } = edgeRouteTableFactory({
confServer: { 'orphan.test': { '/': { proxy: [443] } } },
peers: [{ id: 'only', address: '10.0.0.2', publicKey: 'AAA=' }],
});
expect(routes[0]).to.include({ peerId: 'only', via: 'default' });
});
// A dropped hostname answers nothing at all, and nothing about the deploy
// reveals it — so an unbindable host is reported instead.
it('reports a hostname that binds to nothing rather than dropping it', () => {
const { routes, unresolved } = edgeRouteTableFactory({
confServer: { 'orphan.test': { '/': { proxy: [443] } } },
peers: PEERS,
});
expect(routes).to.deep.equal([]);
expect(unresolved).to.deep.equal(['orphan.test']);
});
it('does not loop on redirects that point at each other', () => {
const { routes, unresolved } = edgeRouteTableFactory({
confServer: {
'a.test': { '/': { proxy: [443], redirect: 'https://b.test' } },
'b.test': { '/': { proxy: [443], redirect: 'https://a.test' } },
},
peers: PEERS,
});
expect(routes).to.deep.equal([]);
expect(unresolved).to.deep.equal(['a.test', 'b.test']);
});
it('merges a hostname declared by both conf.server.json and an instance', () => {
const { routes } = edgeRouteTableFactory({
confServer: { 'server.cyberiaonline.com': { '/': { proxy: [443] } } },
instances: INSTANCES,
peers: PEERS,
});
const server = routes.find((route) => route.host === 'server.cyberiaonline.com');
expect(server.ports).to.deep.equal([80, 443]);
expect(server.via).to.equal('instance');
});
it('returns only the peers the routes reference, so no unused backend is emitted', () => {
const { peers } = edgeRouteTableFactory({
confServer: { 'www.dogmadual.com': CONF_SERVER['www.dogmadual.com'] },
peers: PEERS,
});
expect(peers.map((peer) => peer.id)).to.deep.equal(['homelab-a']);
});
});
describe('allowedIpsConflictsFactory', () => {
it('is quiet when every peer claims a distinct CIDR', () => {
expect(allowedIpsConflictsFactory({ peers: PEERS })).to.deep.equal([]);
});
// WireGuard picks one peer by longest-prefix match; the other silently
// never receives that traffic. Two homelabs on 192.168.1.0/24 is the
// ordinary way this happens.
it('reports a LAN subnet two spokes both claim', () => {
const conflicts = allowedIpsConflictsFactory({
peers: [
{ id: 'homelab-a', address: '10.0.0.2', allowedIPs: ['10.0.0.2/32', '192.168.1.0/24'] },
{ id: 'homelab-b', address: '10.0.0.3', allowedIPs: ['10.0.0.3/32', '192.168.1.0/24'] },
],
});
expect(conflicts).to.deep.equal([{ cidr: '192.168.1.0/24', peers: ['homelab-a', 'homelab-b'] }]);
});
// An address with no explicit allowedIPs contributes its own /32, so the
// same pass catches two spokes handed the same tunnel address.
it('catches a duplicated tunnel address', () => {
const conflicts = allowedIpsConflictsFactory({
peers: [
{ id: 'homelab-a', address: '10.0.0.2' },
{ id: 'homelab-b', address: '10.0.0.2' },
],
});
expect(conflicts).to.deep.equal([{ cidr: '10.0.0.2/32', peers: ['homelab-a', 'homelab-b'] }]);
});
});
// One rule for the catch-all, shared by hostname resolution, the HAProxy
// default backends and the QUIC forward — so all three name the same spoke.
describe('defaultPeerFactory', () => {
it('nominates the peer marked default', () => {
expect(defaultPeerFactory([...PEERS, { id: 'fallback', address: '10.0.0.9', default: true }]).id).to.equal(
'fallback',
);
});
it('treats a lone peer as its own fallback', () => {
expect(defaultPeerFactory([PEERS[0]]).id).to.equal('homelab-a');
});
// Picking one arbitrarily would send every unmatched hostname and all of
// UDP :443 to a spoke nobody nominated.
it('nominates nothing when several peers exist and none is marked', () => {
expect(defaultPeerFactory(PEERS)).to.equal(null);
expect(defaultPeerFactory([])).to.equal(null);
});
});
describe('deployListFactory', () => {
it('expands the dd meta id through dd.router', () => {
const list = deployListFactory('dd');
expect(list.length).to.be.above(0);
for (const id of list) expect(id.startsWith('dd-')).to.equal(true);
});
// `dd` names every deploy; prefixing it would address a deploy that does
// not exist and silently route nothing.
it('never prefixes the dd meta id itself', () => {
expect(deployListFactory('dd')).to.not.include('dd-dd');
});
it('accepts a comma-separated list and normalizes bare ids', () => {
expect(deployListFactory('dd-core,cyberia')).to.deep.equal(['dd-core', 'dd-cyberia']);
});
it('resolves an empty id to no deploys rather than to everything', () => {
expect(deployListFactory('')).to.deep.equal([]);
});
});
describe('mergeRouteTablesFactory', () => {
// The edge holds one pair of map files. Publishing one deploy's table alone
// would overwrite them and take every other deploy off the internet.
it('unions every deploy into one table, attributing each hostname', () => {
const { routes } = mergeRouteTablesFactory({
tables: [
{
deployId: 'dd-cyberia',
routes: [{ host: 'www.cyberiaonline.com', ports: [80, 443], peerId: 'homelab-a', address: '10.0.0.2' }],
},
{
deployId: 'dd-core',
routes: [{ host: 'www.nexodev.org', ports: [80, 443], peerId: 'homelab-b', address: '10.0.0.3' }],
},
],
});
expect(routes.map((route) => [route.host, route.deployId])).to.deep.equal([
['www.cyberiaonline.com', 'dd-cyberia'],
['www.nexodev.org', 'dd-core'],
]);
});
it('reports a hostname two deploys claim, and serves the first deterministically', () => {
const { routes, conflicts } = mergeRouteTablesFactory({
tables: [
{
deployId: 'dd-core',
routes: [{ host: 'shared.test', ports: [443], peerId: 'homelab-a', address: '10.0.0.2' }],
},
{
deployId: 'dd-test',
routes: [{ host: 'shared.test', ports: [443], peerId: 'homelab-b', address: '10.0.0.3' }],
},
],
});
expect(routes).to.have.lengthOf(1);
expect(routes[0].peerId).to.equal('homelab-a');
expect(conflicts).to.deep.equal([{ host: 'shared.test', claimed: ['dd-core', 'dd-test'], serving: 'homelab-a' }]);
});
it('does not report a duplicate that both deploys route identically', () => {
const { conflicts } = mergeRouteTablesFactory({
tables: [
{
deployId: 'dd-core',
routes: [{ host: 'shared.test', ports: [443], peerId: 'homelab-a', address: '10.0.0.2' }],
},
{
deployId: 'dd-test',
routes: [{ host: 'shared.test', ports: [443], peerId: 'homelab-a', address: '10.0.0.2' }],
},
],
});
expect(conflicts).to.deep.equal([]);
});
it('keeps unresolved hostnames attributed to the deploy that declared them', () => {
const { unresolved } = mergeRouteTablesFactory({
tables: [
{ deployId: 'dd-core', routes: [], unresolved: ['orphan.test'] },
{ deployId: 'dd-lampp', routes: [], unresolved: ['other.test'] },
],
});
expect(unresolved).to.deep.equal([
{ host: 'orphan.test', deployId: 'dd-core' },
{ host: 'other.test', deployId: 'dd-lampp' },
]);
});
it('deduplicates the peers every deploy shares, so one backend is emitted per spoke', () => {
const { peers } = mergeRouteTablesFactory({
tables: [
{ deployId: 'dd-core', routes: [], peers: [{ id: 'homelab-a', address: '10.0.0.2' }] },
{ deployId: 'dd-cyberia', routes: [], peers: [{ id: 'homelab-a', address: '10.0.0.2' }] },
],
});
expect(peers).to.have.lengthOf(1);
});
});
describe('haproxyMapsFactory', () => {
it('writes one map line per hostname and transport, keyed on the declared ports', () => {
const { sni, http } = haproxyMapsFactory({
routes: [
{ host: 'a.test', ports: [80, 443], peerId: 'homelab-a', address: '10.0.0.2' },
{ host: 'b.test', ports: [443], peerId: 'homelab-b', address: '10.0.0.3' },
{ host: 'c.test', ports: [80], peerId: 'homelab-b', address: '10.0.0.3' },
],
});
expect(sni).to.equal('a.test be_tls_homelab_a\nb.test be_tls_homelab_b\n');
expect(http).to.equal('a.test be_http_homelab_a\nc.test be_http_homelab_b\n');
});
it('renders an empty map rather than a stray newline when nothing is routed', () => {
expect(haproxyMapsFactory({ routes: [] })).to.deep.equal({ sni: '', http: '' });
});
it('keeps peer ids that are not valid proxy names out of the backend names', () => {
expect(backendNameFactory('tls', 'home lab.a-1')).to.equal('be_tls_home_lab_a_1');
});
});
describe('haproxyConfFactory', () => {
const conf = haproxyConfFactory({ peers: PEERS.map(peerFactory), defaultPeerId: 'homelab-a' });
// The whole point of the L4 edge: no certificate, no key, nothing to decrypt.
it('never terminates TLS', () => {
expect(conf).to.not.match(/bind\s+.*\bssl\b/);
expect(conf).to.not.include('crt ');
expect(conf).to.include('mode tcp');
});
it('selects the TLS backend from the ClientHello SNI', () => {
expect(conf).to.include('tcp-request content accept if { req_ssl_hello_type 1 }');
expect(conf).to.include(
`use_backend %[req.ssl_sni,lower,map(${UNDERPOST_EDGE.haproxyDir}/${UNDERPOST_EDGE.sniMapName},be_tls_default)]`,
);
});
it('selects the cleartext backend from the Host header, ignoring an appended port', () => {
expect(conf).to.include(
`use_backend %[req.hdr(host),lower,word(1,:),map(${UNDERPOST_EDGE.haproxyDir}/${UNDERPOST_EDGE.httpMapName},be_http_default)]`,
);
});
it('emits both transports for every peer', () => {
for (const peer of PEERS) {
expect(conf).to.include(`backend be_http_${peer.id.replace(/-/g, '_')}`);
expect(conf).to.include(`server ${peer.id} ${peer.address}:443 check`);
}
});
it('points the default backends at the nominated spoke', () => {
expect(conf).to.include('backend be_tls_default\n mode tcp\n server homelab-a 10.0.0.2:443 check');
});
it('refuses an unmatched hostname when no spoke is nominated as default', () => {
const strict = haproxyConfFactory({ peers: [] });
expect(strict).to.include('http-request deny deny_status 421');
expect(strict).to.include('tcp-request content reject');
});
it('keeps long-lived streams alive past the idle timeouts', () => {
expect(conf).to.include('timeout tunnel 1h');
});
it('hands listening sockets to the incoming process so a reload drops nothing', () => {
expect(conf).to.include('expose-fd listeners');
});
});
describe('wireguard interface configs', () => {
const server = wireguardServerConfFactory({
address: '10.0.0.1/24',
keyPath: '/etc/wireguard/wg0.key',
peers: PEERS,
});
// No rendered config, dry-run print or log line may ever carry the key.
it('loads the private key from its own file instead of inlining it', () => {
expect(server).to.not.include('PrivateKey =');
expect(server).to.include('PostUp = wg set %i private-key /etc/wireguard/wg0.key');
});
it('writes one peer block per registered spoke, carrying its routed subnets', () => {
expect(server).to.include('PublicKey = AAA=');
expect(server).to.include('AllowedIPs = 10.0.0.2/32, 192.168.10.0/24');
// A spoke that declares no subnets still gets its own tunnel address.
expect(server).to.include('AllowedIPs = 10.0.0.3/32');
});
it('skips a peer that has no public key yet', () => {
const partial = wireguardServerConfFactory({
address: '10.0.0.1/24',
keyPath: '/etc/wireguard/wg0.key',
peers: [{ id: 'pending', address: '10.0.0.5' }],
});
expect(partial).to.not.include('[Peer]');
});
it('removes its own forwarding rules on teardown', () => {
expect(server).to.include('PostUp = iptables -I FORWARD -i wg0 -j ACCEPT');
expect(server).to.include('PostDown = iptables -D FORWARD -i wg0 -j ACCEPT');
});
const client = wireguardClientConfFactory({
address: '10.0.0.2',
keyPath: '/etc/wireguard/wg0.key',
publicKey: 'HUB=',
endpoint: 'vps.example.com:51820',
});
// A default route here would send a whole cluster's egress via the VPS.
it('routes only the tunnel subnet through the hub, never a default route', () => {
expect(client).to.include('AllowedIPs = 10.0.0.0/24');
expect(client).to.not.include('0.0.0.0/0');
});
it('holds the outbound NAT mapping open across a CGNAT boundary', () => {
expect(client).to.include(`PersistentKeepalive = ${UNDERPOST_EDGE.keepalive}`);
expect(UNDERPOST_EDGE.keepalive).to.be.below(30);
});
it('gives a bare spoke address a host prefix', () => {
expect(client).to.include('Address = 10.0.0.2/32');
});
it('masquerades pod traffic only when it enters the tunnel', () => {
expect(client).to.include('PostUp = iptables -t nat -C POSTROUTING -o %i -d 10.0.0.0/24 -j MASQUERADE');
expect(client).to.not.include('POSTROUTING -o %i -j MASQUERADE');
});
it('forwards tunnel requests and established replies for spoke workloads', () => {
expect(client).to.include('PostUp = iptables -C FORWARD -o %i -d 10.0.0.0/24 -j ACCEPT');
expect(client).to.include(
'PostUp = iptables -C FORWARD -i %i -s 10.0.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT',
);
});
it('withdraws every spoke forwarding rule with the interface', () => {
expect(client).to.include(
'PostDown = iptables -t nat -D POSTROUTING -o %i -d 10.0.0.0/24 -j MASQUERADE 2>/dev/null || true',
);
expect(client).to.include('PostDown = iptables -D FORWARD -o %i -d 10.0.0.0/24 -j ACCEPT');
});
it('recovers repeatable client settings from an installed config without reading a private key', () => {
expect(wireguardClientSettingsFactory(client)).to.deep.equal({
address: '10.0.0.2/32',
hubPublicKey: 'HUB=',
endpoint: 'vps.example.com:51820',
cidr: '10.0.0.0/24',
});
});
});
describe('quicForwardCommandsFactory', () => {
const { ensure, remove } = quicForwardCommandsFactory({ target: '10.0.0.2' });
// Flush-then-refill is what makes a re-run idempotent: no accumulating
// near-duplicate rules, and no guessing what a previous run installed.
it('flushes its own chains before adding rules', () => {
expect(ensure.filter((command) => command.includes('-F UNDERPOST_WG_PRE'))).to.have.lengthOf(1);
expect(ensure.indexOf('sudo iptables -t nat -F UNDERPOST_WG_PRE')).to.be.below(
ensure.findIndex((command) => command.includes('DNAT')),
);
});
it('adds the jump rules only when they are missing', () => {
expect(ensure).to.include(
'sudo iptables -t nat -C PREROUTING -j UNDERPOST_WG_PRE 2>/dev/null || sudo iptables -t nat -A PREROUTING -j UNDERPOST_WG_PRE',
);
});
it('does not re-DNAT datagrams that already came through the tunnel', () => {
expect(ensure.find((command) => command.includes('DNAT'))).to.include('! -i wg0');
});
it('renders the chains with no rules when there is no spoke to forward to', () => {
const empty = quicForwardCommandsFactory({});
expect(empty.ensure.some((command) => command.includes('DNAT'))).to.equal(false);
});
it('removes the jump rules before deleting the chains', () => {
expect(remove.findIndex((command) => command.includes('-D PREROUTING'))).to.be.below(
remove.findIndex((command) => command.includes('-X UNDERPOST_WG_PRE')),
);
});
});
describe('firewallCommandsFactory', () => {
it('opens both transports and the tunnel port on the hub', () => {
const commands = firewallCommandsFactory({ role: 'server' }).join('\n');
for (const port of ['80/tcp', '443/tcp', '443/udp', '51820/udp']) expect(commands).to.include(port);
});
// A spoke dials out; it publishes nothing. What it does need is for the
// tunnel interface to sit in a zone that permits the forwarded traffic.
it('opens nothing on a spoke beyond trusting the tunnel interface', () => {
const commands = firewallCommandsFactory({ role: 'client' }).join('\n');
expect(commands).to.include('--zone=trusted --add-interface=wg0');
expect(commands).to.not.include('--add-port');
});
it('is a no-op where firewalld is not running', () => {
for (const command of firewallCommandsFactory({ role: 'server' }))
expect(command).to.include('systemctl is-active --quiet firewalld');
});
// A reset that leaves the ports open has not returned the host to zero, and
// the only way the two directions cannot drift is sharing one rule list.
it('withdraws exactly the rules it opens, for either role', () => {
const opened = firewallCommandsFactory({ role: 'server', listenPort: 51821 });
const withdrawn = firewallCommandsFactory({ role: 'server', listenPort: 51821, remove: true });
expect(withdrawn).to.have.lengthOf(opened.length);
expect(withdrawn.join('\n')).to.equal(opened.join('\n').replace(/--add-/g, '--remove-'));
expect(firewallCommandsFactory({ role: 'client', remove: true }).join('\n')).to.include(
'--zone=trusted --remove-interface=wg0',
);
});
});
describe('wireguardStatusFactory', () => {
// One row per spoke carries both halves of its state: what the registry
// binds to it, and what the live interface reports — so there is no second
// command to list peers.
it('folds link state onto the registry bindings', () => {
const rows = wireguardStatusFactory({
peers: PEERS,
latestHandshakes: 'AAA=\t1000\nBBB=\t0',
transfer: 'AAA=\t2048\t4096',
endpoints: 'AAA=\t203.0.113.7:51820\nBBB=\t(none)',
now: 1030,
});
expect(rows).to.deep.equal([
{
id: 'homelab-a',
address: '10.0.0.2',
allowedIPs: ['10.0.0.2/32', '192.168.10.0/24'],
hosts: ['www.dogmadual.com'],
instances: [],
default: false,
endpoint: '203.0.113.7:51820',
handshakeAgeSeconds: 30,
rxBytes: 2048,
txBytes: 4096,
online: true,
},
{
id: 'homelab-b',
address: '10.0.0.3',
allowedIPs: ['10.0.0.3/32'],
hosts: ['www.nexodev.org'],
instances: ['mmo-server'],
default: false,
endpoint: '',
handshakeAgeSeconds: null,
rxBytes: 0,
txBytes: 0,
online: false,
},
]);
});
it('treats a peer past the rekey window as down rather than idle', () => {
const [row] = wireguardStatusFactory({
peers: [PEERS[0]],
latestHandshakes: 'AAA=\t1000',
now: 1000 + UNDERPOST_EDGE.handshakeStaleSeconds + 1,
});
expect(row.online).to.equal(false);
});
});
describe('registry normalization', () => {
it('fills the defaults a hand-written registry may omit', () => {
const peer = peerFactory({ id: 'homelab-a', address: '10.0.0.2' });
expect(peer.allowedIPs).to.deep.equal(['10.0.0.2/32']);
expect(peer.hosts).to.deep.equal([]);
expect(peer.instances).to.deep.equal([]);
expect(peer.default).to.equal(false);
});
// The three routing bindings are the whole registry surface; anything else
// a hand-edited file carries is not a binding the edge honours.
it('keeps only the three routing bindings on a peer', () => {
const peer = peerFactory({ id: 'a', address: '10.0.0.2', clients: ['legacy'], keepalive: 15 });
expect(Object.keys(peer)).to.deep.equal([
'id',
'address',
'publicKey',
'allowedIPs',
'hosts',
'instances',
'default',
]);
});
it('lower-cases bound hostnames so they match the map lookups', () => {
expect(peerFactory({ id: 'a', address: '10.0.0.2', hosts: ['WWW.Nexodev.ORG'] }).hosts).to.deep.equal([
'www.nexodev.org',
]);
});
it('drops registry entries with no id', () => {
const state = edgeStateFactory({ peers: [{ address: '10.0.0.2' }, { id: 'ok', address: '10.0.0.3' }] });
expect(state.peers.map((peer) => peer.id)).to.deep.equal(['ok']);
});
// A spoke rebuilds its interface from `endpoint` + `hubPublicKey`. Without
// the second one recorded, re-running the setup — and every --wireguard-
// reinstall, which ends in one — fails on a flag the operator already gave.
it('records the hub identity a spoke dials, so its setup is repeatable', () => {
const state = edgeStateFactory({ role: 'client', endpoint: 'vps.example.com:51820', hubPublicKey: 'HUB=' });
expect(state.endpoint).to.equal('vps.example.com:51820');
expect(state.hubPublicKey).to.equal('HUB=');
expect(edgeStateFactory().hubPublicKey).to.equal('');
});
it('falls back to the subsystem defaults for an empty registry', () => {
const state = edgeStateFactory();
expect(state.interfaceName).to.equal(UNDERPOST_EDGE.interfaceName);
expect(state.listenPort).to.equal(UNDERPOST_EDGE.listenPort);
expect(state.peers).to.deep.equal([]);
});
});
});
// The forward proxy is the one outbound path across the tunnel: a spoke's
// request leaves through the hub, so the origin sees the VPS address. Everything
// below runs on loopback with no network egress.
describe('edge hub forward proxy', () => {
const API_KEY = 'forward-proxy-test-key';
describe('forwardProxyAuthorizedFactory', () => {
it('accepts the configured key presented as a bearer token', () => {
expect(forwardProxyAuthorizedFactory({ header: `Bearer ${API_KEY}`, apiKey: API_KEY })).to.equal(true);
expect(forwardProxyAuthorizedFactory({ header: `bearer ${API_KEY}`, apiKey: API_KEY })).to.equal(true);
});
it('refuses a wrong key, a missing header and a bare key with no scheme', () => {
expect(forwardProxyAuthorizedFactory({ header: 'Bearer wrong-key', apiKey: API_KEY })).to.equal(false);
expect(forwardProxyAuthorizedFactory({ apiKey: API_KEY })).to.equal(false);
expect(forwardProxyAuthorizedFactory({ header: API_KEY, apiKey: API_KEY })).to.equal(false);
expect(forwardProxyAuthorizedFactory({ header: `Basic ${API_KEY}`, apiKey: API_KEY })).to.equal(false);
});
// A server that read an unset key as "no authentication required" would be
// an open relay on the tunnel, which is the one failure that must not be
// possible by omission.
it('authorizes nothing when no key is configured', () => {
expect(forwardProxyAuthorizedFactory({ header: 'Bearer anything' })).to.equal(false);
expect(forwardProxyAuthorizedFactory({ header: 'Bearer ', apiKey: '' })).to.equal(false);
expect(forwardProxyAuthorizedFactory()).to.equal(false);
});
});
describe('forwardProxyTargetFactory', () => {
it('reads the origin out of an absolute request URI, keeping the query', () => {
expect(forwardProxyTargetFactory('http://api.vultr.com/v2/plans?per_page=500')).to.deep.equal({
hostname: 'api.vultr.com',
port: 80,
path: '/v2/plans?per_page=500',
host: 'api.vultr.com',
});
expect(forwardProxyTargetFactory('http://origin.test:8080/x')).to.include({ port: 8080 });
});
// An https origin arrives as CONNECT. Serving one over the forward path
// would mean terminating TLS on the hub, which the whole subsystem avoids.
it('refuses a relative URI and any scheme other than http', () => {
expect(forwardProxyTargetFactory('/v2/plans')).to.equal(null);
expect(forwardProxyTargetFactory('https://api.vultr.com/v2/plans')).to.equal(null);
expect(forwardProxyTargetFactory('')).to.equal(null);
expect(forwardProxyTargetFactory('http://')).to.equal(null);
});
});
describe('forwardProxyTunnelTargetFactory', () => {
it('parses a CONNECT authority, defaulting to the https port', () => {
expect(forwardProxyTunnelTargetFactory('api.vultr.com:443')).to.deep.equal({
hostname: 'api.vultr.com',
port: 443,
});
expect(forwardProxyTunnelTargetFactory('api.vultr.com')).to.deep.equal({
hostname: 'api.vultr.com',
port: UNDERPOST_EDGE.httpsPort,
});
});
it('refuses an authority that is not a bare host and port', () => {
expect(forwardProxyTunnelTargetFactory('http://api.vultr.com/')).to.equal(null);
expect(forwardProxyTunnelTargetFactory('user@api.vultr.com:443')).to.equal(null);
expect(forwardProxyTunnelTargetFactory('api.vultr.com:0')).to.equal(null);
expect(forwardProxyTunnelTargetFactory('api.vultr.com:https')).to.equal(null);
expect(forwardProxyTunnelTargetFactory('')).to.equal(null);
});
});
describe('forwardProxyHeadersFactory', () => {
// Relaying the proxy key onward would hand the hub's credential to every
// origin a spoke talks to.
it('drops the hop-by-hop headers, the proxy credential included', () => {
const headers = forwardProxyHeadersFactory({
host: 'api.vultr.com',
authorization: 'Bearer vultr-key',
'proxy-authorization': `Bearer ${API_KEY}`,
'Proxy-Connection': 'keep-alive',
connection: 'close',
'transfer-encoding': 'chunked',
upgrade: 'h2c',
});
expect(headers).to.deep.equal({ host: 'api.vultr.com', authorization: 'Bearer vultr-key' });
});
});
describe('forwardProxyConfigFactory', () => {
const saved = { ...process.env };
afterEach(() => {
for (const key of Object.values(FORWARD_PROXY.env))
if (saved[key] === undefined) delete process.env[key];
else process.env[key] = saved[key];
});
// Asserted against the constants rather than against an unset environment:
// the factory also reads `./.env` and the underpost root env, which exist on
// a configured host and would make an "unset" expectation depend on the box
// the suite runs on.
it('falls back to the hub tunnel address and the subsystem port', () => {
expect(tunnelAddressFactory(UNDERPOST_EDGE.cidr)).to.equal('10.0.0.1');
expect(tunnelAddressFactory('10.0.0.1')).to.equal('10.0.0.1');
expect(FORWARD_PROXY.port).to.equal(1080);
});
it('reads the environment, and lets an explicit endpoint win over it', () => {
process.env[FORWARD_PROXY.env.host] = '10.0.0.9';
process.env[FORWARD_PROXY.env.port] = '3128';
process.env[FORWARD_PROXY.env.apiKey] = API_KEY;
expect(forwardProxyConfigFactory()).to.deep.equal({ host: '10.0.0.9', port: 3128, apiKey: API_KEY });
expect(forwardProxyConfigFactory({ host: '10.0.0.1', port: 1080, apiKey: 'other' })).to.deep.equal({
host: '10.0.0.1',
port: 1080,
apiKey: 'other',
});
});
});
describe('firewallCommandsFactory', () => {
// The listener binds the tunnel address alone, and the rule narrows the port
// to the tunnel CIDR on top of that — the port is reachable from nowhere else.
it('admits the proxy port from the tunnel only, on the hub only', () => {
const hub = firewallCommandsFactory({ role: 'server' }).join('\n');
expect(hub).to.include(
`--add-rich-rule="rule family=ipv4 source address=${UNDERPOST_EDGE.tunnelCidr} port port=${FORWARD_PROXY.port} protocol=tcp accept"`,
);
expect(hub).to.not.include(`--add-port=${FORWARD_PROXY.port}`);
expect(firewallCommandsFactory({ role: 'client' }).join('\n')).to.not.include('rich-rule');
});
it('admits a custom recorded tunnel subnet instead of the default', () => {
expect(firewallCommandsFactory({ role: 'server', tunnelCidr: '10.23.0.0/24' }).join('\n')).to.include(
`source address=10.23.0.0/24 port port=${FORWARD_PROXY.port} protocol=tcp accept`,
);
expect(tunnelNetworkCidrFactory('10.23.7.9/20')).to.equal('10.23.0.0/20');
});
});
describe('forwardProxyUnitFactory', () => {
const unit = () =>
forwardProxyUnitFactory({
host: '10.0.0.1',
port: 1080,
apiKey: API_KEY,
interfaceName: 'wg0',
workingDirectory: '/home/dd/engine',
user: 'dd',
command: forwardProxyCommandFactory({
host: '10.0.0.1',
port: 1080,
execPath: '/usr/bin/node',
scriptPath: '/home/dd/engine/bin/index.js',
}),
});
// The unit runs the same command the operator did, so there is one code path
// to the listener rather than a second one only systemd takes.
it('runs this CLI with the resolved host and port, marked as supervised', () => {
expect(unit()).to.include(
'ExecStart=/usr/bin/node /home/dd/engine/bin/index.js wireguard --forward-proxy-server ' +
'--forward-proxy-server-host 10.0.0.1 --forward-proxy-server-port 1080',
);
expect(unit()).to.include(`Environment=${FORWARD_PROXY.supervisedEnv}=1`);
expect(unit()).to.include(`Environment=${FORWARD_PROXY.env.apiKey}=${API_KEY}`);
expect(unit()).to.include('WorkingDirectory=/home/dd/engine');
expect(unit()).to.include('User=dd');
});
it('carries the host and port the flags resolved into the description and the command', () => {
const custom = forwardProxyUnitFactory({
host: '10.0.0.5',
port: 3128,
apiKey: API_KEY,
execPath: '/usr/bin/node',
});
expect(custom).to.include('Description=Underpost edge forward proxy on 10.0.0.5:3128');
expect(custom).to.include('--forward-proxy-server-host 10.0.0.5 --forward-proxy-server-port 3128');
});
// The address the proxy binds exists only while the interface is up, so the
// two units are one lifecycle: the tunnel's stop and restart propagate, and
// starting the tunnel brings the proxy back.
it('ties the service to the tunnel unit and always restarts', () => {
const rendered = forwardProxyUnitFactory({ host: '10.0.0.1', port: 1080, apiKey: API_KEY, interfaceName: 'wg1' });
expect(rendered).to.include('Requires=wg-quick@wg1.service');
expect(rendered).to.include('PartOf=wg-quick@wg1.service');
expect(rendered).to.include('WantedBy=multi-user.target wg-quick@wg1.service');
expect(rendered).to.include('Restart=always');
expect(rendered).to.include(`RestartSec=${FORWARD_PROXY.restartSeconds}`);
// No start-limit window, so a bind that fails while the tunnel comes up
// retries instead of latching failed.
expect(rendered).to.include('StartLimitIntervalSec=0');
});
});
describe('forwardProxyNodeCandidatesFactory', () => {
// The failure this ordering exists for: systemd cannot enter /root, so a unit
// pointed at an nvm install there restarts on 203/EXEC forever while every
// ordinary permission check on the binary passes.
it('prefers a system Node and keeps one under a home directory for last', () => {
expect(
forwardProxyNodeCandidatesFactory({ execPath: '/root/.nvm/versions/node/v24.15.0/bin/node' }),
).to.deep.equal([...FORWARD_PROXY.nodePaths, '/root/.nvm/versions/node/v24.15.0/bin/node']);
expect(forwardProxyNodeCandidatesFactory({ execPath: '/home/dd/.nvm/x/bin/node' })).to.deep.equal([
...FORWARD_PROXY.nodePaths,
'/home/dd/.nvm/x/bin/node',
]);
});
it('puts the running interpreter first when it is not in a home directory, without duplicating it', () => {
expect(forwardProxyNodeCandidatesFactory({ execPath: '/opt/node/bin/node' })).to.deep.equal([
'/opt/node/bin/node',
...FORWARD_PROXY.nodePaths,
]);
expect(forwardProxyNodeCandidatesFactory({ execPath: '/usr/bin/node' })).to.deep.equal(FORWARD_PROXY.nodePaths);
});
it('recognises a home directory path without mistaking a lookalike for one', () => {
expect(homeDirectoryPathFactory('/root/.nvm/x/node')).to.equal(true);
expect(homeDirectoryPathFactory('/root')).to.equal(true);
expect(homeDirectoryPathFactory('/home/dd/engine')).to.equal(true);
expect(homeDirectoryPathFactory('/usr/bin/node')).to.equal(false);
expect(homeDirectoryPathFactory('/rootfs/bin/node')).to.equal(false);
expect(homeDirectoryPathFactory('')).to.equal(false);
});
});
describe('forward proxy start probes', () => {
// `test -x` passes on a binary systemd still refuses, so the only reliable
// test is running it through systemd itself, as the unit's own user.
it('asks systemd whether it can execute the interpreter', () => {
expect(forwardProxyNodeProbeCommandFactory('/usr/bin/node', 'root')).to.equal(
'sudo systemd-run --quiet --collect --wait --uid=root --property=Type=oneshot /usr/bin/node --version',
);
});
it('asks the same of the whole entry point, from the working directory the service will use', () => {
expect(
forwardProxyStartProbeCommandFactory({
nodePath: '/usr/bin/node',
scriptPath: '/opt/underpost/engine/bin',
user: 'dd',
workingDirectory: '/opt/underpost/engine',
}),
).to.equal(
'sudo systemd-run --quiet --collect --wait --uid=dd --property=Type=oneshot ' +
'--property=WorkingDirectory=/opt/underpost/engine /usr/bin/node /opt/underpost/engine/bin --version',
);
});
});
describe('forwardProxyServiceCommandsFactory', () => {
// Re-running the command must not restart a service that is carrying
// connections, and must not fail because one is already up.
it('starts an unchanged service without reloading or restarting it', () => {
const { ensure } = forwardProxyServiceCommandsFactory({ changed: false });
expect(ensure).to.deep.equal([
`sudo systemctl enable ${FORWARD_PROXY.serviceName} || true`,
`sudo systemctl start ${FORWARD_PROXY.serviceName} || true`,
]);
expect(ensure.join('\n')).to.not.include('restart');
expect(ensure.join('\n')).to.not.include('daemon-reload');
});
it('reloads and restarts only when the unit file changed', () => {
expect(forwardProxyServiceCommandsFactory({ changed: true }).ensure).to.deep.equal([
'sudo systemctl daemon-reload',
`sudo systemctl enable ${FORWARD_PROXY.serviceName} || true`,
`sudo systemctl restart ${FORWARD_PROXY.serviceName} || true`,
]);
});
it('withdraws the unit it installed, reloading after the removal', () => {
expect(forwardProxyServiceCommandsFactory().remove).to.deep.equal([
`sudo systemctl disable --now ${FORWARD_PROXY.serviceName} 2>/dev/null || true`,
`sudo rm -f ${FORWARD_PROXY.unitPath}`,
'sudo systemctl daemon-reload',
]);
});
});
// Loopback only: an origin, the proxy handlers, and the client that drives
// them. No name resolution and no egress, so the path is deterministic.
describe('request and CONNECT relay', () => {
const listen = (server) => new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server)));
const port = (server) => server.address().port;
const servers = [];
let origin;
let proxy;
let proxyConfig;
before(async () => {
origin = await listen(
http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({ url: req.url, host: req.headers.host, relayed: req.headers['proxy-authorization'] }),
);
}),
);
proxy = await listen(http.createServer());
proxy.on('request', forwardProxyRequestHandlerFactory({ apiKey: API_KEY }));
proxy.on('connect', forwardProxyConnectHandlerFactory({ apiKey: API_KEY }));
servers.push(origin, proxy);
proxyConfig = { host: '127.0.0.1', port: port(proxy), apiKey: API_KEY };
});
after(() => {
for (const server of servers) server.close();
});
it('relays an http request and returns the origin answer', async () => {
const response = await fetchViaForwardProxy(`http://127.0.0.1:${port(origin)}/v2/instances?per_page=500`, {
proxy: proxyConfig,
headers: { authorization: 'Bearer vultr-key' },
});
expect(response.status).to.equal(200);
const body = JSON.parse(response.body);
expect(body.url).to.equal('/v2/instances?per_page=500');
expect(body.host).to.equal(`127.0.0.1:${port(origin)}`);
expect(body.relayed).to.equal(undefined);
});
it('answers 407 to a request with the wrong key, without reaching the origin', async () => {
const response = await fetchViaForwardProxy(`http://127.0.0.1:${port(origin)}/v2/instances`, {
proxy: { ...proxyConfig, apiKey: 'wrong-key' },
});
expect(response.status).to.equal(407);
expect(response.headers['proxy-authenticate']).to.include('Bearer');
});
it('answers 502 when the origin cannot be reached', async () => {
const response = await fetchViaForwardProxy('http://127.0.0.1:1/dead', { proxy: proxyConfig });
expect(response.status).to.equal(502);
});
it('rejects an unsupported target scheme before opening any socket', async () => {
try {
await fetchViaForwardProxy('ftp://origin.test/file', { proxy: proxyConfig });
expect.fail('only http: and https: targets are proxyable');
} catch (error) {
expect(error.message).to.include('http: and https:');
}
});
// CONNECT carries opaque bytes — TLS in production. A byte-echo origin proves
// the splice without a certificate, and covers the `head` bytes a client
// sends before the tunnel is established.
it('splices a CONNECT tunnel, delivering the bytes sent with the request', async () => {
const echo = await listen(net.createServer((socket) => socket.pipe(socket)));
servers.push(echo);
const tunnelled = await new Promise((resolve, reject) => {
const request = http.request({
host: '127.0.0.1',
port: port(proxy),
method: 'CONNECT',
path: `127.0.0.1:${port(echo)}`,
headers: { 'proxy-authorization': `Bearer ${API_KEY}` },
});
request.on('connect', (res, socket) => {
if (res.statusCode !== 200) return void reject(new Error(`CONNECT refused (${res.statusCode})`));
let received = '';
socket.on('data', (chunk) => {
received += chunk;
if (received.length >= 6) {
socket.destroy();
resolve(received);
}
});
socket.write('tunnel');
});
request.on('error', reject);
request.end();
});
expect(tunnelled).to.equal('tunnel');
});
it('refuses a CONNECT with the wrong key', async () => {
const status = await new Promise((resolve, reject) => {
const request = http.request({
host: '127.0.0.1',
port: port(proxy),
method: 'CONNECT',
path: '127.0.0.1:443',
headers: { 'proxy-authorization': 'Bearer wrong-key' },
});
request.on('connect', (res, socket) => {
socket.destroy();
resolve(res.statusCode);
});
// A refused CONNECT never becomes a tunnel, so it arrives as a response.
request.on('response', (res) => {
res.resume();
resolve(res.statusCode);
});
request.on('error', reject);
request.end();
});
expect(status).to.equal(407);
});
});
// SSH carries no SNI and no Host, so there is nothing to route it on. The port
// goes whole to the fallback spoke, exactly as UDP/443 does — it exists so CI,
// which has no fixed address, can reach a cluster node behind CGNAT.
describe('ssh forward', () => {
const SPOKE = [peerFactory({ id: 'homelab-a', address: '10.0.0.2', publicKey: 'AAA=', default: true })];
it('is closed by default, so no edge exposes SSH it was not told to', () => {
expect(edgeStateFactory({}).sshForwardPort).to.equal(0);
expect(edgeStateFactory({ sshForwardPort: 'nonsense' }).sshForwardPort).to.equal(0);
expect(edgeStateFactory({ sshForwardPort: 2222 }).sshForwardPort).to.equal(2222);
expect(haproxyConfFactory({ peers: SPOKE, defaultPeerId: 'homelab-a' })).to.not.include('fe_ssh');
});
it('binds the public port and sends it whole to the fallback spoke', () => {
const conf = haproxyConfFactory({ peers: SPOKE, defaultPeerId: 'homelab-a', sshForwardPort: 2222 });
expect(conf).to.include('frontend fe_ssh');
expect(conf).to.include('bind :2222');
expect(conf).to.include('default_backend be_ssh_default');
expect(conf).to.include(`server homelab-a 10.0.0.2:${UNDERPOST_EDGE.sshPort} check`);
});
// The server speaks first in SSH, so waiting on client bytes would stall
// every connection until the inspect timeout expired.
it('does not wait for client bytes the way the SNI frontend does', () => {
const conf = haproxyConfFactory({ peers: SPOKE, defaultPeerId: 'homelab-a', sshForwardPort: 2222 });
const directives = conf
.split('frontend fe_ssh')[1]
.split('\n\n')[0]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'));
expect(directives).to.include('mode tcp');
expect(directives.some((line) => line.startsWith('tcp-request'))).to.equal(false);
});
// Binding a port with nothing behind it would accept connections only to
// drop them, which reads as a broken host rather than a closed door.
it('emits nothing when there is no spoke to forward to', () => {
expect(haproxyConfFactory({ peers: [], sshForwardPort: 2222 })).to.not.include('fe_ssh');
});
it('opens the port publicly, and withdraws it on teardown', () => {
const opened = firewallCommandsFactory({ role: 'server', sshForwardPort: 2222 }).join('\n');
expect(opened).to.include('--add-port=2222/tcp');
const closed = firewallCommandsFactory({ role: 'server', sshForwardPort: 2222, remove: true }).join('\n');
expect(closed).to.include('--remove-port=2222/tcp');
});
it('opens no SSH port when none is configured, and never on a spoke', () => {
expect(firewallCommandsFactory({ role: 'server' }).join('\n')).to.not.include('2222');
expect(firewallCommandsFactory({ role: 'client', sshForwardPort: 2222 }).join('\n')).to.not.include('2222');
});
});
});