Matches the Real-Debrid-Downloader's rd-diagnostics-mcp model so the read-only
diagnostics agent is reachable over Tailscale (or any private tunnel) the same way
the downloader is, instead of requiring an SSH local-forward.
- lib/ip-allowlist.js (NEW): fail-closed IP allowlist — normalizeIp strips
::ffff:, loopback is always allowed, an empty allowlist accepts loopback ONLY
(fail-closed), exact IP + CIDR (incl. the Tailscale CGNAT range 100.64.0.0/10) +
wildcard rules. The real socket peer IP is the authority (never a forwarded header).
- remote-server.js: rejects non-allowlisted peers at connection (close 4005). Opt-in
via config.allowlist (the existing remote-control server, which passes none, is
unaffected). Loopback always passes, so local + SSH-forward use keeps working.
- Two bind modes (config diagnostics.bindMode): "local" -> 127.0.0.1 (default),
"network" -> 0.0.0.0 but ONLY when a non-empty allowlist is set (else it stays
loopback, fail-closed). The allowlist + token gate access; the tunnel
(Tailscale/WireGuard) is the confidentiality layer (transport is still plaintext ws://).
- The connection code now carries the host: mhu1_<base64url{v,h,p,t,n,fp?,s?}>. The
gateway decode is tolerant of the legacy {port,token,label} keys; connect_server
takes the host from the code (host arg is an optional override). Proven end-to-end:
the integration harness now connects with NO host arg and resolves it from the code.
- Renderer: Sichtbarkeit selector (local/network), public-host input with
suggested-host chips (os.networkInterfaces — the Tailscale IP shows up there),
allowlist textarea (network mode), and network-requires-allowlist validation.
- main.js: bindMode->host, getSuggestedRemoteHosts, host-in-code, allowlist plumbed
into startDiagnosticAgent + the diagnostics IPC (get/save/status).
- docs: rewritten for Tailscale (set the allowlist to your tailnet, put the Tailscale
IP/MagicDNS in the code address — no SSH forward needed).
This supersedes the v3.3.85 hard loopback-lock with the downloader's allowlist model.
Tests: lib/ip-allowlist (8) + remote-server allowlist wiring/loopback (2) + gateway
decode (host short-key + legacy tolerance). 393 app tests + 9 gateway tests + e2e +
host-in-code integration + adversarial all green; lint 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
1.6 KiB
JavaScript
51 lines
1.6 KiB
JavaScript
function normalizeIp(ip) {
|
|
return String(ip || '').trim().replace(/^::ffff:/i, '').toLowerCase();
|
|
}
|
|
|
|
function isLoopbackIp(ip) {
|
|
const c = normalizeIp(ip);
|
|
return c === '' || c === '::1' || c === 'localhost' || /^127\./.test(c);
|
|
}
|
|
|
|
function ipv4ToInt(ip) {
|
|
const parts = String(ip).split('.');
|
|
if (parts.length !== 4) return null;
|
|
let n = 0;
|
|
for (const p of parts) {
|
|
if (!/^\d{1,3}$/.test(p)) return null;
|
|
const v = Number(p);
|
|
if (v < 0 || v > 255) return null;
|
|
n = (n << 8) + v;
|
|
}
|
|
return n >>> 0;
|
|
}
|
|
|
|
function matchIpRule(clientIp, rule) {
|
|
const client = normalizeIp(clientIp);
|
|
const r = String(rule || '').trim().toLowerCase();
|
|
if (!r) return false;
|
|
if (r === '*' || r === '0.0.0.0/0') return true;
|
|
if (r === client) return true;
|
|
const slash = r.indexOf('/');
|
|
if (slash > 0) {
|
|
const baseInt = ipv4ToInt(r.slice(0, slash));
|
|
const clientInt = ipv4ToInt(client);
|
|
const bits = Number(r.slice(slash + 1));
|
|
if (baseInt === null || clientInt === null || !Number.isInteger(bits) || bits < 0 || bits > 32) return false;
|
|
if (bits === 0) return true;
|
|
const mask = bits === 32 ? 0xffffffff : (~((1 << (32 - bits)) - 1)) >>> 0;
|
|
return (clientInt & mask) === (baseInt & mask);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function evaluateClientAllowed(clientIp, rules) {
|
|
const client = normalizeIp(clientIp);
|
|
if (isLoopbackIp(client)) return true;
|
|
const list = Array.isArray(rules) ? rules : [];
|
|
if (list.length === 0) return false;
|
|
return list.some((rule) => matchIpRule(client, rule));
|
|
}
|
|
|
|
module.exports = { normalizeIp, isLoopbackIp, ipv4ToInt, matchIpRule, evaluateClientAllowed };
|