Marli-Net.psm1
|
#Requires -Version 5.1 <# Marli-Net - Netzwerkkarten-Verwaltung Version 2.0.1 (Autor: Marco Lippert) Aufruf: Start-MarliNet (oder wie früher: Marli-Net) Funktionen: D = IPv4 per DHCP S = Statische IPv4 (Maske als 255.255.255.0 ODER 24 eingeben) R = Netzwerkkarte neu starten C = Netzwerkkarte komplett neu installieren + "Geister-Karten" entfernen #> # Modulordner merken (für den Neustart mit Admin-Rechten) $script:ModuleRoot = $PSScriptRoot #region ---------- Hilfsfunktionen (nicht exportiert) ---------- function Test-MarliAdmin { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal $identity return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # 24 -> 255.255.255.0 function ConvertTo-SubnetMask { param([Parameter(Mandatory)][ValidateRange(0, 32)][int]$PrefixLength) $octets = foreach ($i in 0..3) { $bits = [Math]::Min(8, [Math]::Max(0, $PrefixLength - (8 * $i))) [int](256 - [Math]::Pow(2, 8 - $bits)) } return ($octets -join '.') } # 255.255.255.0 -> 24 (gibt $null zurück, wenn die Maske ungültig ist) function ConvertTo-PrefixLength { param([Parameter(Mandatory)][string]$SubnetMask) if (-not (Test-IPv4Address $SubnetMask)) { return $null } $bytes = ([ipaddress]$SubnetMask).GetAddressBytes() $binary = ($bytes | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') }) -join '' # Gültige Maske = erst nur Einsen, dann nur Nullen (z.B. 255.0.255.0 ist ungültig) if ($binary -match '01') { return $null } return ($binary -replace '0').Length } function Test-IPv4Address { param([string]$Address) if ($Address -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { return $false } $ip = $null return [ipaddress]::TryParse($Address, [ref]$ip) } function Read-IPv4 { param( [Parameter(Mandatory)][string]$Prompt, [switch]$AllowEmpty ) while ($true) { $value = "$(Read-Host $Prompt)".Trim() if ($AllowEmpty -and -not $value) { return $null } if (Test-IPv4Address $value) { return $value } Write-Host ' Invalid IPv4 address, please try again.' -ForegroundColor Red } } function Read-YesNo { param([Parameter(Mandatory)][string]$Prompt) return ("$(Read-Host "$Prompt (Y/N)")".Trim() -match '^[yYjJ]$') } function Wait-Enter { [void](Read-Host ' Press Enter to continue') } function Write-MarliHeader { param([string]$Subtitle) Clear-Host Write-Host '' Write-Host ' Marli-Net - Networkcard changer' -ForegroundColor Cyan Write-Host (' ' + ('-' * 60)) if ($Subtitle) { Write-Host " $Subtitle" -ForegroundColor Yellow } Write-Host '' } function Write-MarliValue { param([string]$Label, [string]$Value, [string]$Color = 'Yellow') Write-Host (' {0,-14}' -f $Label) -NoNewline Write-Host $Value -ForegroundColor $Color } # Alle "echten" Netzwerkkarten (ohne Loopback/Bluetooth) function Get-MarliAdapter { $adapters = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceDescription -notmatch 'Bluetooth|Loopback' -and $_.Name -notmatch 'Bluetooth' } | Sort-Object Name return @($adapters) } # Aktuelle IPv4-Konfiguration einer Karte als Objekt function Get-MarliIpInfo { param([Parameter(Mandatory)][string]$Alias) $adapter = Get-NetAdapter -Name $Alias -ErrorAction Stop $ipIf = Get-NetIPInterface -InterfaceAlias $Alias -AddressFamily IPv4 -ErrorAction SilentlyContinue $address = @(Get-NetIPAddress -InterfaceAlias $Alias -AddressFamily IPv4 -ErrorAction SilentlyContinue) | Select-Object -First 1 $gateway = @(Get-NetRoute -InterfaceAlias $Alias -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue) | Select-Object -First 1 $dns = @(Get-DnsClientServerAddress -InterfaceAlias $Alias -AddressFamily IPv4 -ErrorAction SilentlyContinue | ForEach-Object { $_.ServerAddresses }) [pscustomobject]@{ Alias = $Alias Description = $adapter.InterfaceDescription Status = [string]$adapter.Status LinkSpeed = [string]$adapter.LinkSpeed Mac = [string]$adapter.MacAddress Mode = $(if ($ipIf -and "$($ipIf.Dhcp)" -eq 'Enabled') { 'DHCP' } else { 'STATIC' }) IPAddress = $(if ($address) { $address.IPAddress } else { '-' }) SubnetMask = $(if ($address) { ConvertTo-SubnetMask $address.PrefixLength } else { '-' }) Gateway = $(if ($gateway) { $gateway.NextHop } else { '-' }) Dns = $(if ($dns.Count -gt 0) { $dns -join ', ' } else { '-' }) } } function Show-MarliIpInfo { param([Parameter(Mandatory)]$Info) $statusColor = if ($Info.Status -eq 'Up') { 'Green' } else { 'Red' } Write-MarliValue 'Name:' $Info.Alias Write-MarliValue 'Status:' "$($Info.Status) ($($Info.LinkSpeed))" $statusColor Write-MarliValue 'MAC:' $Info.Mac Write-Host '' Write-MarliValue 'Mode:' $Info.Mode 'Green' Write-MarliValue 'IP-Address:' $Info.IPAddress Write-MarliValue 'Subnetmask:' $Info.SubnetMask Write-MarliValue 'Gateway:' $Info.Gateway Write-MarliValue 'DNS:' $Info.Dns Write-Host (' ' + ('-' * 60)) } # Wartet, bis die Karte wieder "Up" ist (max. X Sekunden) function Wait-MarliAdapter { param([Parameter(Mandatory)][string]$Alias, [int]$TimeoutSeconds = 20) Write-Host ' Please wait...' -ForegroundColor DarkGray $watch = [Diagnostics.Stopwatch]::StartNew() do { Start-Sleep -Seconds 1 $isUp = "$((Get-NetAdapter -Name $Alias -ErrorAction SilentlyContinue).Status)" -eq 'Up' } until ($isUp -or $watch.Elapsed.TotalSeconds -ge $TimeoutSeconds) Start-Sleep -Seconds 2 # DHCP braucht kurz für die Adresse } # Entfernt alte IPv4-Adressen und das alte Standard-Gateway function Clear-MarliIpv4 { param([Parameter(Mandatory)][string]$Alias) Get-NetIPAddress -InterfaceAlias $Alias -AddressFamily IPv4 -ErrorAction SilentlyContinue | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue Get-NetRoute -InterfaceAlias $Alias -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue } #endregion #region ---------- Aktionen ---------- function Set-MarliDhcp { param([Parameter(Mandatory)][string]$Alias) Clear-MarliIpv4 -Alias $Alias Set-NetIPInterface -InterfaceAlias $Alias -AddressFamily IPv4 -Dhcp Enabled -ErrorAction Stop Set-DnsClientServerAddress -InterfaceAlias $Alias -ResetServerAddresses -ErrorAction Stop Restart-NetAdapter -Name $Alias -Confirm:$false -ErrorAction Stop Wait-MarliAdapter -Alias $Alias } function Read-MarliStaticConfig { Write-Host '' $ip = Read-IPv4 -Prompt ' New IPv4 address' while ($true) { $mask = "$(Read-Host ' Subnet mask (e.g. 255.255.255.0 or 24)')".Trim().TrimStart('/') $prefix = $null if ($mask -match '^\d{1,2}$') { if ([int]$mask -ge 1 -and [int]$mask -le 32) { $prefix = [int]$mask } } elseif ($mask) { $prefix = ConvertTo-PrefixLength $mask } if ($prefix) { break } Write-Host ' Invalid subnet mask, please try again.' -ForegroundColor Red } $gateway = Read-IPv4 -Prompt ' Default gateway (Enter = none)' -AllowEmpty $dns1 = Read-IPv4 -Prompt ' Primary DNS (Enter = none)' -AllowEmpty $dns2 = $null if ($dns1) { $dns2 = Read-IPv4 -Prompt ' Secondary DNS (Enter = none)' -AllowEmpty } $dnsList = @(@($dns1, $dns2) | Where-Object { $_ }) # Zusammenfassung vor dem Übernehmen Write-Host '' Write-MarliValue 'IP-Address:' $ip Write-MarliValue 'Subnetmask:' "$(ConvertTo-SubnetMask $prefix) (/$prefix)" Write-MarliValue 'Gateway:' $(if ($gateway) { $gateway } else { '-' }) Write-MarliValue 'DNS:' $(if ($dnsList) { $dnsList -join ', ' } else { '-' }) Write-Host '' if (-not (Read-YesNo ' Apply these settings?')) { return $null } return @{ IPAddress = $ip PrefixLength = $prefix Gateway = $gateway Dns = @($dnsList) } } function Set-MarliStatic { param( [Parameter(Mandatory)][string]$Alias, [Parameter(Mandatory)][string]$IPAddress, [Parameter(Mandatory)][int]$PrefixLength, [string]$Gateway, [string[]]$Dns ) Set-NetIPInterface -InterfaceAlias $Alias -AddressFamily IPv4 -Dhcp Disabled -ErrorAction Stop Clear-MarliIpv4 -Alias $Alias $newIp = @{ InterfaceAlias = $Alias AddressFamily = 'IPv4' IPAddress = $IPAddress PrefixLength = $PrefixLength ErrorAction = 'Stop' } if ($Gateway) { $newIp.DefaultGateway = $Gateway } New-NetIPAddress @newIp | Out-Null if ($Dns -and $Dns.Count -gt 0) { Set-DnsClientServerAddress -InterfaceAlias $Alias -ServerAddresses $Dns -ErrorAction Stop } else { Set-DnsClientServerAddress -InterfaceAlias $Alias -ResetServerAddresses -ErrorAction Stop } } function Restart-MarliAdapter { param([Parameter(Mandatory)][string]$Alias) Restart-NetAdapter -Name $Alias -Confirm:$false -ErrorAction Stop Wait-MarliAdapter -Alias $Alias } # Karte deinstallieren + neu erkennen lassen, Geister-Karten entfernen. # Gibt $true zurück, wenn die Karte neu installiert wurde (Name kann sich ändern). function Reset-MarliAdapter { param([Parameter(Mandatory)][string]$Alias) if ([Environment]::OSVersion.Version.Build -lt 19041) { Write-Host ' This function needs Windows 10 2004 or newer (pnputil).' -ForegroundColor Red Wait-Enter return $false } Write-Host '' Write-Host ' WARNING: The network card will be uninstalled and reinstalled.' -ForegroundColor Red Write-Host ' All settings of this card (IP, DNS, ...) will be lost.' -ForegroundColor Red Write-Host ' Remote connections over this card will be interrupted!' -ForegroundColor Red if (-not (Read-YesNo ' Continue?')) { return $false } $instanceId = (Get-NetAdapter -Name $Alias -ErrorAction Stop).PnPDeviceID Write-Host ' Flushing DNS cache and resetting Winsock...' -ForegroundColor DarkGray ipconfig /flushdns | Out-Null netsh winsock reset | Out-Null Write-Host ' Removing network card...' -ForegroundColor DarkGray pnputil /remove-device "$instanceId" | Out-Null if ($LASTEXITCODE -ne 0) { Write-Host " pnputil exit code $LASTEXITCODE" -ForegroundColor Yellow } # "Geister-Karten" = Netzwerkkarten, die nicht mehr vorhanden sind $ghosts = @(Get-PnpDevice -Class Net -ErrorAction SilentlyContinue | Where-Object { -not $_.Present }) foreach ($ghost in $ghosts) { Write-Host " Removing old device: $($ghost.FriendlyName)" -ForegroundColor DarkGray pnputil /remove-device "$($ghost.InstanceId)" | Out-Null } Write-Host ' Searching for new hardware...' -ForegroundColor DarkGray pnputil /scan-devices | Out-Null Start-Sleep -Seconds 5 Write-Host '' Write-Host ' Done. A restart is recommended (Winsock reset).' -ForegroundColor Green Wait-Enter return $true } #endregion #region ---------- Menüs ---------- function Select-MarliAdapter { while ($true) { $adapters = Get-MarliAdapter Write-MarliHeader 'Please select the network adapter' if ($adapters.Count -eq 0) { Write-Host ' No network adapter found.' -ForegroundColor Red Wait-Enter return $null } for ($i = 0; $i -lt $adapters.Count; $i++) { $a = $adapters[$i] $up = "$($a.Status)" -eq 'Up' $state = if ($up) { 'Connected' } else { "$($a.Status)" } $color = if ($up) { 'Green' } else { 'Red' } Write-Host (' [{0,2}] ' -f ($i + 1)) -NoNewline -ForegroundColor Yellow Write-Host ('{0,-48}' -f $a.InterfaceDescription) -NoNewline Write-Host ('{0,-11}' -f $a.LinkSpeed) -NoNewline -ForegroundColor Yellow Write-Host $state -ForegroundColor $color } Write-Host '' $choice = "$(Read-Host ' Select Nr (Q = Quit)')".Trim() if ($choice -match '^[qQ]$') { return $null } $nr = 0 if ([int]::TryParse($choice, [ref]$nr) -and $nr -ge 1 -and $nr -le $adapters.Count) { return $adapters[$nr - 1] } } } function Invoke-MarliAdapterMenu { param([Parameter(Mandatory)][string]$Alias) while ($true) { try { $info = Get-MarliIpInfo -Alias $Alias } catch { Write-Host " Adapter '$Alias' not found anymore." -ForegroundColor Red Wait-Enter return } Write-MarliHeader $info.Description Show-MarliIpInfo $info Write-Host ' (D)HCP (S)tatic IP (R)estart (C)lear networkcard (B)ack' $mode = "$(Read-Host ' Select')".Trim().ToUpper() try { switch ($mode) { 'D' { Set-MarliDhcp -Alias $Alias } 'S' { $config = Read-MarliStaticConfig if ($config) { Set-MarliStatic -Alias $Alias @config } } 'R' { Restart-MarliAdapter -Alias $Alias } 'C' { if (Reset-MarliAdapter -Alias $Alias) { return } } 'B' { return } } } catch { Write-Host '' Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red Wait-Enter } } } #endregion #region ---------- Öffentliche Funktion ---------- function Start-MarliNet { <# .SYNOPSIS Manage IPv4 (DHCP / static), restart or reset a network card. .EXAMPLE Start-MarliNet .EXAMPLE Marli-Net #> [CmdletBinding()] param() if (-not (Test-MarliAdmin)) { Write-Warning 'Administrator rights required - starting an elevated PowerShell...' $manifest = Join-Path $script:ModuleRoot 'Marli-Net.psd1' $command = "Import-Module '$manifest' -Force; Start-MarliNet" try { Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( '-NoProfile', '-NoExit', '-Command', "& { $command }" ) -ErrorAction Stop } catch { Write-Warning 'Elevation was cancelled.' } return } while ($true) { $adapter = Select-MarliAdapter if (-not $adapter) { break } Invoke-MarliAdapterMenu -Alias $adapter.Name } Clear-Host } #endregion New-Alias -Name 'Marli-Net' -Value 'Start-MarliNet' -Force Export-ModuleMember -Function 'Start-MarliNet' -Alias 'Marli-Net' |