refactor(infra): remove unused Windows firewall probes (#117662)

This commit is contained in:
Peter Steinberger
2026-08-01 15:42:40 -07:00
committed by GitHub
parent 08ea9cc31d
commit 9f3b44a141
3 changed files with 177 additions and 502 deletions
-1
View File
@@ -825,7 +825,6 @@ src/infra/update-managed-service-handoff.ts
src/infra/update-runner.test.ts
src/infra/update-startup.test.ts
src/infra/update-startup.ts
src/infra/windows-gateway-firewall-diagnostics.ts
src/interactive/payload.ts
src/llm/providers/stream-wrappers/openai.ts
src/logging/diagnostic-stability-bundle.ts
@@ -1,7 +1,7 @@
// Windows Gateway firewall diagnostics classify LAN reachability risks.
import { describe, expect, it, vi } from "vitest";
import { inspectWindowsGatewayFirewall } from "./windows-gateway-firewall-diagnostics.js";
import { getWindowsPowerShellExePath, getWindowsSystem32ExePath } from "./windows-install-roots.js";
import { getWindowsPowerShellExePath } from "./windows-install-roots.js";
type InspectOptions = Parameters<typeof inspectWindowsGatewayFirewall>[0];
type FirewallCommandRunner = NonNullable<InspectOptions["runCommandWithTimeout"]>;
@@ -102,8 +102,8 @@ function ruleJson(params?: {
function quickPayloadJson(params?: {
state?: string;
activeRules?: Array<Record<string, unknown>>;
localRules?: Array<Record<string, unknown>>;
activeRules?: unknown[];
localRules?: unknown[];
}) {
return JSON.stringify({
State: JSON.parse(params?.state ?? stateJson({ localAllowRules: "True" })),
@@ -139,7 +139,7 @@ function ruleRow(params?: {
};
}
async function classify(params: { stateJson: string; rulesJson: string; netshOutput?: string }) {
async function classify(params: { stateJson: string; rulesJson: string }) {
const parsedRules = JSON.parse(params.rulesJson) as unknown;
const rulePayload =
parsedRules && typeof parsedRules === "object" && !Array.isArray(parsedRules)
@@ -151,25 +151,10 @@ async function classify(params: { stateJson: string; rulesJson: string; netshOut
: Array.isArray(parsedRules)
? parsedRules
: [];
const runner: FirewallCommandRunner = async (argv) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: params.stateJson };
}
if (command.includes("HNetCfg.FwPolicy2")) {
return { code: 0, stdout: JSON.stringify(localRules) };
}
if (command.includes("PolicyStore ActiveStore")) {
return { code: 0, stdout: JSON.stringify(activeRules) };
}
if (command.includes("PolicyStore PersistentStore")) {
return { code: 0, stdout: JSON.stringify(localRules) };
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: params.netshOutput ?? "" };
}
throw new Error(`unexpected command: ${command}`);
};
const runner: FirewallCommandRunner = async () => ({
code: 0,
stdout: quickPayloadJson({ state: params.stateJson, activeRules, localRules }),
});
return await inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
@@ -214,7 +199,6 @@ describe("Windows Gateway firewall diagnostics", () => {
localAllowRules: "NotConfigured",
}),
rulesJson: ruleJson(),
netshOutput: "LocalFirewallRules N/A (GPO-store only)",
});
expect(diagnostic).toMatchObject({
@@ -222,7 +206,9 @@ describe("Windows Gateway firewall diagnostics", () => {
severity: "warning",
code: "windows_firewall_local_rules_ignored",
});
expect(diagnostic.details.join("\n")).toContain("GPO-store only");
expect(diagnostic.details.join("\n")).toContain(
"Local firewall rules are disabled for the active profile.",
);
});
it("detects ignored local rules even when they are absent from ActiveStore", async () => {
@@ -235,7 +221,6 @@ describe("Windows Gateway firewall diagnostics", () => {
active: [],
local: [ruleRow()],
}),
netshOutput: "LocalFirewallRules N/A (GPO-store only)",
});
expect(diagnostic).toMatchObject({
@@ -267,7 +252,6 @@ describe("Windows Gateway firewall diagnostics", () => {
classify({
stateJson: stateJson({ localAllowRules: "NotConfigured" }),
rulesJson: ruleJson(),
netshOutput: "LocalFirewallRules N/A (GPO-store only)",
}),
).resolves.toMatchObject({
applies: true,
@@ -381,19 +365,10 @@ describe("Windows Gateway firewall diagnostics", () => {
});
it("classifies empty successful rule output as no allow rule", async () => {
const runner = vi.fn<FirewallCommandRunner>(async (argv) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: stateJson() };
}
if (command.includes("HNetCfg.FwPolicy2")) {
return { code: 0, stdout: "" };
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: "" };
}
throw new Error(`unexpected command: ${command}`);
});
const runner = vi.fn<FirewallCommandRunner>(async () => ({
code: 0,
stdout: quickPayloadJson({ state: stateJson(), activeRules: [], localRules: [] }),
}));
await expect(
inspectWindowsGatewayFirewall({
@@ -405,56 +380,75 @@ describe("Windows Gateway firewall diagnostics", () => {
).resolves.toMatchObject({
code: "windows_firewall_no_allow_rule",
});
expect(runner).toHaveBeenCalledTimes(1);
});
it("fails closed when firewall rule output is truncated", async () => {
const runner = vi.fn<FirewallCommandRunner>(async (argv) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: stateJson() };
}
if (command.includes("HNetCfg.FwPolicy2")) {
return { code: 0, stdout: ruleJson(), stdoutTruncatedBytes: 1 };
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: "" };
}
throw new Error(`unexpected command: ${command}`);
});
it.each([
[
"a nonzero exit",
async () => ({ code: 1, stdout: quickPayloadJson() }),
"OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
],
[
"truncated stdout",
async () => ({ code: 0, stdout: quickPayloadJson(), stdoutTruncatedBytes: 1 }),
"OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
],
[
"truncated stderr",
async () => ({ code: 0, stdout: quickPayloadJson(), stderrTruncatedBytes: 1 }),
"OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
],
[
"malformed JSON",
async () => ({ code: 0, stdout: "{" }),
"OpenClaw could not parse Windows Firewall LAN Gateway policy.",
],
[
"a runner exception",
async () => {
throw new Error("probe failed");
},
"OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
],
] satisfies Array<[string, FirewallCommandRunner, string]>)(
"fails closed after %s",
async (_label, run, message) => {
const runner = vi.fn<FirewallCommandRunner>(run);
await expect(
inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: runner,
await expect(
inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: runner,
}),
).resolves.toEqual({
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message,
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
});
expect(runner).toHaveBeenCalledTimes(1);
expect(runner.mock.calls[0]?.[1]).toEqual({
timeoutMs: 5_000,
maxOutputBytes: 2 * 1024 * 1024,
});
},
);
it("reports disabled local-rule policy without follow-up probes", async () => {
const runner = vi.fn<FirewallCommandRunner>(async () => ({
code: 0,
stdout: quickPayloadJson({
state: stateJson({ activeAllowLocalRules: "False" }),
activeRules: [],
localRules: [],
}),
).resolves.toMatchObject({
code: "windows_firewall_inspection_failed",
});
});
it("reports local-rule policy when the persistent detail probe is unavailable", async () => {
const runner = vi.fn<FirewallCommandRunner>(async (argv, opts) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: stateJson({ activeAllowLocalRules: "False" }) };
}
if (command.includes("HNetCfg.FwPolicy2")) {
return { code: 0, stdout: "" };
}
if (command.includes("PolicyStore ActiveStore")) {
return { code: 0, stdout: "" };
}
if (command.includes("PolicyStore PersistentStore")) {
expect(opts.timeoutMs).toBeGreaterThanOrEqual(10_000);
return { code: null, stdout: "" };
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: "LocalFirewallRules N/A (GPO-store only)" };
}
throw new Error(`unexpected command: ${command}`);
});
}));
await expect(
inspectWindowsGatewayFirewall({
@@ -466,35 +460,30 @@ describe("Windows Gateway firewall diagnostics", () => {
).resolves.toMatchObject({
code: "windows_firewall_local_rules_ignored",
});
expect(runner).toHaveBeenCalledTimes(1);
});
it("preserves managed ActiveStore allow rules when local rules are disabled", async () => {
it("preserves managed ActiveStore allow rules by default", async () => {
const runner = vi.fn<FirewallCommandRunner>(async (argv) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: stateJson({ activeAllowLocalRules: "False" }) };
}
if (command.includes("HNetCfg.FwPolicy2")) {
return { code: 0, stdout: ruleJson({ displayName: "Ignored local allow" }) };
}
if (command.includes("PolicyStore ActiveStore")) {
expect(command).toContain("requestedPolicyStoreSourceTypes");
expect(command).toContain("-ieq");
expect(command).toContain("GroupPolicy");
expect(command).toContain("MDM");
return {
code: 0,
stdout: ruleJson({
displayName: "MDM-managed Gateway allow",
policyStoreSource: "Intune",
policyStoreSourceType: "MDM",
}),
};
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: "" };
}
throw new Error(`unexpected command: ${command}`);
expect(command).toContain("requestedPolicyStoreSourceTypes");
expect(command).toContain("-ieq");
expect(command).toContain("GroupPolicy");
expect(command).toContain("MDM");
return {
code: 0,
stdout: quickPayloadJson({
state: stateJson({ activeAllowLocalRules: "False" }),
activeRules: [
ruleRow({
displayName: "MDM-managed Gateway allow",
policyStoreSource: "Intune",
policyStoreSourceType: "MDM",
}),
],
localRules: [ruleRow({ displayName: "Ignored local allow" })],
}),
};
});
await expect(
@@ -508,9 +497,7 @@ describe("Windows Gateway firewall diagnostics", () => {
severity: "info",
code: "windows_firewall_rule_present",
});
expect(
runner.mock.calls.some(([argv]) => argv.join(" ").includes("PolicyStore PersistentStore")),
).toBe(false);
expect(runner).toHaveBeenCalledTimes(1);
});
it("keeps broad any-port rules from structured Windows rule output", async () => {
@@ -635,64 +622,41 @@ describe("Windows Gateway firewall diagnostics", () => {
expect(runner).toHaveBeenCalledTimes(1);
});
it("runs bounded read-only full Windows probes for LAN binding", async () => {
const runner = vi.fn<FirewallCommandRunner>(async (argv) => {
const command = argv.join(" ");
if (command.includes("Get-NetConnectionProfile")) {
return { code: 0, stdout: stateJson({ localAllowRules: "True" }) };
}
if (command.includes("HNetCfg.FwPolicy2")) {
expect(command).toContain("$targetPort = 18789");
expect(command).not.toContain("Grouping");
expect(command).not.toContain("Description");
expect(command).toContain("System.Collections.ArrayList");
expect(command).toContain("$matchingRules.Add");
expect(command).toContain("[string]$rule.LocalAddresses");
expect(command).toContain("[string]$rule.RemoteAddresses");
return { code: 0, stdout: ruleJson() };
}
if (command.includes("advfirewall")) {
return { code: 0, stdout: "" };
}
throw new Error(`unexpected command: ${command}`);
it("uses the quick probe by default and honors an explicit timeout", async () => {
const createRunner = () =>
vi.fn<FirewallCommandRunner>(async () => ({ code: 0, stdout: quickPayloadJson() }));
const defaultRunner = createRunner();
const explicitQuickRunner = createRunner();
const overrideRunner = createRunner();
const defaultResult = await inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: defaultRunner,
});
const explicitQuickResult = await inspectWindowsGatewayFirewall({
bind: "lan",
mode: "quick",
port: 18789,
platform: "win32",
runCommandWithTimeout: explicitQuickRunner,
});
await inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: overrideRunner,
timeoutMs: 1234,
});
await expect(
inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: runner,
timeoutMs: 1234,
}),
).resolves.toMatchObject({
code: "windows_firewall_rule_present",
});
expect(runner).toHaveBeenCalledTimes(3);
expect(runner.mock.calls.map(([argv]) => argv[0])).toEqual(
expect.arrayContaining([
getWindowsPowerShellExePath(),
getWindowsSystem32ExePath("netsh.exe"),
]),
);
for (const [, opts] of runner.mock.calls) {
expect(opts).toMatchObject({ timeoutMs: 1234 });
}
runner.mockClear();
await expect(
inspectWindowsGatewayFirewall({
bind: "lan",
port: 18789,
platform: "win32",
runCommandWithTimeout: runner,
}),
).resolves.toMatchObject({
code: "windows_firewall_rule_present",
});
expect(runner).toHaveBeenCalledTimes(3);
for (const [, opts] of runner.mock.calls) {
expect(opts).toMatchObject({ timeoutMs: 5_000 });
}
expect(defaultResult).toEqual(explicitQuickResult);
expect(defaultRunner).toHaveBeenCalledTimes(1);
expect(explicitQuickRunner).toHaveBeenCalledTimes(1);
expect(overrideRunner).toHaveBeenCalledTimes(1);
expect(defaultRunner.mock.calls[0]?.[0]).toEqual(explicitQuickRunner.mock.calls[0]?.[0]);
expect(defaultRunner.mock.calls[0]?.[1]).toMatchObject({ timeoutMs: 5_000 });
expect(explicitQuickRunner.mock.calls[0]?.[1]).toMatchObject({ timeoutMs: 5_000 });
expect(overrideRunner.mock.calls[0]?.[1]).toMatchObject({ timeoutMs: 1234 });
});
});
+42 -330
View File
@@ -1,9 +1,8 @@
// Read-only diagnostics for Windows LAN Gateway reachability.
import { runCommandWithTimeout as defaultRunCommandWithTimeout } from "../process/exec.js";
import { getWindowsPowerShellExePath, getWindowsSystem32ExePath } from "./windows-install-roots.js";
import { getWindowsPowerShellExePath } from "./windows-install-roots.js";
const DEFAULT_WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS = 5_000;
const QUICK_WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS = 5_000;
const WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS = 5_000;
const DEFAULT_OUTPUT_BYTES = 2 * 1024 * 1024;
const WINDOWS_MANAGED_FIREWALL_POLICY_SOURCE_TYPES = [
"GroupPolicy",
@@ -16,152 +15,6 @@ const WINDOWS_MANAGED_FIREWALL_POLICY_SOURCE_TYPES = [
"HostFirewallMDM",
];
const WINDOWS_FIREWALL_STATE_COMMAND = [
"$ErrorActionPreference = 'Stop'",
"$connections = Get-NetConnectionProfile | Select-Object InterfaceAlias, @{Name='NetworkCategory';Expression={$_.NetworkCategory.ToString()}}",
"$activeProfiles = Get-NetFirewallProfile -PolicyStore ActiveStore | Select-Object Name, @{Name='Enabled';Expression={$_.Enabled.ToString()}}, @{Name='DefaultInboundAction';Expression={$_.DefaultInboundAction.ToString()}}, @{Name='AllowInboundRules';Expression={$_.AllowInboundRules.ToString()}}, @{Name='AllowLocalFirewallRules';Expression={$_.AllowLocalFirewallRules.ToString()}}",
"$localProfiles = Get-NetFirewallProfile -PolicyStore localhost | Select-Object Name, @{Name='Enabled';Expression={$_.Enabled.ToString()}}, @{Name='DefaultInboundAction';Expression={$_.DefaultInboundAction.ToString()}}, @{Name='AllowInboundRules';Expression={$_.AllowInboundRules.ToString()}}, @{Name='AllowLocalFirewallRules';Expression={$_.AllowLocalFirewallRules.ToString()}}",
"[pscustomobject]@{ConnectionProfiles = $connections; ActiveFirewallProfiles = $activeProfiles; LocalFirewallProfiles = $localProfiles} | ConvertTo-Json -Depth 4 -Compress",
].join("\n");
function buildWindowsNetSecurityFirewallRulesCommand(
port: number,
policyStore: "ActiveStore" | "PersistentStore",
policyStoreSourceTypes?: readonly string[],
): string {
const sourceTypeNames = policyStoreSourceTypes?.map((name) => `'${name}'`).join(", ");
const sourceTypeSetup = sourceTypeNames
? `
$policyStoreSourceType = (Get-Command Get-NetFirewallRule).Parameters['PolicyStoreSourceType'].ParameterType.GetElementType()
$requestedPolicyStoreSourceTypes = @(${sourceTypeNames})
$supportedPolicyStoreSourceTypes = [enum]::GetNames($policyStoreSourceType)
$policyStoreSourceTypes = @(
foreach ($requestedPolicyStoreSourceType in $requestedPolicyStoreSourceTypes) {
$supportedPolicyStoreSourceTypes | Where-Object { $_ -ieq $requestedPolicyStoreSourceType } | Select-Object -First 1
}
)
`
: "";
const ruleQuery = sourceTypeNames
? `
$rules = if ($policyStoreSourceTypes.Count -gt 0) {
@(Get-NetFirewallRule -Direction Inbound -Enabled True -Action Allow -PolicyStore ${policyStore} -PolicyStoreSourceType $policyStoreSourceTypes -ErrorAction SilentlyContinue)
} else {
@()
}
`
: `
$rules = @(Get-NetFirewallRule -Direction Inbound -Enabled True -Action Allow -PolicyStore ${policyStore})
`;
return `
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$targetPort = ${port}
${sourceTypeSetup}
function Test-OpenClawPortMatch($value) {
foreach ($entry in @($value)) {
$text = ([string]$entry).Trim()
if ($text -eq 'Any') { return $true }
foreach ($part in $text -split ',') {
$range = $part.Trim()
if ($range -eq ([string]$targetPort)) { return $true }
if ($range -match '^(\\d+)-(\\d+)$') {
$start = [int]$Matches[1]
$end = [int]$Matches[2]
if ($start -le $targetPort -and $targetPort -le $end) { return $true }
}
}
}
return $false
}
${ruleQuery}
$matchingRules = New-Object System.Collections.ArrayList
foreach ($rule in $rules) {
foreach ($portFilter in @($rule | Get-NetFirewallPortFilter)) {
$protocol = $portFilter.Protocol.ToString()
if (($protocol -eq 'Any' -or $protocol -eq 'TCP') -and (Test-OpenClawPortMatch $portFilter.LocalPort)) {
$appFilter = $rule | Get-NetFirewallApplicationFilter
$addressFilter = $rule | Get-NetFirewallAddressFilter
[void]$matchingRules.Add([pscustomobject]@{
DisplayName = [string]$rule.DisplayName
Name = [string]$rule.Name
Profile = [string]$rule.Profile
PolicyStoreSource = [string]$rule.PolicyStoreSource
PolicyStoreSourceType = $rule.PolicyStoreSourceType.ToString()
Program = [string]$appFilter.Program
LocalAddress = [string]$addressFilter.LocalAddress
RemoteAddress = [string]$addressFilter.RemoteAddress
})
}
}
}
$matchingRules | ConvertTo-Json -Depth 4 -Compress
`.trim();
}
function buildWindowsPersistentFirewallRulesCommand(port: number): string {
return buildWindowsNetSecurityFirewallRulesCommand(port, "PersistentStore");
}
function buildWindowsManagedActiveFirewallRulesCommand(port: number): string {
return buildWindowsNetSecurityFirewallRulesCommand(
port,
"ActiveStore",
WINDOWS_MANAGED_FIREWALL_POLICY_SOURCE_TYPES,
);
}
function buildWindowsFirewallRulesCommand(port: number): string {
return `
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$targetPort = ${port}
function Test-OpenClawPortMatch($value) {
$text = ([string]$value).Trim()
if ($text -eq '' -or $text -eq '*') { return $true }
foreach ($part in $text -split ',') {
$range = $part.Trim()
if ($range -eq ([string]$targetPort)) { return $true }
if ($range -match '^(\\d+)-(\\d+)$') {
$start = [int]$Matches[1]
$end = [int]$Matches[2]
if ($start -le $targetPort -and $targetPort -le $end) { return $true }
}
}
return $false
}
function Resolve-OpenClawProgramScope($rule) {
$program = ([string]$rule.ApplicationName).Trim()
if ($program) { return $program }
foreach ($field in @('serviceName', 'LocalAppPackageId', 'LocalUserOwner')) {
$value = ([string]$rule.$field).Trim()
if ($value) { return $value }
}
$ports = ([string]$rule.LocalPorts).Trim()
if ($ports -ne '' -and $ports -ne '*') { return 'Any' }
return 'Any'
}
$policy = New-Object -ComObject HNetCfg.FwPolicy2
$matchingRules = New-Object System.Collections.ArrayList
foreach ($rule in $policy.Rules) {
if (-not $rule.Enabled -or $rule.Direction -ne 1 -or $rule.Action -ne 1) { continue }
$protocol = if ($rule.Protocol -eq 6) { 'TCP' } elseif ($rule.Protocol -eq 256) { 'Any' } else { [string]$rule.Protocol }
if (($protocol -ne 'TCP' -and $protocol -ne 'Any') -or -not (Test-OpenClawPortMatch $rule.LocalPorts)) { continue }
[void]$matchingRules.Add([pscustomobject]@{
DisplayName = [string]$rule.Name
Name = [string]$rule.Name
Profile = [string]$rule.Profiles
PolicyStoreSource = 'PersistentStore'
PolicyStoreSourceType = 'Local'
Program = (Resolve-OpenClawProgramScope $rule)
LocalAddress = [string]$rule.LocalAddresses
RemoteAddress = [string]$rule.RemoteAddresses
})
}
$matchingRules | ConvertTo-Json -Depth 4 -Compress
`.trim();
}
function buildWindowsQuickFirewallCommand(port: number): string {
const sourceTypeNames = WINDOWS_MANAGED_FIREWALL_POLICY_SOURCE_TYPES.map(
(name) => `'${name}'`,
@@ -307,7 +160,7 @@ type WindowsGatewayFirewallCommandRunner = (
type InspectWindowsGatewayFirewallParams = {
bind: string | undefined;
port: number;
mode?: "quick" | "full";
mode?: "quick";
platform?: NodeJS.Platform;
runCommandWithTimeout?: WindowsGatewayFirewallCommandRunner;
timeoutMs?: number;
@@ -343,7 +196,6 @@ type ClassifiedFirewallState = {
localProfiles: FirewallProfile[];
matchingRules: FirewallRule[];
localMatchingRules: FirewallRule[];
netshOutput: string;
};
type QuickFirewallPayload = {
@@ -632,9 +484,6 @@ function classifyWindowsGatewayFirewallState(
const localRules = programAgnosticMatchingRules.filter(isLocalRule);
const onlyLocalRules = localRules.length === programAgnosticMatchingRules.length;
if (onlyLocalRules && !localRulesAreAllowed(state)) {
const policyDetail = /gpo-store only/i.test(state.netshOutput)
? "Windows reports LocalFirewallRules as N/A (GPO-store only)."
: "Local firewall rules are not explicitly enabled for the active profile.";
return {
applies: true,
severity: "warning",
@@ -643,7 +492,7 @@ function classifyWindowsGatewayFirewallState(
details: [
`Active network profile: ${activeProfileText}.`,
`Matching local allow rule(s): ${formatRuleNames(programAgnosticMatchingRules)}.`,
policyDetail,
"Local firewall rules are not explicitly enabled for the active profile.",
"Use a Group Policy/administrator-managed inbound TCP allow rule for the Gateway port, or switch to a network path such as loopback, Tailscale, or an SSH tunnel.",
],
};
@@ -693,9 +542,6 @@ function classifyWindowsGatewayFirewallState(
}
if (programAgnosticLocalRules.length > 0 && !localRulesAreAllowed(state)) {
const policyDetail = /gpo-store only/i.test(state.netshOutput)
? "Windows reports LocalFirewallRules as N/A (GPO-store only)."
: "Local firewall rules are disabled for the active profile.";
return {
applies: true,
severity: "warning",
@@ -704,7 +550,7 @@ function classifyWindowsGatewayFirewallState(
details: [
`Active network profile: ${activeProfileText}.`,
`Matching local allow rule(s): ${formatRuleNames(programAgnosticLocalRules)}.`,
policyDetail,
"Local firewall rules are disabled for the active profile.",
"Use a Group Policy/administrator-managed inbound TCP allow rule for the Gateway port, or switch to a network path such as loopback, Tailscale, or an SSH tunnel.",
],
};
@@ -725,9 +571,6 @@ function classifyWindowsGatewayFirewallState(
}
if (!localRulesAreAllowed(state) && state.localMatchingRules.length === 0) {
const policyDetail = /gpo-store only/i.test(state.netshOutput)
? "Windows reports LocalFirewallRules as N/A (GPO-store only)."
: "Local firewall rules are disabled for the active profile.";
return {
applies: true,
severity: "warning",
@@ -736,7 +579,7 @@ function classifyWindowsGatewayFirewallState(
details: [
`Active network profile: ${activeProfileText}.`,
"No active inbound TCP allow rule for the Gateway port was found.",
policyDetail,
"Local firewall rules are disabled for the active profile.",
"Use a Group Policy/administrator-managed inbound TCP allow rule for the Gateway port, or switch to a network path such as loopback, Tailscale, or an SSH tunnel.",
],
};
@@ -767,7 +610,6 @@ function classifyWindowsGatewayFirewallState(
function buildClassifiedState(
stateJson: string,
netshOutput: string,
activeRules: FirewallRule[],
localRules: FirewallRule[],
): ClassifiedFirewallState | null {
@@ -777,21 +619,12 @@ function buildClassifiedState(
ActiveRules: activeRules,
LocalRules: localRules,
}),
netshOutput,
});
}
function shouldProbeManagedActiveRules(diagnostic: WindowsGatewayFirewallDiagnostic): boolean {
return (
diagnostic.severity === "warning" &&
diagnostic.code !== "windows_firewall_inbound_rules_disabled"
);
}
function parseWindowsGatewayFirewallState(params: {
stateJson: string;
rulesJson: string;
netshOutput?: string | null;
}): ClassifiedFirewallState | null {
const state = parseJsonPayload(params.stateJson) as FirewallStatePayload | null;
const rules = parseJsonPayload(params.rulesJson);
@@ -808,7 +641,6 @@ function parseWindowsGatewayFirewallState(params: {
localProfiles: parseFirewallProfiles(state.LocalFirewallProfiles),
matchingRules: parseFirewallRules(rulePayload ? rulePayload.ActiveRules : rules),
localMatchingRules: parseFirewallRules(rulePayload?.LocalRules),
netshOutput: params.netshOutput ?? "",
};
}
@@ -827,191 +659,72 @@ export async function inspectWindowsGatewayFirewall(
}
const runCommandWithTimeout = params.runCommandWithTimeout ?? defaultRunCommandWithTimeout;
const mode = params.mode ?? "full";
const timeoutMs =
params.timeoutMs ??
(mode === "quick"
? QUICK_WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS
: DEFAULT_WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS);
if (mode === "quick") {
const quickJson = await runBestEffortCommand(
runCommandWithTimeout,
powershell(buildWindowsQuickFirewallCommand(params.port)),
timeoutMs,
);
if (quickJson === null) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const quickPayload = parseJsonPayload(quickJson) as QuickFirewallPayload | null;
if (!quickPayload || typeof quickPayload !== "object" || Array.isArray(quickPayload)) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const managedActiveRules = parseFirewallRules(quickPayload.ActiveRules);
const localRules = parseFirewallRules(quickPayload.LocalRules);
const stateJson = JSON.stringify(quickPayload.State ?? null);
const policyState = parseWindowsGatewayFirewallState({
stateJson,
rulesJson: JSON.stringify({
ActiveRules: [],
LocalRules: [],
}),
});
if (!policyState) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const activeRules = [
...managedActiveRules,
...(localRulesAreAllowed(policyState) ? localRules : []),
];
const state = buildClassifiedState(stateJson, "", activeRules, localRules);
return state
? classifyWindowsGatewayFirewallState(state)
: {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const [stateJson, rulesJson, netshOutput] = await Promise.all([
runBestEffortCommand(
runCommandWithTimeout,
powershell(WINDOWS_FIREWALL_STATE_COMMAND),
timeoutMs,
),
runBestEffortCommand(
runCommandWithTimeout,
powershell(buildWindowsFirewallRulesCommand(params.port)),
timeoutMs,
),
runBestEffortCommand(
runCommandWithTimeout,
[getWindowsSystem32ExePath("netsh.exe"), "advfirewall", "show", "allprofiles"],
timeoutMs,
),
]);
if (stateJson === null || rulesJson === null) {
const timeoutMs = params.timeoutMs ?? WINDOWS_GATEWAY_FIREWALL_TIMEOUT_MS;
const quickJson = await runBestEffortCommand(
runCommandWithTimeout,
powershell(buildWindowsQuickFirewallCommand(params.port)),
timeoutMs,
);
if (quickJson === null) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not inspect Windows Firewall policy for LAN Gateway reachability.",
message: "OpenClaw could not quickly inspect Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` from a normal PowerShell session and verify the advertised LAN URL from another device.",
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const firewallPolicyText = netshOutput ?? "";
const localRules = parseFirewallRules(parseJsonPayload(rulesJson));
const quickPayload = parseJsonPayload(quickJson) as QuickFirewallPayload | null;
if (!quickPayload || typeof quickPayload !== "object" || Array.isArray(quickPayload)) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
const managedActiveRules = parseFirewallRules(quickPayload.ActiveRules);
const localRules = parseFirewallRules(quickPayload.LocalRules);
const stateJson = JSON.stringify(quickPayload.State ?? null);
const policyState = parseWindowsGatewayFirewallState({
stateJson,
rulesJson: JSON.stringify({
ActiveRules: [],
LocalRules: [],
}),
netshOutput: firewallPolicyText,
});
if (!policyState) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall policy for LAN Gateway reachability.",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` from a normal PowerShell session and verify the advertised LAN URL from another device.",
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
let activeRules = localRulesAreAllowed(policyState) ? localRules : [];
let state = buildClassifiedState(stateJson, firewallPolicyText, activeRules, localRules);
if (!state) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall policy for LAN Gateway reachability.",
details: [
"Run `openclaw gateway status --deep` from a normal PowerShell session and verify the advertised LAN URL from another device.",
],
};
}
const initialDiagnostic = classifyWindowsGatewayFirewallState(state);
if (shouldProbeManagedActiveRules(initialDiagnostic)) {
const managedRulesJson = await runBestEffortCommand(
runCommandWithTimeout,
powershell(buildWindowsManagedActiveFirewallRulesCommand(params.port)),
timeoutMs,
);
if (managedRulesJson !== null) {
activeRules = [...activeRules, ...parseFirewallRules(parseJsonPayload(managedRulesJson))];
state = buildClassifiedState(stateJson, firewallPolicyText, activeRules, localRules);
if (!state) {
return {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message: "OpenClaw could not parse Windows Firewall policy for LAN Gateway reachability.",
details: [
"Run `openclaw gateway status --deep` from a normal PowerShell session and verify the advertised LAN URL from another device.",
],
};
}
} else if (!localRulesAreAllowed(state)) {
return {
const activeRules = [
...managedActiveRules,
...(localRulesAreAllowed(policyState) ? localRules : []),
];
const state = buildClassifiedState(stateJson, activeRules, localRules);
return state
? classifyWindowsGatewayFirewallState(state)
: {
applies: true,
severity: "warning",
code: "windows_firewall_inspection_failed",
message:
"OpenClaw could not inspect managed Windows Firewall rules for LAN Gateway reachability.",
message: "OpenClaw could not parse Windows Firewall LAN Gateway policy.",
details: [
"Run `openclaw gateway status --deep` from a normal PowerShell session and verify Group Policy or administrator-managed allow rules for the Gateway port.",
"Run `openclaw gateway status --deep` again, or verify the advertised LAN URL from another device.",
],
};
}
}
const diagnosticBeforeLocalDetail = classifyWindowsGatewayFirewallState(state);
if (!localRulesAreAllowed(state) && diagnosticBeforeLocalDetail.severity !== "info") {
const localRulesJson = await runBestEffortCommand(
runCommandWithTimeout,
powershell(buildWindowsPersistentFirewallRulesCommand(params.port)),
Math.max(timeoutMs, 10_000),
);
if (localRulesJson !== null) {
state.localMatchingRules = parseFirewallRules(parseJsonPayload(localRulesJson));
}
}
return classifyWindowsGatewayFirewallState(state);
}
export function formatWindowsGatewayFirewallGuidance(params: {
@@ -1026,4 +739,3 @@ export function formatWindowsGatewayFirewallGuidance(params: {
"Windows firewall: if another device cannot connect to the LAN URL, run `openclaw gateway status --deep` from this Windows host.",
];
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */