mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
fix(mobile): stop pinning the Windows pairing firewall rule to one port (STA-7672)
buildRepairScript wrote the Allow rule with -LocalPort <port> for whichever port happened to be bound when the user ran repair, but the desktop's listen port is not stable. A persisted mobile-ws-fallback-port.json (preferred over the pinned port for STA-1511), a pairing widen rebind, or an OS-assigned port all move it, and nothing reconciled the rule with the new bind. The phone then knocked on a port the firewall did not cover and LAN/direct pairing failed with no diagnosis pointing at the firewall. The Allow rule is now written with -LocalPort Any. Inspection already accepted 'Any' (buildInspectionScript matches -eq 'Any' -or -eq '<port>'), so the two halves can no longer desync. The rule stays scoped to this program, the Private profile, and -EdgeTraversalPolicy Block; the port still scopes which conflicting Block rules the repair removes. The port drift itself is cross-platform, but only Windows writes a per-port permission: macOS approves per app and Linux does not filter inbound LAN traffic by default.
This commit is contained in:
@@ -178,11 +178,43 @@ describe('windows mobile firewall', () => {
|
||||
expect(repairScript).toContain('$rule | Remove-NetFirewallRule')
|
||||
expect(repairScript).toContain('-Profile Private')
|
||||
expect(repairScript).toContain('-Protocol TCP')
|
||||
expect(repairScript).toContain('-LocalPort 6769')
|
||||
expect(repairScript).toContain("-Program 'C:\\Users\\O''Brien\\Orca\\Orca.exe'")
|
||||
expect(repairScript).toContain('-EdgeTraversalPolicy Block')
|
||||
})
|
||||
|
||||
it('writes a port-agnostic allow rule so a drifting bind port cannot outrun it (STA-7672)', async () => {
|
||||
const runPowerShell = vi.fn().mockResolvedValue('{"launched":true,"exitCode":0}')
|
||||
await repairWindowsMobileFirewall(6769, environment(runPowerShell))
|
||||
|
||||
const outerScript = runPowerShell.mock.calls[0]![0] as string
|
||||
const encoded = outerScript.match(/'-EncodedCommand', '([^']+)'/)?.[1]
|
||||
const repairScript = Buffer.from(encoded!, 'base64').toString('utf16le')
|
||||
const newRule = repairScript.split('New-NetFirewallRule')[1]!
|
||||
|
||||
// The desktop moves off 6768 on a persisted fallback, a pairing widen, or an
|
||||
// OS-assigned port, and nothing reconciles a pinned rule with the new bind.
|
||||
expect(newRule).toContain('-LocalPort Any')
|
||||
expect(newRule).not.toContain('-LocalPort 6769')
|
||||
// Still narrow where it matters: this program, private profile, no edge traversal.
|
||||
expect(newRule).toContain('-Profile Private')
|
||||
expect(newRule).toContain('-Protocol TCP')
|
||||
expect(newRule).toContain('-EdgeTraversalPolicy Block')
|
||||
// The port still scopes which conflicting Block rules the repair removes.
|
||||
expect(repairScript.split('New-NetFirewallRule')[0]).toContain("-eq '6769'")
|
||||
})
|
||||
|
||||
it('matches a port-agnostic rule whatever port the transport landed on', async () => {
|
||||
// The repaired rule now carries LocalPort 'Any', so the inspection filter has
|
||||
// to accept it for every bind the desktop can drift to (STA-7672).
|
||||
for (const port of [6768, 6769, 51_234]) {
|
||||
const runPowerShell = vi.fn().mockResolvedValue('{"privateFirewallEnabled":true}')
|
||||
await inspectWindowsMobileFirewall(port, undefined, environment(runPowerShell))
|
||||
|
||||
const script = runPowerShell.mock.calls[0]![0] as string
|
||||
expect(script).toContain(`[string]$_ -eq 'Any' -or [string]$_ -eq '${port}'`)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the elevated child encoded because Start-Process re-splits its ArgumentList', async () => {
|
||||
// Why: `Start-Process -ArgumentList` joins the array into one ShellExecuteEx parameter
|
||||
// string without quoting and PowerShell re-splits it on whitespace, which collapses runs
|
||||
|
||||
@@ -214,6 +214,12 @@ function buildRepairScript(port: number, executablePath: string): string {
|
||||
// rules, so the user's repair action must remove exact-app conflicts first.
|
||||
// Removal deliberately ignores the Block rule's remote-address scope,
|
||||
// mirroring the fail-closed inspection (the phone address is unknown).
|
||||
// `port` scopes only that removal. The Allow rule itself is written with
|
||||
// `-LocalPort Any` because the desktop's listen port is not stable: a
|
||||
// persisted fallback (mobile-ws-fallback-port.json), a pairing widen rebind,
|
||||
// or an OS-assigned port all move it, and nothing reconciles a port-pinned
|
||||
// rule with the new bind — which silently broke LAN pairing in STA-7672. The
|
||||
// rule stays scoped to this program, Private profile, and no edge traversal.
|
||||
return `$ErrorActionPreference = 'Stop'
|
||||
$blockingRules = @(Get-NetFirewallApplicationFilter -Program ${quotePowerShell(executablePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Block' })
|
||||
foreach ($rule in $blockingRules) {
|
||||
@@ -226,7 +232,7 @@ foreach ($rule in $blockingRules) {
|
||||
}
|
||||
}
|
||||
Get-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
||||
New-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -DisplayName ${quotePowerShell(FIREWALL_RULE_DISPLAY_NAME)} -Description 'Allows Orca Mobile to connect to this Orca desktop on private networks.' -Direction Inbound -Action Allow -Enabled True -Profile Private -Protocol TCP -LocalPort ${port} -Program ${quotePowerShell(executablePath)} -EdgeTraversalPolicy Block | Out-Null`
|
||||
New-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -DisplayName ${quotePowerShell(FIREWALL_RULE_DISPLAY_NAME)} -Description 'Allows Orca Mobile to connect to this Orca desktop on private networks.' -Direction Inbound -Action Allow -Enabled True -Profile Private -Protocol TCP -LocalPort Any -Program ${quotePowerShell(executablePath)} -EdgeTraversalPolicy Block | Out-Null`
|
||||
}
|
||||
|
||||
// Why the elevated child keeps `-EncodedCommand` while the local runner does not: `Start-Process
|
||||
|
||||
Reference in New Issue
Block a user