import fs from "node:fs"; import type { IncomingMessage } from "node:http"; import net from "node:net"; import type { GatewayBindMode } from "../config/types.gateway.js"; import {
pickMatchingExternalInterfaceAddress,
readNetworkInterfaces,
} from "../infra/network-interfaces.js"; import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js"; import {
isCanonicalDottedDecimalIPv4,
isIpInCidr,
isLoopbackIpAddress,
isPrivateOrLoopbackIpAddress,
normalizeIpAddress,
} from "../shared/net/ip.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export function normalizeHostHeader(hostHeader?: string): string { return normalizeLowercaseStringOrEmpty(hostHeader);
}
export function resolveHostName(hostHeader?: string): string { const host = normalizeHostHeader(hostHeader); if (!host) { return"";
} if (host.startsWith("[")) { const end = host.indexOf("]"); if (end !== -1) { return host.slice(1, end);
}
} // Unbracketed IPv6 host (e.g. "::1") has no port and should be returned as-is. if (net.isIP(host) === 6) { return host;
} const [name] = host.split(":"); return name ?? "";
}
const forwardedChain: string[] = []; for (const entry of forwardedFor?.split(",") ?? []) { const normalized = parseIpLiteral(entry); if (normalized) {
forwardedChain.push(normalized);
}
} if (forwardedChain.length === 0) { return undefined;
}
// Walk right-to-left and return the first untrusted hop. for (let index = forwardedChain.length - 1; index >= 0; index -= 1) { const hop = forwardedChain[index]; if (isLoopbackAddress(hop)) { continue;
} if (!isTrustedProxyAddress(hop, trustedProxies)) { return hop;
}
} return undefined;
}
export function resolveClientIp(params: {
remoteAddr?: string;
forwardedFor?: string;
realIp?: string;
trustedProxies?: string[]; /** Default false: only trust X-Real-IP when explicitly enabled. */
allowRealIpFallback?: boolean;
}): string | undefined { const remote = normalizeIp(params.remoteAddr); if (!remote) { return undefined;
} if (!isTrustedProxyAddress(remote, params.trustedProxies)) { return remote;
} // Fail closed when traffic comes from a trusted proxy but client-origin headers // are missing or invalid. Falling back to the proxy's own IP can accidentally // treat unrelated requests as local/trusted. const forwardedIp = resolveForwardedClientIp({
forwardedFor: params.forwardedFor,
trustedProxies: params.trustedProxies,
}); if (forwardedIp) { return forwardedIp;
} if (params.allowRealIpFallback) { return parseRealIp(params.realIp);
} return undefined;
}
if (mode === "loopback") { // 127.0.0.1 rarely fails, but handle gracefully if (await canBindToHost("127.0.0.1")) { return"127.0.0.1";
} return"0.0.0.0"; // extreme fallback
}
if (mode === "tailnet") { const tailnetIP = pickPrimaryTailnetIPv4(); if (tailnetIP && (await canBindToHost(tailnetIP))) { return tailnetIP;
} if (await canBindToHost("127.0.0.1")) { return"127.0.0.1";
} return"0.0.0.0";
}
if (mode === "lan") { return"0.0.0.0";
}
if (mode === "custom") { const host = customHost?.trim(); if (!host) { return"0.0.0.0";
} // invalid config → fall back to all
if (isValidIPv4(host) && (await canBindToHost(host))) { return host;
} // Custom IP failed → fall back to LAN return"0.0.0.0";
}
if (mode === "auto") { // Inside a container, loopback is unreachable from the host network // namespace, so prefer 0.0.0.0 to make port-forwarding work. if (isContainerEnvironment()) { return"0.0.0.0";
} if (await canBindToHost("127.0.0.1")) { return"127.0.0.1";
} return"0.0.0.0";
}
/** *SecuritycheckforWebSocketURLs(CWE-319:CleartextTransmissionofSensitiveInformation). * *ReturnstrueiftheURLissecurefortransmittingdata: *-wss:// (TLS) is always secure *-ws:// is secure only for loopback addresses by default *-optionalbreak-glass:privatews:// can be enabled for trusted networks * *Allotherws:// URLs are considered insecure because both credentials *ANDchat/conversationdatawouldbeexposedtonetworkinterception.
*/
export function isSecureWebSocketUrl(
url: string,
opts?: {
allowPrivateWs?: boolean;
},
): boolean {
let parsed: URL; try {
parsed = new URL(url);
} catch { returnfalse;
}
// Node's ws client accepts http(s) URLs and normalizes them to ws(s). // Treat those aliases the same way here so loopback cron announce delivery // and TLS-backed https endpoints follow the same security policy. const protocol =
parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol;
if (protocol === "wss:") { returntrue;
}
if (protocol !== "ws:") { returnfalse;
}
// Default policy stays strict: loopback-only plaintext ws://. if (isLoopbackHost(parsed.hostname)) { returntrue;
} // Optional break-glass for trusted private-network overlays. if (opts?.allowPrivateWs) { if (isPrivateOrLoopbackHost(parsed.hostname)) { returntrue;
} // Hostnames may resolve to private networks (for example in VPN/Tailnet DNS), // but resolution is not available in this synchronous validator. const hostForIpCheck =
parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]")
? parsed.hostname.slice(1, -1)
: parsed.hostname; return net.isIP(hostForIpCheck) === 0;
} returnfalse;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.19 Sekunden
(vorverarbeitet am 2026-06-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.