santifer/career-ops · error · Error
plugin egress to is blocked (private/loopback/metadata…
Error message
plugin egress to ${hostname} is blocked (private/loopback/metadata range) What it means
resolveAndValidate in the plugin egress guard resolves a hostname to IP addresses and blocks any address in private, loopback, link-local, or cloud-metadata ranges. For a hostname that is already an IP literal, it validates directly via isBlockedIp and throws this error when the IP falls in a blocked range. The guard exists to stop SSRF: plugins must not be able to reach internal services, localhost, or metadata endpoints (e.g. 169.254.169.254).
Solutions
- If the target is a legitimate local AI provider (Ollama/LM Studio), pass { allowsLocalhost: true } and use a loopback literal (127.0.0.1) — private non-loopback IPs stay blocked.
- Change the plugin's endpoint to a public hostname/IP; the block is intentional for anything private.
- If the endpoint must be internal, move it behind an approved public gateway/proxy rather than weakening the guard.
- Check the hostname value in the plugin config for typos (0.0.0.0, 169.254.x, LAN addresses).
Example fix
// before: localhost target blocked because allowsLocalhost not passed
await resolveAndValidate("127.0.0.1:11434");
// after: explicitly allow loopback for local providers
await resolveAndValidate("127.0.0.1", { allowsLocalhost: true }); Defensive patterns
Strategy: validation
Validate before calling
import { isIP } from 'net';
const BLOCKED_RE = /^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0$)/;
function isPublicEndpoint(hostname) {
const host = hostname.replace(/:\d+$/, '');
if (isIP(host)) return !BLOCKED_RE.test(host);
return !['localhost', 'metadata.google.internal'].includes(host.toLowerCase());
}
// before calling: isPublicEndpoint(hostname) || explicitlyPassingAllowsLocalhost Type guard
function isLoopbackLiteral(host) {
return isIP(host) !== 0 && /^(127\.|::1$)/.test(host);
} Try / catch
try {
await resolveAndValidate(hostname, { allowsLocalhost });
} catch (err) {
if (err.message.includes('is blocked (private/loopback/metadata range)')) {
console.error(`Endpoint ${hostname} is private/metadata — use a public endpoint or allowsLocalhost with 127.0.0.1`);
}
throw err;
} Prevention
- Use public hostnames for plugin API endpoints; never point plugins at LAN or metadata IPs.
- Pass allowsLocalhost: true only for legitimate local AI providers, with 127.0.0.1 as the host.
- Validate configured URLs against blocked ranges at config-load time, not request time.
- Treat this error as an SSRF signal — audit the plugin config that produced it.
When it happens
Trigger: Calling resolveAndValidate with (a) an IP-literal hostname in a private range (10.x, 192.168.x, 127.x, 169.254.x, etc.) without allowsLocalhost, or (b) such an IP with allowsLocalhost=true where the IP is private/metadata but not a loopback literal (the loopback exemption only covers 127.0.0.0/8-style literals).
Common situations: A plugin configured to call a local Ollama/LM Studio instance at 127.0.0.1 but the caller forgot to pass allowsLocalhost: true; a plugin pointed at an internal staging service on 10.x; a config using a metadata IP or a LAN address (192.168.1.x) as an API endpoint; typo'd base URL like http://0.0.0.0.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IP
- Blocked request to restricted destination
- plugin egress: cannot resolve
- plugin egress: resolves to a blocked address ( ) — possible…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/81844cb0f7866812.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/_net.mjs:81
if (a >= 224) return true; // multicast / reserved
return false;
}
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
/**
* Resolve a hostname and reject if ANY resolved address is blocked. Returns the
* validated addresses. Throws on a blocked or unresolvable host.
* @param {string} hostname
* @param {{ allowsLocalhost?: boolean }} [opts]
* @returns {Promise<string[]>}
*/
export async function resolveAndValidate(hostname, { allowsLocalhost = false } = {}) {
// An IP literal host: validate directly (no DNS).
if (isIP(hostname)) {
if (isBlockedIp(hostname)) {
if (allowsLocalhost && isLoopbackLiteral(hostname)) return [hostname];
throw new Error(`plugin egress to ${hostname} is blocked (private/loopback/metadata range)`);
}
return [hostname];
}
if (allowsLocalhost && LOOPBACK_HOSTS.has(hostname.toLowerCase())) {
// Local-AI providers (Ollama/LM Studio). Resolve but allow loopback through.
return ['127.0.0.1'];
}
let addrs;
try {
addrs = await dnsLookup(hostname, { all: true });
} catch (err) {
throw new Error(`plugin egress: cannot resolve ${hostname} — ${err.message}`);
}
if (!addrs.length) throw new Error(`plugin egress: ${hostname} resolved to no addresses`);
for (const { address } of addrs) {
if (isBlockedIp(address)) {View on GitHub (pinned to aac998c7ed)