santifer/career-ops · error
Access denied: Egress guard blocked private target IPv6
Error message
Access denied: Egress guard blocked private target IPv6 ${ip} What it means
The IPv6 counterpart of the egress guard: validateUrlSecurity() blocks hostnames resolving to ::1 (loopback), fe80::/10 (link-local), or fc00::/7 (unique-local) addresses. Same SSRF rationale as the IPv4 check — a fetch to a resolved IPv6 private address is refused before any request is made.
Solutions
- Point the URL at a host whose AAAA record is a public (globally routable) IPv6 address.
- Prefer the file path / pasted-text input for internal or local content.
- If DNS wrongly publishes a private AAAA record for a public domain, correct the DNS record.
Example fix
// before node upskill.mjs --url http://[fd00::10]:8080/jd // after node upskill.mjs --file ./jd.md
Defensive patterns
Strategy: validation
Validate before calling
const { lookup } = await import('dns/promises');
const addrs = await lookup(new URL(url).hostname, { all: true, family: 6 });
const bad = a => a === '::1' || a.startsWith('fe80:') || a.startsWith('fc00:') || a.startsWith('fd00:');
if (addrs.some(a => bad(a.address))) console.error('hostname resolves to a private IPv6; use a public host or a file'); Try / catch
try {
await upskillFromUrl(url);
} catch (e) {
if (e.message.includes('blocked private target IPv6')) {
console.error('AAAA record is private (loopback/link-local/ULA); pass the JD as a file instead');
} else throw e;
} Prevention
- Inspect AAAA records (dig AAAA host) before using IPv6-reachable hosts.
- Remove stale /etc/hosts entries mapping hostnames to ::1 for hosts you intend to fetch.
- Use the file input path for any content on ULA (fd00::/8) networks.
When it happens
Trigger: A JD URL whose hostname's DNS AAAA record is ::1, fe80::/10, fc00::/7 (fc00::/fd00:: prefixes) — e.g. testing against an IPv6 loopback or a ULA-addressed internal host.
Common situations: Local dev servers advertising ::1 via /etc/hosts or mDNS; internal IPv6 networks using unique-local addresses; hosts with AAAA records pointing at private infrastructure while A records are public (the resolver returned the AAAA first).
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: Egress guard blocked private target IP
- Blocked request to restricted destination (DNS)
- plugin egress: resolves to a blocked address ( ) — possible…
- Access denied: Egress guard blocked private target IP
- Access denied: Localhost or internal domain target detected.
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/83c6b17e47685658.
Report an issue: GitHub.
Appendix: source
Thrown at upskill.mjs:877
throw new Error('Access denied: Localhost or internal domain target detected.');
}
let addresses;
if (dnsCache.has(hostname)) {
addresses = dnsCache.get(hostname);
} else {
addresses = await dns.resolve(hostname).catch(() => []);
const lookupRes = await dns.lookup(hostname).catch(() => null);
if (lookupRes) addresses.push(lookupRes.address);
dnsCache.set(hostname, addresses);
}
for (const ip of addresses) {
if (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|169\.254\.)/.test(ip)) {
throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);
}
if (ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:')) {
throw new Error(`Access denied: Egress guard blocked private target IPv6 ${ip}`);
}
}
return url.toString();
}
// --- CLI ---
// Everything below runs ONLY when upskill.mjs is the process entry point.
//
// Without this guard the module tail was unconditional, so `import
// { knownSkillsText } from './upskill.mjs'` re-parsed the IMPORTER's argv and ran
// one of these branches. That made the pure helpers above un-unit-testable despite
// their "exported for unit testing" docblocks — every assertion about them had to
// live inside --self-test.
//
// Under tests/ it also broke the harness, because test-all.mjs imports discovered
// suites IN-PROCESS and they therefore share its argv. Both branches were
// reachable, and both were measured by pinning isMain to true:
// - ordinary argv → the aggregate branch walked the tracker and every linkedView on GitHub (pinned to aac998c7ed)