diff --git a/INSTALL.md b/INSTALL.md index aa97b2b..fc7d903 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -36,6 +36,43 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\Programs\ Правила брандмауэра установщик не меняет. Для подключения из домашней сети потребуется разрешённый доступ к 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`. diff --git a/PLAN.md b/PLAN.md index f0eb667..4068f04 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,6 @@ # MusicBridge — план проекта -Обновлено: 2026-09-09. Репозиторий находится в `MusicBridge.Agent/`. +Обновлено: 2026-09-10. Репозиторий находится в `MusicBridge.Agent/`. ## Цель @@ -86,6 +86,9 @@ Windows Core Audio → системная громкость устройств - [x] Автозапуск текущего пользователя через меню трея; скрытый старт `--minimized`. - [x] Самостоятельный Windows x64 пакет со встроенными Runtime, ZIP-установка, ярлык в меню Пуск и скрипт удаления. Это не MSI и не подписанный установщик. +- [x] Скрипт брандмауэра: просмотр, применение и удаление собственных правил; + только конкретный exe, Private + LocalSubnet, TCP порта агента и UDP 5353. +- [x] Восстановление опроса после ошибок чтения; освобождение WSS после обрывов. ### 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: +- Последний этап — устойчивость: **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 проверок прошло** с `-c Packaging --no-build -- --desktop-smoke --qr-window-smoke --mdns-smoke --live-read`. Проверены запись/удаление тестовой записи автозапуска и скрытый старт окна. @@ -194,7 +229,8 @@ Windows Core Audio → системная громкость устройств объявляется на домашних IPv4-адаптерах с multicast. Автоизменения брандмауэра нет. Следующий шаг: проверка с другого устройства и устойчивости при смене сети, -проверка входа Windows с автозапуском и подготовка правил частного брандмауэра. +проверка входа Windows с автозапуском. Правила для домашнего Ethernet применены +и проверены для текущего пути Release-сборки (см. запись от 2026-09-10). Трей, автозапуск и ZIP-упаковка реализованы. iPhone-клиент пока не начинать. Точные границы реализации и следующие шаги — в `CONNECTION-ROADMAP.md`. Начать разработку iOS только после новой команды пользователя. @@ -221,6 +257,9 @@ Windows Core Audio → системная громкость устройств - `Desktop/DesktopHost.cs`: STA-поток, значок/меню трея и жизненный цикл окна. - `Desktop/StartupSettings.cs`: HKCU Run, экранированная команда автозапуска. - `Packaging/`: сборка standalone ZIP, установка/удаление и изолированный smoke-тест. +- `Packaging/Firewall.ps1`: Preview/Apply/Remove правил LocalSubnet; Private по + умолчанию, Domain только с явно указанным интерфейсом домашней сети. +- `Packaging/Test-Firewall.ps1`: проверка определений правил без изменения системы. - `INSTALL.md`: установка, автозапуск, обновление и удаление. - `Network/LocalAddresses.cs`: подходящие домашние IPv4-адреса. - `Network/DiscoveryDescriptor.cs`: публичные поля DNS-SD. diff --git a/PROTOCOL.md b/PROTOCOL.md index 2f2530c..2a9f3c4 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -235,6 +235,11 @@ Windows-окно с QR; `B` создаёт новый код. При `--lan` а Поддерживается до 8 одновременных WebSocket. После потери соединения клиент переподключается с задержкой и получает полный снимок. Медленный клиент получает последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди. +Отправка снимка ограничена 5 секундами. Keepalive отправляется через 20 секунд, +ожидание ответа ограничено 10 секундами; оборванные соединения освобождают слот. +При ошибке чтения медиасессии сервер публикует пустое состояние с `mediaError` +и продолжает опрос. После восстановления очередной снимок содержит свежие данные. +На чтение передаётся отмена через 5 секунд. Запросы с Origin отклоняются: браузерные клиенты в проекте не поддерживаются. diff --git a/Packaging/Firewall.ps1 b/Packaging/Firewall.ps1 index 31c888d..f1475bc 100644 --- a/Packaging/Firewall.ps1 +++ b/Packaging/Firewall.ps1 @@ -2,9 +2,17 @@ param( [ValidateSet('Preview','Apply','Remove')][string]$Action = 'Preview', [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' +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.' } $exe = [IO.Path]::GetFullPath($ProgramPath) 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) } finally { $sha.Dispose() } $definitions = @( - @{Name="MusicBridge-$id-HTTPS"; DisplayName='MusicBridge HTTPS (private LAN)'; Protocol='TCP'; LocalPort=$Port}, - @{Name="MusicBridge-$id-mDNS"; DisplayName='MusicBridge mDNS (private LAN)'; Protocol='UDP'; LocalPort=5353} + @{Name="MusicBridge-$id-HTTPS"; DisplayName='MusicBridge HTTPS (local subnet)'; Protocol='TCP'; LocalPort=$Port}, + @{Name="MusicBridge-$id-mDNS"; DisplayName='MusicBridge mDNS (local subnet)'; Protocol='UDP'; LocalPort=5353} ) foreach ($definition in $definitions) { $definition.Program = $exe - $definition.Profile = 'Private' + $definition.Profile = $Profile + $definition.InterfaceAlias = if ($InterfaceAlias) { $InterfaceAlias } else { 'Any' } $definition.Direction = 'Inbound' $definition.Action = 'Allow' $definition.RemoteAddress = 'LocalSubnet' diff --git a/Packaging/Test-Firewall.ps1 b/Packaging/Test-Firewall.ps1 index 8ef8800..079141f 100644 --- a/Packaging/Test-Firewall.ps1 +++ b/Packaging/Test-Firewall.ps1 @@ -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) if (($rules.Name -join ',') -ne ($again.Name -join ',')) { throw 'Changing port would leave old rule names behind.' } & $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.'