mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(infra): remove unused Windows firewall probes (#117662)
This commit is contained in:
committed by
GitHub
parent
08ea9cc31d
commit
9f3b44a141
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user