summaryrefslogtreecommitdiffstats
path: root/packaging/win/firewall.ps1
blob: 9b3687acdf83b6422b390589e5fce74ca46acd81 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<#
.SYNOPSIS
    Add (or remove) the inbound Windows Firewall rules MeshBay needs.

.DESCRIPTION
    Two independent things need an inbound allow, and Windows prompts
    "Allow access" for each the first time it happens if there is no rule:

    1. WebRTC (MeshBay.exe and meshbay-node.exe). It binds an ephemeral UDP
       port per connection, and the browser always dials the node -- aioice
       cannot resolve the peer's mDNS `.local` candidate to call back -- so the
       node (and, during ICE connectivity checks, the client) must accept
       unsolicited inbound UDP. There is no fixed port, so these rules are
       scoped by PROGRAM with no LocalPort restriction, which is the Windows-
       native way to say "this binary, any port it happens to bind" -- the
       Linux packaging (packaging/firewall/) has to approximate the same thing
       with a broad 1024-65535/udp port range because netfilter has no
       equivalent program scoping.
    2. LAN casting (MeshBay.exe only): the HTTP relay to a Chromecast/Smart TV
       (src/cast-relay.js, fixed TCP 19550-19553) and mDNS device discovery
       (src/cast-chromecast.js via bonjour-service, UDP 5353). Both scoped by
       program AND port -- narrower than the WebRTC rules, since these ports
       are fixed and known. Matches packaging/firewall/*/meshbay-cast.xml.

    The installer runs this once, elevated, so the user answers one UAC prompt
    instead of up to four dialogs spread across first use of chat, downloads
    and casting. Declining the installer's offer is fine -- the dialogs are
    the fallback, one per program/port combination as each is first used.

    Shipped as an extraResource at <install>\resources\firewall.ps1, so it
    locates the two executables from its own path and takes no arguments
    beyond the action. add/remove run elevated and windowless, leaving a
    trace at %TEMP%\meshbay-firewall.log; check is a plain read (no admin
    needed -- Get-NetFirewallRule does not require it, only New/Remove do)
    that the installer runs first, unelevated, so re-running setup with the
    rules already in place skips the prompt and the UAC entirely.

.PARAMETER Action
    add     (default) create/replace the rules
    remove  delete them
    check   exit 0 if all rules already exist, 1 otherwise -- no admin needed
#>
[CmdletBinding()]
param(
    [ValidateSet("add", "remove", "check")]
    [string]$Action = "add"
)

$log = Join-Path $env:TEMP "meshbay-firewall.log"
"[{0}] {1}" -f (Get-Date -Format s), $Action | Add-Content $log

# This script sits at <install>\resources\firewall.ps1.
$resources = $PSScriptRoot
$install   = Split-Path -Parent $resources
$GROUP     = "MeshBay"

$client = Join-Path $install   "MeshBay.exe"
$node   = Join-Path $resources "node-runtime\meshbay-node.exe"

# Program-scoped, any port: the ephemeral-UDP-port problem WebRTC always has.
# Port-scoped as well: the two fixed, known ports LAN casting actually uses.
$rules = @(
    @{ Name = "MeshBay";                Path = $client; Protocol = "UDP"; LocalPort = "Any" }
    @{ Name = "MeshBay Node";           Path = $node;   Protocol = "UDP"; LocalPort = "Any" }
    @{ Name = "MeshBay Cast";           Path = $client; Protocol = "TCP"; LocalPort = "19550-19553" }
    @{ Name = "MeshBay Cast Discovery"; Path = $client; Protocol = "UDP"; LocalPort = "5353" }
)

if ($Action -eq "check") {
    $missing = $rules | Where-Object { -not (Get-NetFirewallRule -DisplayName $_.Name -ErrorAction SilentlyContinue) }
    if ($missing) {
        "  check: missing $(($missing | ForEach-Object { $_.Name }) -join ', ')" | Add-Content $log
        exit 1
    }
    "  check: all rules present" | Add-Content $log
    exit 0
}

$ErrorActionPreference = "Stop"
try {
    foreach ($r in $rules) {
        # Idempotent: clear any existing rule of this name first.
        Remove-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue

        if ($Action -eq "add") {
            if (-not (Test-Path $r.Path)) {
                "  skip $($r.Name): $($r.Path) not found" | Add-Content $log
                continue
            }
            New-NetFirewallRule -DisplayName $r.Name -Group $GROUP `
                -Direction Inbound -Action Allow `
                -Program $r.Path -Protocol $r.Protocol -LocalPort $r.LocalPort `
                -Profile Any | Out-Null
            "  allowed $($r.Name)  ($($r.Protocol) $($r.LocalPort), $($r.Path))" | Add-Content $log
        }
        else {
            "  removed $($r.Name)" | Add-Content $log
        }
    }
}
catch {
    "  ERROR: $_" | Add-Content $log
    throw
}