This commit is contained in:
Anthony Maio 2026-01-30 10:51:14 -05:00 committed by GitHub
commit 455c85348d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 1 deletions

View File

@ -6,7 +6,7 @@ import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
import { note } from "../terminal/note.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { isLoopbackHost, resolveGatewayBindHost } from "../gateway/net.js";
import { isLoopbackHost, resolveGatewayBindHost, getPublicIPs, isPrivateIP } from "../gateway/net.js";
export async function noteSecurityWarnings(cfg: OpenClawConfig) {
const warnings: string[] = [];
@ -44,6 +44,12 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
const bindDescriptor = `"${gatewayBind}" (${resolvedBindHost})`;
if (isExposed) {
// Check for public IP exposure (this is how installations end up on Shodan)
const publicIPs = getPublicIPs();
const hasPublicIP = publicIPs.length > 0;
const isBindingToPublicIP =
resolvedBindHost === "0.0.0.0" || !isPrivateIP(resolvedBindHost);
if (!hasSharedSecret) {
const authFixLines =
resolvedAuth.mode === "password"
@ -70,6 +76,16 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
` Ensure your auth credentials are strong and not exposed.`,
);
}
// Additional warning for public IP exposure (Shodan risk)
if (isBindingToPublicIP && hasPublicIP) {
warnings.push(
`- WARNING: System has public IP(s): ${publicIPs.join(", ")}`,
` Binding to ${resolvedBindHost} exposes your gateway to the internet.`,
` This is how installations end up on Shodan.`,
` For remote access, consider: VPN/Tailscale (preferred) or HTTPS reverse proxy.`,
);
}
}
const warnDmPolicy = async (params: {

View File

@ -1,7 +1,92 @@
import net from "node:net";
import os from "node:os";
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
/**
* Check if an IPv4 address is in a private range (RFC 1918 + loopback + link-local).
*
* Private ranges:
* - 10.0.0.0/8
* - 172.16.0.0/12
* - 192.168.0.0/16
* - 127.0.0.0/8 (loopback)
* - 169.254.0.0/16 (link-local)
*/
export function isPrivateIPv4(ip: string): boolean {
// 10.0.0.0/8
if (ip.startsWith("10.")) return true;
// 172.16.0.0/12 (172.16.x.x - 172.31.x.x)
if (ip.startsWith("172.")) {
const parts = ip.split(".");
if (parts.length >= 2) {
const second = parseInt(parts[1], 10);
if (!Number.isNaN(second) && second >= 16 && second <= 31) return true;
}
}
// 192.168.0.0/16
if (ip.startsWith("192.168.")) return true;
// 127.0.0.0/8 (loopback)
if (ip.startsWith("127.")) return true;
// 169.254.0.0/16 (link-local)
if (ip.startsWith("169.254.")) return true;
return false;
}
/**
* Check if an IPv6 address is in a private/local range.
*
* Private ranges:
* - fc00::/7 (unique local addresses - fc00:: and fd00::)
* - fe80::/10 (link-local)
* - ::1 (loopback)
*/
export function isPrivateIPv6(ip: string): boolean {
const lower = ip.toLowerCase();
// fc00::/7 (unique local) - starts with fc or fd
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
// fe80::/10 (link-local)
if (lower.startsWith("fe80")) return true;
// ::1 (loopback)
if (lower === "::1") return true;
// ::ffff: mapped IPv4 - check the IPv4 portion
if (lower.startsWith("::ffff:")) {
const ipv4Part = ip.slice("::ffff:".length);
return isPrivateIPv4(ipv4Part);
}
return false;
}
/**
* Check if an IP address (v4 or v6) is private/local.
*/
export function isPrivateIP(ip: string): boolean {
if (!ip) return false;
const trimmed = ip.trim();
if (trimmed.includes(":")) return isPrivateIPv6(trimmed);
return isPrivateIPv4(trimmed);
}
/**
* Get all public (non-private) IP addresses from network interfaces.
* Useful for warning users when they bind to 0.0.0.0 on a machine with public IPs.
*/
export function getPublicIPs(): string[] {
const interfaces = os.networkInterfaces();
const publicIPs: string[] = [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name] ?? []) {
if (iface.internal) continue;
if (!isPrivateIP(iface.address)) {
publicIPs.push(iface.address);
}
}
}
return publicIPs;
}
export function isLoopbackAddress(ip: string | undefined): boolean {
if (!ip) return false;
if (ip === "127.0.0.1") return true;

View File

@ -3,6 +3,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import type { loadConfig } from "../config/config.js";
import { getResolvedLoggerSettings } from "../logging.js";
import { getPublicIPs, isPrivateIP } from "./net.js";
export function logGatewayStartup(params: {
cfg: ReturnType<typeof loadConfig>;
@ -37,4 +38,60 @@ export function logGatewayStartup(params: {
if (params.isNixMode) {
params.log.info("gateway: running in Nix mode (config managed externally)");
}
// Warn if binding to 0.0.0.0 or a public IP
warnIfPublicBind(params.bindHost, params.port, params.log);
}
/**
* Warn if the gateway is binding to an address that exposes it to the public internet.
* This is how installations end up on Shodan - users deploy on a VPS, bind to 0.0.0.0
* or a public interface, and expose their gateway to the internet.
*/
function warnIfPublicBind(
bindAddress: string,
port: number,
log: { info: (msg: string, meta?: Record<string, unknown>) => void },
): void {
// 0.0.0.0 or :: binds to all interfaces
if (bindAddress === "0.0.0.0" || bindAddress === "::" || bindAddress === "") {
const publicIPs = getPublicIPs();
if (publicIPs.length > 0) {
const warning = [
"",
chalk.bgYellow.black(" WARNING ") + chalk.yellow(" Gateway exposed on public interface(s)"),
chalk.dim("─".repeat(60)),
` Bind: ${chalk.cyan(`${bindAddress}:${port}`)}`,
` Public IPs: ${chalk.red(publicIPs.join(", "))}`,
"",
chalk.dim(" This is how installations end up on Shodan."),
"",
chalk.dim(" If intentional: ensure HTTPS + strong auth"),
chalk.dim(" Recommended: use VPN/Tailscale or loopback + reverse proxy"),
chalk.dim("─".repeat(60)),
"",
].join("\n");
log.info(warning, { consoleMessage: warning });
}
return;
}
// Specific IP bind - check if it's a public IP
if (!isPrivateIP(bindAddress)) {
const warning = [
"",
chalk.bgYellow.black(" WARNING ") + chalk.yellow(" Gateway binding to public IP"),
chalk.dim("─".repeat(60)),
` Bind: ${chalk.red(`${bindAddress}:${port}`)}`,
"",
chalk.dim(" Binding to a public IP exposes your gateway to anyone"),
chalk.dim(" on the internet who scans your IP."),
"",
chalk.dim(" Recommended: bind to 127.0.0.1 and use a reverse proxy,"),
chalk.dim(" or use VPN/Tailscale for remote access."),
chalk.dim("─".repeat(60)),
"",
].join("\n");
log.info(warning, { consoleMessage: warning });
}
}