[CmdletBinding(SupportsShouldProcess = $true)] 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, [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.' } if ($Action -ne 'Remove' -and !(Test-Path -LiteralPath $exe -PathType Leaf)) { throw 'Install or publish the executable first.' } $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 (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 = $Profile $definition.InterfaceAlias = if ($InterfaceAlias) { $InterfaceAlias } else { 'Any' } $definition.Direction = 'Inbound' $definition.Action = 'Allow' $definition.RemoteAddress = 'LocalSubnet' $definition.EdgeTraversalPolicy = 'Block' $definition.Enabled = 'True' } if ($Action -eq 'Preview') { $definitions | ForEach-Object { [pscustomobject]$_ }; return } $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (!$WhatIfPreference -and !$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Open PowerShell as Administrator to apply or remove these rules. Preview requires no elevation.' } foreach ($definition in $definitions) { if (!$PSCmdlet.ShouldProcess($definition.Name, $Action)) { continue } $existing = Get-NetFirewallRule -Name $definition.Name -ErrorAction SilentlyContinue if ($existing) { $application = $existing | Get-NetFirewallApplicationFilter if ($application.Program -ne $exe -or $existing.Group -ne 'MusicBridge managed network access') { throw 'A conflicting rule exists; refusing to modify it.' } } if ($Action -eq 'Remove') { if ($existing) { $existing | Remove-NetFirewallRule } } elseif ($existing) { $update = $definition.Clone() $update.Remove('DisplayName') Set-NetFirewallRule @update | Out-Null } else { New-NetFirewallRule @definition -Group 'MusicBridge managed network access' | Out-Null } } if (!$WhatIfPreference) { Get-NetFirewallRule -Name ($definitions.Name) -ErrorAction SilentlyContinue | Select-Object Name,Enabled,Profile,Direction,Action }