Merge ebc11b6df1 into 09be5d45d5
This commit is contained in:
commit
d223cb69f7
@ -40,7 +40,7 @@ Notes:
|
||||
### Options
|
||||
|
||||
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
|
||||
- `--bind <loopback|lan|tailnet|auto|custom>`: listener bind mode.
|
||||
- `--bind <loopback|lan|tailnet|wireguard|auto|custom>`: listener bind mode.
|
||||
- `--auth <token|password>`: auth mode override.
|
||||
- `--token <token>`: token override (also sets `OPENCLAW_GATEWAY_TOKEN` for the process).
|
||||
- `--password <password>`: password override (also sets `OPENCLAW_GATEWAY_PASSWORD` for the process).
|
||||
|
||||
@ -3142,10 +3142,11 @@ Defaults:
|
||||
- bind: `lan` (binds to `0.0.0.0`)
|
||||
|
||||
Bind modes:
|
||||
- `lan`: `0.0.0.0` (reachable on any interface, including LAN/Wi‑Fi and Tailscale)
|
||||
- `tailnet`: bind only to the machine’s Tailscale IP (recommended for Vienna ⇄ London)
|
||||
- `lan`: `0.0.0.0` (reachable on any interface, including LAN/Wi-Fi and overlay networks)
|
||||
- `tailnet`: bind only to the machine's Tailscale IP (100.64.0.0/10 range)
|
||||
- `wireguard`: bind only to the machine's WireGuard mesh IP (wg0/wt0 interface - Netbird, Headscale, etc.)
|
||||
- `loopback`: `127.0.0.1` (local only)
|
||||
- `auto`: prefer tailnet IP if present, else `lan`
|
||||
- `auto`: prefer loopback if available, else `lan`
|
||||
|
||||
TLS:
|
||||
- `bridge.tls.enabled`: enable TLS for bridge connections (TLS-only when enabled).
|
||||
|
||||
@ -181,11 +181,14 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
bindRaw === "lan" ||
|
||||
bindRaw === "auto" ||
|
||||
bindRaw === "custom" ||
|
||||
bindRaw === "tailnet"
|
||||
bindRaw === "tailnet" ||
|
||||
bindRaw === "wireguard"
|
||||
? bindRaw
|
||||
: null;
|
||||
if (!bind) {
|
||||
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
|
||||
defaultRuntime.error(
|
||||
'Invalid --bind (use "loopback", "lan", "tailnet", "wireguard", "auto", or "custom")',
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@ -314,7 +317,7 @@ export function addGatewayRunCommand(cmd: Command): Command {
|
||||
.option("--port <port>", "Port for the gateway WebSocket")
|
||||
.option(
|
||||
"--bind <mode>",
|
||||
'Bind mode ("loopback"|"lan"|"tailnet"|"auto"|"custom"). Defaults to config gateway.bind (or loopback).',
|
||||
'Bind mode ("loopback"|"lan"|"tailnet"|"wireguard"|"auto"|"custom"). Defaults to config gateway.bind (or loopback).',
|
||||
)
|
||||
.option(
|
||||
"--token <token>",
|
||||
|
||||
@ -41,6 +41,11 @@ export async function promptGatewayConfig(
|
||||
label: "Tailnet (Tailscale IP)",
|
||||
hint: "Bind to your Tailscale IP only (100.x.x.x)",
|
||||
},
|
||||
{
|
||||
value: "wireguard",
|
||||
label: "WireGuard (WireGuard IP)",
|
||||
hint: "Bind to your WireGuard mesh IP (wg0/wt0 interface)",
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto (Loopback → LAN)",
|
||||
@ -59,7 +64,7 @@ export async function promptGatewayConfig(
|
||||
],
|
||||
}),
|
||||
runtime,
|
||||
) as "auto" | "lan" | "loopback" | "custom" | "tailnet";
|
||||
) as "auto" | "lan" | "loopback" | "custom" | "tailnet" | "wireguard";
|
||||
|
||||
let customBindHost: string | undefined;
|
||||
if (bind === "custom") {
|
||||
|
||||
@ -20,7 +20,14 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
|
||||
|
||||
const gatewayBind = (cfg.gateway?.bind ?? "loopback") as string;
|
||||
const customBindHost = cfg.gateway?.customBindHost?.trim();
|
||||
const bindModes: GatewayBindMode[] = ["auto", "lan", "loopback", "custom", "tailnet"];
|
||||
const bindModes: GatewayBindMode[] = [
|
||||
"auto",
|
||||
"lan",
|
||||
"loopback",
|
||||
"custom",
|
||||
"tailnet",
|
||||
"wireguard",
|
||||
];
|
||||
const bindMode = bindModes.includes(gatewayBind as GatewayBindMode)
|
||||
? (gatewayBind as GatewayBindMode)
|
||||
: undefined;
|
||||
|
||||
@ -12,6 +12,7 @@ import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
import { pickPrimaryWireguardIPv4 } from "../infra/wireguard.js";
|
||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||
import { isWSL } from "../infra/wsl.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
@ -393,7 +394,7 @@ export const DEFAULT_WORKSPACE = DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
|
||||
export function resolveControlUiLinks(params: {
|
||||
port: number;
|
||||
bind?: "auto" | "lan" | "loopback" | "custom" | "tailnet";
|
||||
bind?: "auto" | "lan" | "loopback" | "custom" | "tailnet" | "wireguard";
|
||||
customBindHost?: string;
|
||||
basePath?: string;
|
||||
}): { httpUrl: string; wsUrl: string } {
|
||||
@ -401,11 +402,13 @@ export function resolveControlUiLinks(params: {
|
||||
const bind = params.bind ?? "loopback";
|
||||
const customBindHost = params.customBindHost?.trim();
|
||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||
const wireguardIPv4 = pickPrimaryWireguardIPv4();
|
||||
const host = (() => {
|
||||
if (bind === "custom" && customBindHost && isValidIPv4(customBindHost)) {
|
||||
return customBindHost;
|
||||
}
|
||||
if (bind === "tailnet" && tailnetIPv4) return tailnetIPv4 ?? "127.0.0.1";
|
||||
if (bind === "wireguard" && wireguardIPv4) return wireguardIPv4 ?? "127.0.0.1";
|
||||
return "127.0.0.1";
|
||||
})();
|
||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||
|
||||
@ -92,7 +92,7 @@ export async function runNonInteractiveOnboardingLocal(params: {
|
||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
if (!opts.skipHealth) {
|
||||
const links = resolveControlUiLinks({
|
||||
bind: gatewayResult.bind as "auto" | "lan" | "loopback" | "custom" | "tailnet",
|
||||
bind: gatewayResult.bind as "auto" | "lan" | "loopback" | "custom" | "tailnet" | "wireguard",
|
||||
port: gatewayResult.port,
|
||||
customBindHost: nextConfig.gateway?.customBindHost,
|
||||
basePath: undefined,
|
||||
|
||||
@ -35,7 +35,7 @@ export type AuthChoice =
|
||||
| "skip";
|
||||
export type GatewayAuthChoice = "token" | "password";
|
||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||
export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet";
|
||||
export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard";
|
||||
export type TailscaleMode = "off" | "serve" | "funnel";
|
||||
export type NodeManagerChoice = "npm" | "pnpm" | "bun";
|
||||
export type ChannelChoice = ChannelId;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom" | "tailnet";
|
||||
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom" | "tailnet" | "wireguard";
|
||||
|
||||
export type GatewayTlsConfig = {
|
||||
/** Enable TLS for the gateway server. */
|
||||
@ -221,6 +221,7 @@ export type GatewayConfig = {
|
||||
* - lan: 0.0.0.0 (all interfaces, no fallback)
|
||||
* - loopback: 127.0.0.1 (local-only)
|
||||
* - tailnet: Tailnet IPv4 if available (100.64.0.0/10), else loopback
|
||||
* - wireguard: WireGuard IPv4 if available (wg0/wt0 interface), else loopback
|
||||
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable (requires customBindHost)
|
||||
* Default: loopback (127.0.0.1).
|
||||
*/
|
||||
|
||||
@ -312,6 +312,7 @@ export const OpenClawSchema = z
|
||||
z.literal("loopback"),
|
||||
z.literal("custom"),
|
||||
z.literal("tailnet"),
|
||||
z.literal("wireguard"),
|
||||
])
|
||||
.optional(),
|
||||
controlUi: z
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import net from "node:net";
|
||||
|
||||
import { pickPrimaryWireguardIPv4 } from "../infra/wireguard.js";
|
||||
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
|
||||
|
||||
export function isLoopbackAddress(ip: string | undefined): boolean {
|
||||
@ -74,6 +75,8 @@ export function isLocalGatewayAddress(ip: string | undefined): boolean {
|
||||
if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) return true;
|
||||
const tailnetIPv6 = pickPrimaryTailnetIPv6();
|
||||
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) return true;
|
||||
const wireguardIPv4 = pickPrimaryWireguardIPv4();
|
||||
if (wireguardIPv4 && normalized === wireguardIPv4.toLowerCase()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -83,7 +86,8 @@ export function isLocalGatewayAddress(ip: string | undefined): boolean {
|
||||
* Modes:
|
||||
* - loopback: 127.0.0.1 (rarely fails, but handled gracefully)
|
||||
* - lan: always 0.0.0.0 (no fallback)
|
||||
* - tailnet: Tailnet IPv4 if available, else loopback
|
||||
* - tailnet: Tailnet IPv4 if available (100.64.0.0/10), else loopback
|
||||
* - wireguard: WireGuard IPv4 if available (wg0/wt0 interface), else loopback
|
||||
* - auto: Loopback if available, else 0.0.0.0
|
||||
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable
|
||||
*
|
||||
@ -94,17 +98,24 @@ export async function resolveGatewayBindHost(
|
||||
customHost?: string,
|
||||
): Promise<string> {
|
||||
const mode = bind ?? "loopback";
|
||||
|
||||
const localhost = "127.0.0.1";
|
||||
if (mode === "loopback") {
|
||||
// 127.0.0.1 rarely fails, but handle gracefully
|
||||
if (await canBindToHost("127.0.0.1")) return "127.0.0.1";
|
||||
if (await canBindToHost(localhost)) return localhost;
|
||||
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";
|
||||
if (await canBindToHost(localhost)) return localhost;
|
||||
return "0.0.0.0";
|
||||
}
|
||||
|
||||
if (mode === "wireguard") {
|
||||
const wgIP = pickPrimaryWireguardIPv4();
|
||||
if (wgIP && (await canBindToHost(wgIP))) return wgIP;
|
||||
if (await canBindToHost(localhost)) return localhost;
|
||||
return "0.0.0.0";
|
||||
}
|
||||
|
||||
@ -122,7 +133,7 @@ export async function resolveGatewayBindHost(
|
||||
}
|
||||
|
||||
if (mode === "auto") {
|
||||
if (await canBindToHost("127.0.0.1")) return "127.0.0.1";
|
||||
if (await canBindToHost(localhost)) return localhost;
|
||||
return "0.0.0.0";
|
||||
}
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ export const testState = {
|
||||
allowFrom: undefined as string[] | undefined,
|
||||
cronStorePath: undefined as string | undefined,
|
||||
cronEnabled: false as boolean | undefined,
|
||||
gatewayBind: undefined as "auto" | "lan" | "tailnet" | "loopback" | undefined,
|
||||
gatewayBind: undefined as "auto" | "lan" | "tailnet" | "wireguard" | "loopback" | undefined,
|
||||
gatewayAuth: undefined as Record<string, unknown> | undefined,
|
||||
gatewayControlUi: undefined as Record<string, unknown> | undefined,
|
||||
hooksConfig: undefined as HooksConfig | undefined,
|
||||
|
||||
44
src/infra/wireguard.ts
Normal file
44
src/infra/wireguard.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import os from "node:os";
|
||||
|
||||
export type WireguardAddresses = {
|
||||
ipv4: string[];
|
||||
ipv6: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect WireGuard mesh VPN interfaces (Netbird wt*, standard wg*, etc.).
|
||||
* These typically use the 100.64.0.0/10 CGNAT range or private RFC1918 ranges.
|
||||
*/
|
||||
function isWireguardInterface(name: string): boolean {
|
||||
// wt0, wt1 (Netbird); wg0, wg1
|
||||
return /^(wt|wg)\d+$/.test(name);
|
||||
}
|
||||
|
||||
export function listWireguardAddresses(): WireguardAddresses {
|
||||
const ipv4: string[] = [];
|
||||
const ipv6: string[] = [];
|
||||
|
||||
const ifaces = os.networkInterfaces();
|
||||
for (const [ifaceName, entries] of Object.entries(ifaces)) {
|
||||
if (!entries) continue;
|
||||
if (!isWireguardInterface(ifaceName)) continue;
|
||||
|
||||
for (const e of entries) {
|
||||
if (!e || e.internal) continue;
|
||||
const address = e.address?.trim();
|
||||
if (!address) continue;
|
||||
if (e.family === "IPv4" || (e.family as unknown) === 4) {
|
||||
ipv4.push(address);
|
||||
}
|
||||
if (e.family === "IPv6" || (e.family as unknown) === 6) {
|
||||
ipv6.push(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ipv4: [...new Set(ipv4)], ipv6: [...new Set(ipv6)] };
|
||||
}
|
||||
|
||||
export function pickPrimaryWireguardIPv4(): string | undefined {
|
||||
return listWireguardAddresses().ipv4[0];
|
||||
}
|
||||
@ -96,11 +96,14 @@ async function main() {
|
||||
bindRaw === "lan" ||
|
||||
bindRaw === "auto" ||
|
||||
bindRaw === "custom" ||
|
||||
bindRaw === "tailnet"
|
||||
bindRaw === "tailnet" ||
|
||||
bindRaw === "wireguard"
|
||||
? bindRaw
|
||||
: null;
|
||||
if (!bind) {
|
||||
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
|
||||
defaultRuntime.error(
|
||||
'Invalid --bind (use "loopback", "lan", "tailnet", "wireguard", "auto", or "custom")',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@ -54,11 +54,12 @@ export async function configureGatewayForOnboarding(
|
||||
{ value: "loopback", label: "Loopback (127.0.0.1)" },
|
||||
{ value: "lan", label: "LAN (0.0.0.0)" },
|
||||
{ value: "tailnet", label: "Tailnet (Tailscale IP)" },
|
||||
{ value: "wireguard", label: "WireGuard (WireGuard IP)" },
|
||||
{ value: "auto", label: "Auto (Loopback → LAN)" },
|
||||
{ value: "custom", label: "Custom IP" },
|
||||
],
|
||||
})) as "loopback" | "lan" | "auto" | "custom" | "tailnet")
|
||||
) as "loopback" | "lan" | "auto" | "custom" | "tailnet";
|
||||
})) as "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard")
|
||||
) as "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard";
|
||||
|
||||
let customBindHost = quickstartGateway.customBindHost;
|
||||
if (bind === "custom") {
|
||||
|
||||
@ -236,11 +236,14 @@ export async function runOnboardingWizard(
|
||||
})();
|
||||
|
||||
if (flow === "quickstart") {
|
||||
const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => {
|
||||
const formatBind = (
|
||||
value: "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard",
|
||||
) => {
|
||||
if (value === "loopback") return "Loopback (127.0.0.1)";
|
||||
if (value === "lan") return "LAN";
|
||||
if (value === "custom") return "Custom IP";
|
||||
if (value === "tailnet") return "Tailnet (Tailscale IP)";
|
||||
if (value === "wireguard") return "WireGuard (WireGuard IP)";
|
||||
return "Auto";
|
||||
};
|
||||
const formatAuth = (value: GatewayAuthChoice) => {
|
||||
|
||||
@ -5,7 +5,7 @@ export type WizardFlow = "quickstart" | "advanced";
|
||||
export type QuickstartGatewayDefaults = {
|
||||
hasExisting: boolean;
|
||||
port: number;
|
||||
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet";
|
||||
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard";
|
||||
authMode: GatewayAuthChoice;
|
||||
tailscaleMode: "off" | "serve" | "funnel";
|
||||
token?: string;
|
||||
@ -16,7 +16,7 @@ export type QuickstartGatewayDefaults = {
|
||||
|
||||
export type GatewayWizardSettings = {
|
||||
port: number;
|
||||
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet";
|
||||
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet" | "wireguard";
|
||||
customBindHost?: string;
|
||||
authMode: GatewayAuthChoice;
|
||||
gatewayToken?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user