diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 7f52ba08a6b6..d6dc01e6ae39 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -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 diff --git a/src/infra/windows-gateway-firewall-diagnostics.test.ts b/src/infra/windows-gateway-firewall-diagnostics.test.ts index 0c5ba962d3aa..8a9ed753e9fa 100644 --- a/src/infra/windows-gateway-firewall-diagnostics.test.ts +++ b/src/infra/windows-gateway-firewall-diagnostics.test.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[0]; type FirewallCommandRunner = NonNullable; @@ -102,8 +102,8 @@ function ruleJson(params?: { function quickPayloadJson(params?: { state?: string; - activeRules?: Array>; - localRules?: Array>; + 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(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(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(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(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(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(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(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(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(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 }); }); }); diff --git a/src/infra/windows-gateway-firewall-diagnostics.ts b/src/infra/windows-gateway-firewall-diagnostics.ts index 2cb0a5a56c0a..3c95810fd055 100644 --- a/src/infra/windows-gateway-firewall-diagnostics.ts +++ b/src/infra/windows-gateway-firewall-diagnostics.ts @@ -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. */