Add firewall script for managing inbound rules; support for domain profiles and interface aliases

This commit is contained in:
2026-09-10 08:56:22 +03:00
parent 369c0347a7
commit e9931cb941
5 changed files with 108 additions and 6 deletions
+37
View File
@@ -36,6 +36,43 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\Programs\
Правила брандмауэра установщик не меняет. Для подключения из домашней сети Правила брандмауэра установщик не меняет. Для подключения из домашней сети
потребуется разрешённый доступ к TCP-порту агента и UDP 5353 в частной сети. потребуется разрешённый доступ к TCP-порту агента и UDP 5353 в частной сети.
## Брандмауэр
Установленный `Firewall.ps1` создаёт два входящих правила для конкретного exe:
TCP 8765 (HTTPS/WSS) и UDP 5353 (mDNS). Только профиль Private и адреса
LocalSubnet; Edge Traversal запрещён. Профили сети и чужие правила не меняются.
По умолчанию в доменной или публичной сети эти правила не разрешат подключение.
Если домашний Ethernet имеет доменный профиль, можно явно передать
`-Profile Domain -InterfaceAlias Ethernet`. Для Domain обязательно точное имя
интерфейса; маски и `Any` запрещены. Адреса остаются ограничены LocalSubnet.
Профиль Public не поддерживается. Повторное применение заменяет профиль и
интерфейс собственных правил для этого exe, а не добавляет ещё одну пару правил.
Просмотр без изменений:
```powershell
& "$env:LOCALAPPDATA\Programs\MusicBridge\Firewall.ps1" -Action Preview
```
Применение из PowerShell администратора:
```powershell
& "$env:LOCALAPPDATA\Programs\MusicBridge\Firewall.ps1" -Action Apply
```
Для нестандартной установки указать абсолютный `-ProgramPath` к exe;
для другого порта — `-Port`. `-Action Apply -WhatIf` показывает план без изменений.
Повторное применение обновляет собственные правила этого пути, включая порт.
Скрипт не сужает доступ, который уже разрешён другими правилами Windows.
Перед удалением приложения можно удалить его правила из PowerShell администратора:
```powershell
& "$env:LOCALAPPDATA\Programs\MusicBridge\Firewall.ps1" -Action Remove
```
Удаление правил выполняется отдельно: установщик и деинсталлятор не запрашивают
административные права. Для нестандартной установки передать тот же `-ProgramPath`.
## Для разработчика ## Для разработчика
Собрать пакет: `powershell -NoProfile -File Packaging/Publish.ps1`. Собрать пакет: `powershell -NoProfile -File Packaging/Publish.ps1`.
+41 -2
View File
@@ -1,6 +1,6 @@
# MusicBridge — план проекта # MusicBridge — план проекта
Обновлено: 2026-09-09. Репозиторий находится в `MusicBridge.Agent/`. Обновлено: 2026-09-10. Репозиторий находится в `MusicBridge.Agent/`.
## Цель ## Цель
@@ -86,6 +86,9 @@ Windows Core Audio → системная громкость устройств
- [x] Автозапуск текущего пользователя через меню трея; скрытый старт `--minimized`. - [x] Автозапуск текущего пользователя через меню трея; скрытый старт `--minimized`.
- [x] Самостоятельный Windows x64 пакет со встроенными Runtime, ZIP-установка, - [x] Самостоятельный Windows x64 пакет со встроенными Runtime, ZIP-установка,
ярлык в меню Пуск и скрипт удаления. Это не MSI и не подписанный установщик. ярлык в меню Пуск и скрипт удаления. Это не MSI и не подписанный установщик.
- [x] Скрипт брандмауэра: просмотр, применение и удаление собственных правил;
только конкретный exe, Private + LocalSubnet, TCP порта агента и UDP 5353.
- [x] Восстановление опроса после ошибок чтения; освобождение WSS после обрывов.
### 4. Нативное приложение iPhone — отложено пользователем ### 4. Нативное приложение iPhone — отложено пользователем
@@ -112,8 +115,40 @@ Windows Core Audio → системная громкость устройств
## Проверки и следующий шаг ## Проверки и следующий шаг
2026-09-10:
- Пользователь подтвердил: Ethernet подключён к домашней сети. В `Firewall.ps1`
добавлены `-Profile Domain -InterfaceAlias Ethernet`; по умолчанию Private.
Domain требует точный интерфейс, Public и маски запрещены. LocalSubnet,
привязка к exe и порты сохранены. Preview/WhatIf и проверки ограничений прошли.
- После повторного запроса пользователя правила применены через Windows UAC.
В ActiveStore проверены оба правила: включены, Inbound Allow, Domain,
интерфейс Ethernet, LocalSubnet, EdgeTraversal Block, TCP 8765 и UDP 5353.
Привязаны к `bin/Release/net10.0-windows10.0.19041.0/MusicBridge.Agent.exe`;
имена `MusicBridge-0F2EC7194BF18EBC-HTTPS` и `MusicBridge-0F2EC7194BF18EBC-mDNS`.
Агент во время проверки не запущен. При установке в другой каталог нужны
правила для нового пути. Подключение со второго устройства ещё не проверено.
2026-09-09: 2026-09-09:
- Последний этап — устойчивость: **112 проверок прошли**, сборка без ошибок и
предупреждений. Конфигурация `Resilience`, параметры `--desktop-smoke
--qr-window-smoke --mdns-smoke --live-read`. Работающий Release-агент не остановлен.
Проверены ошибки первого и последующих чтений, восстановление состояния,
очистка устаревших данных при ошибке и 12 последовательных обрывов WSS.
Чтение получает отмену через 5 секунд; отправка WSS ограничена 5 секундами,
keepalive — 20 секунд с ожиданием ответа 10 секунд. После длинного перерыва
mDNS пересоздаёт объявления. Реальный сон/пробуждение ещё не проверен.
- `Packaging/Test-Firewall.ps1` проверил Preview и Apply -WhatIf без изменения
системы. Правила ещё не применены: Ethernet имеет профиль DomainAuthenticated,
Tailscale — Private, tun2 — Public. Правила Private не откроют доступ через
Ethernet. Категории сети не менять автоматически; доступ для Domain требует
отдельного решения пользователя.
- ZIP пересобран с изменениями устойчивости и `Firewall.ps1`. Повторная проверка
установки, запуска со встроенным Runtime, обновления, защиты занятого exe и
удаления прошла. Проверка правил в режиме WhatIf также прошла; реальных
изменений брандмауэра и установки в рабочую папку пользователя не выполнялось.
- Последний этап — автозапуск и упаковка: **107 проверок прошло** с - Последний этап — автозапуск и упаковка: **107 проверок прошло** с
`-c Packaging --no-build -- --desktop-smoke --qr-window-smoke --mdns-smoke --live-read`. `-c Packaging --no-build -- --desktop-smoke --qr-window-smoke --mdns-smoke --live-read`.
Проверены запись/удаление тестовой записи автозапуска и скрытый старт окна. Проверены запись/удаление тестовой записи автозапуска и скрытый старт окна.
@@ -194,7 +229,8 @@ Windows Core Audio → системная громкость устройств
объявляется на домашних IPv4-адаптерах с multicast. Автоизменения брандмауэра нет. объявляется на домашних IPv4-адаптерах с multicast. Автоизменения брандмауэра нет.
Следующий шаг: проверка с другого устройства и устойчивости при смене сети, Следующий шаг: проверка с другого устройства и устойчивости при смене сети,
проверка входа Windows с автозапуском и подготовка правил частного брандмауэра. проверка входа Windows с автозапуском. Правила для домашнего Ethernet применены
и проверены для текущего пути Release-сборки (см. запись от 2026-09-10).
Трей, автозапуск и ZIP-упаковка реализованы. iPhone-клиент пока не начинать. Трей, автозапуск и ZIP-упаковка реализованы. iPhone-клиент пока не начинать.
Точные границы реализации и следующие шаги — в `CONNECTION-ROADMAP.md`. Точные границы реализации и следующие шаги — в `CONNECTION-ROADMAP.md`.
Начать разработку iOS только после новой команды пользователя. Начать разработку iOS только после новой команды пользователя.
@@ -221,6 +257,9 @@ Windows Core Audio → системная громкость устройств
- `Desktop/DesktopHost.cs`: STA-поток, значок/меню трея и жизненный цикл окна. - `Desktop/DesktopHost.cs`: STA-поток, значок/меню трея и жизненный цикл окна.
- `Desktop/StartupSettings.cs`: HKCU Run, экранированная команда автозапуска. - `Desktop/StartupSettings.cs`: HKCU Run, экранированная команда автозапуска.
- `Packaging/`: сборка standalone ZIP, установка/удаление и изолированный smoke-тест. - `Packaging/`: сборка standalone ZIP, установка/удаление и изолированный smoke-тест.
- `Packaging/Firewall.ps1`: Preview/Apply/Remove правил LocalSubnet; Private по
умолчанию, Domain только с явно указанным интерфейсом домашней сети.
- `Packaging/Test-Firewall.ps1`: проверка определений правил без изменения системы.
- `INSTALL.md`: установка, автозапуск, обновление и удаление. - `INSTALL.md`: установка, автозапуск, обновление и удаление.
- `Network/LocalAddresses.cs`: подходящие домашние IPv4-адреса. - `Network/LocalAddresses.cs`: подходящие домашние IPv4-адреса.
- `Network/DiscoveryDescriptor.cs`: публичные поля DNS-SD. - `Network/DiscoveryDescriptor.cs`: публичные поля DNS-SD.
+5
View File
@@ -235,6 +235,11 @@ Windows-окно с QR; `B` создаёт новый код. При `--lan` а
Поддерживается до 8 одновременных WebSocket. После потери соединения клиент Поддерживается до 8 одновременных WebSocket. После потери соединения клиент
переподключается с задержкой и получает полный снимок. Медленный клиент получает переподключается с задержкой и получает полный снимок. Медленный клиент получает
последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди. последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди.
Отправка снимка ограничена 5 секундами. Keepalive отправляется через 20 секунд,
ожидание ответа ограничено 10 секундами; оборванные соединения освобождают слот.
При ошибке чтения медиасессии сервер публикует пустое состояние с `mediaError`
и продолжает опрос. После восстановления очередной снимок содержит свежие данные.
На чтение передаётся отмена через 5 секунд.
Запросы с Origin Запросы с Origin
отклоняются: браузерные клиенты в проекте не поддерживаются. отклоняются: браузерные клиенты в проекте не поддерживаются.
+13 -4
View File
@@ -2,9 +2,17 @@
param( param(
[ValidateSet('Preview','Apply','Remove')][string]$Action = 'Preview', [ValidateSet('Preview','Apply','Remove')][string]$Action = 'Preview',
[string]$ProgramPath = (Join-Path $env:LOCALAPPDATA 'Programs\MusicBridge\MusicBridge.Agent.exe'), [string]$ProgramPath = (Join-Path $env:LOCALAPPDATA 'Programs\MusicBridge\MusicBridge.Agent.exe'),
[ValidateRange(1,65535)][int]$Port = 8765 [ValidateRange(1,65535)][int]$Port = 8765,
[ValidateSet('Private','Domain')][string]$Profile = 'Private',
[string]$InterfaceAlias
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if ($Profile -eq 'Domain' -and [string]::IsNullOrWhiteSpace($InterfaceAlias)) {
throw 'Domain access requires an explicit home network InterfaceAlias.'
}
if ($InterfaceAlias -and ([string]::IsNullOrWhiteSpace($InterfaceAlias) -or $InterfaceAlias -eq 'Any' -or [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($InterfaceAlias))) {
throw 'Select one exact network interface, without wildcards.'
}
if (![IO.Path]::IsPathRooted($ProgramPath)) { throw 'ProgramPath must be absolute.' } if (![IO.Path]::IsPathRooted($ProgramPath)) { throw 'ProgramPath must be absolute.' }
$exe = [IO.Path]::GetFullPath($ProgramPath) $exe = [IO.Path]::GetFullPath($ProgramPath)
if ([IO.Path]::GetFileName($exe) -ne 'MusicBridge.Agent.exe') { throw 'Select MusicBridge.Agent.exe.' } if ([IO.Path]::GetFileName($exe) -ne 'MusicBridge.Agent.exe') { throw 'Select MusicBridge.Agent.exe.' }
@@ -13,12 +21,13 @@ $sha = [Security.Cryptography.SHA256]::Create()
try { $id = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($exe.ToUpperInvariant())))).Replace('-','').Substring(0,16) } try { $id = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($exe.ToUpperInvariant())))).Replace('-','').Substring(0,16) }
finally { $sha.Dispose() } finally { $sha.Dispose() }
$definitions = @( $definitions = @(
@{Name="MusicBridge-$id-HTTPS"; DisplayName='MusicBridge HTTPS (private LAN)'; Protocol='TCP'; LocalPort=$Port}, @{Name="MusicBridge-$id-HTTPS"; DisplayName='MusicBridge HTTPS (local subnet)'; Protocol='TCP'; LocalPort=$Port},
@{Name="MusicBridge-$id-mDNS"; DisplayName='MusicBridge mDNS (private LAN)'; Protocol='UDP'; LocalPort=5353} @{Name="MusicBridge-$id-mDNS"; DisplayName='MusicBridge mDNS (local subnet)'; Protocol='UDP'; LocalPort=5353}
) )
foreach ($definition in $definitions) { foreach ($definition in $definitions) {
$definition.Program = $exe $definition.Program = $exe
$definition.Profile = 'Private' $definition.Profile = $Profile
$definition.InterfaceAlias = if ($InterfaceAlias) { $InterfaceAlias } else { 'Any' }
$definition.Direction = 'Inbound' $definition.Direction = 'Inbound'
$definition.Action = 'Allow' $definition.Action = 'Allow'
$definition.RemoteAddress = 'LocalSubnet' $definition.RemoteAddress = 'LocalSubnet'
+12
View File
@@ -13,4 +13,16 @@ if (($rules | Where-Object Protocol -eq UDP).LocalPort -ne 5353) { throw 'mDNS p
$again = @(& $script -Action Preview -ProgramPath $exe -Port 8765) $again = @(& $script -Action Preview -ProgramPath $exe -Port 8765)
if (($rules.Name -join ',') -ne ($again.Name -join ',')) { throw 'Changing port would leave old rule names behind.' } if (($rules.Name -join ',') -ne ($again.Name -join ',')) { throw 'Changing port would leave old rule names behind.' }
& $script -Action Apply -ProgramPath $exe -WhatIf & $script -Action Apply -ProgramPath $exe -WhatIf
$domain = @(& $script -Action Preview -ProgramPath $exe -Profile Domain -InterfaceAlias Ethernet)
foreach ($rule in $domain) {
if ($rule.Profile -ne 'Domain' -or $rule.InterfaceAlias -ne 'Ethernet' -or $rule.RemoteAddress -ne 'LocalSubnet') { throw 'Domain scope mismatch.' }
}
if (($domain.Name -join ',') -ne ($rules.Name -join ',')) { throw 'Changing profile would leave old rules behind.' }
foreach ($badAlias in @('', 'Any', 'Ether*', 'Ether?et')) {
$rejected = $false
try { & $script -Action Preview -ProgramPath $exe -Profile Domain -InterfaceAlias $badAlias | Out-Null }
catch { $rejected = $true }
if (!$rejected) { throw 'Domain scope accepted an unrestricted interface.' }
}
& $script -Action Apply -ProgramPath $exe -Profile Domain -InterfaceAlias Ethernet -WhatIf
Write-Host 'PASS: private profile, local subnet, executable binding, ports, stable names and dry-run. No firewall changes.' Write-Host 'PASS: private profile, local subnet, executable binding, ports, stable names and dry-run. No firewall changes.'