Net.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidAssignmentToAutomaticVariable', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSUseDeclaredVarsMoreThanAssignments', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"

if ($PSEdition -eq 'Desktop') {
    $IsWindows = $true
}

#region [classes] - [public]
Write-Debug "[$scriptName] - [classes] - [public] - Processing folder"
#region [classes] - [public] - [IPConfig]
Write-Debug "[$scriptName] - [classes] - [public] - [IPConfig] - Importing"
class IPConfig {
    # The interface name
    [string] $InterfaceName

    # The interface description
    [string] $Description

    # The interface status
    [System.Net.NetworkInformation.OperationalStatus] $Status

    # The address family
    [string] $AddressFamily

    # The IP address
    [string] $IPAddress

    # The prefix length
    [int] $PrefixLength

    # The subnet mask
    [string] $SubnetMask

    # The gateway
    [string] $Gateway

    # The DNS servers
    [string] $DNSServers

    IPConfig(
        [System.Net.NetworkInformation.NetworkInterface] $Interface,
        [System.Net.NetworkInformation.UnicastIPAddressInformation] $AddressInformation,
        [System.Net.NetworkInformation.IPInterfaceProperties] $InterfaceProperties
    ) {
        $this.InterfaceName = $Interface.Name
        $this.Description = $Interface.Description
        $this.Status = $Interface.OperationalStatus
        switch ($AddressInformation.Address.AddressFamily) {
            ([System.Net.Sockets.AddressFamily]::InterNetwork) { $this.AddressFamily = 'IPv4'; break }
            ([System.Net.Sockets.AddressFamily]::InterNetworkV6) { $this.AddressFamily = 'IPv6'; break }
            default { $this.AddressFamily = $AddressInformation.Address.AddressFamily.ToString() }
        }
        $this.IPAddress = $AddressInformation.Address.IPAddressToString
        $this.PrefixLength = $AddressInformation.PrefixLength

        if ($AddressInformation.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
            $this.SubnetMask = [IPConfig]::ConvertPrefixToMask($AddressInformation.PrefixLength)
        } else {
            # IPv6 masks are represented by prefix length
            $this.SubnetMask = $null
        }

        $this.Gateway = ($InterfaceProperties.GatewayAddresses | ForEach-Object { $_.Address.IPAddressToString }) -join ', '
        $this.DNSServers = ($InterfaceProperties.DnsAddresses | ForEach-Object { $_.IPAddressToString }) -join ', '
    }

    hidden static [string] ConvertPrefixToMask([int] $prefixLength) {
        if ($prefixLength -le 0) { return '0.0.0.0' }
        if ($prefixLength -ge 32) { return '255.255.255.255' }

        [int[]] $octets = 0, 0, 0, 0
        $bits = $prefixLength
        for ($i = 0; $i -lt 4; $i++) {
            $take = [Math]::Min(8, $bits)
            if ($take -le 0) {
                $octets[$i] = 0
            } else {
                $octets[$i] = 255 - ([math]::Pow(2, (8 - $take)) - 1)
            }
            $bits -= $take
        }
        return ($octets -join '.')
    }
}
Write-Debug "[$scriptName] - [classes] - [public] - [IPConfig] - Done"
#endregion [classes] - [public] - [IPConfig]
Write-Debug "[$scriptName] - [classes] - [public] - Done"
#endregion [classes] - [public]
#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [Get-SubnetMaskFromPrefix]
Write-Debug "[$scriptName] - [functions] - [private] - [Get-SubnetMaskFromPrefix] - Importing"
function Get-SubnetMaskFromPrefix {
    <#
        .SYNOPSIS
        Converts a CIDR prefix length into a subnet mask in dotted decimal notation.

        .DESCRIPTION
        The Get-SubnetMaskFromPrefix function accepts an integer prefix (e.g., 24) and converts it into a corresponding
        subnet mask (e.g., 255.255.255.0). It supports prefix lengths from 0 through 32. If the input prefix is outside
        this valid range, the function returns `$null`. This is useful when translating CIDR-style network definitions
        into traditional subnet mask format.

        .EXAMPLE
        Get-SubnetMaskFromPrefix -prefix 24

        Output:
        ```powershell
        255.255.255.0
        ```

        Converts a /24 prefix to the subnet mask 255.255.255.0.

        .OUTPUTS
        System.String

        .NOTES
        The subnet mask string in dotted decimal format (e.g., 255.255.255.0).
        Returns `$null` if the prefix is not within the valid range (0–32).

        .LINK
        https://psmodule.io/Net/Functions/Get-SubnetMaskFromPrefix
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The CIDR prefix length (0–32) to convert into a subnet mask.
        [Parameter(Mandatory)]
        [int] $prefix
    )

    if ($prefix -lt 0 -or $prefix -gt 32) { return $null }

    $bytes = [byte[]](0..3 | ForEach-Object {
            # Calculate the number of subnet bits for this octet (max 8, min 0)
            $bits = [Math]::Max([Math]::Min($prefix - (8 * $_), 8), 0)
            if ($bits -le 0) {
                # If no bits are set for this octet, value is 0
                0
            } elseif ($bits -ge 8) {
                # If all bits are set for this octet, value is 255
                255
            } else {
                # For partial octets, shift 0xFF left by (8 - $bits) to set the correct number of bits,
                # then mask with 0xFF to ensure only 8 bits are used
                ((0xFF -shl (8 - $bits)) -band 0xFF)
            }
        })
    [System.Net.IPAddress]::new($bytes).ToString()
}
Write-Debug "[$scriptName] - [functions] - [private] - [Get-SubnetMaskFromPrefix] - Done"
#endregion [functions] - [private] - [Get-SubnetMaskFromPrefix]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Get-NetIPConfiguration]
Write-Debug "[$scriptName] - [functions] - [public] - [Get-NetIPConfiguration] - Importing"
function Get-NetIPConfiguration {
    <#
        .SYNOPSIS
        Retrieves IP configuration details for network interfaces on the system.

        .DESCRIPTION
        This function gathers IP configuration data, including IP addresses, subnet masks, gateway addresses,
        and DNS servers for all network interfaces. It supports optional filtering by interface operational status
        (Up or Down) and address family (IPv4 or IPv6). The output includes detailed per-address information in
        a structured object format for each network interface and IP address combination.

        .EXAMPLE
        Get-NetIPConfiguration

        Output:
        ```powershell
        InterfaceName : Ethernet
        Description : Intel(R) Ethernet Connection
        Status : Up
        AddressFamily : InterNetwork
        IPAddress : 192.168.1.10
        PrefixLength : 24
        SubnetMask : 255.255.255.0
        Gateway : 192.168.1.1
        DNSServers : 8.8.8.8, 1.1.1.1
        ```

        Retrieves the IPv4 configuration for all network interfaces that are currently operational (Up).

        .OUTPUTS
        IPConfig

        .LINK
        https://psmodule.io/Net/Functions/Get-NetIPConfiguration
    #>


    [Alias('IPConfig')]
    [OutputType([IPConfig])]
    [CmdletBinding()]
    param(
        # Filters interfaces based on operational status ('Up' or 'Down')
        [Parameter()]
        [ValidateSet('Up', 'Down')]
        [string] $InterfaceStatus,

        # Filters IP addresses by address family ('IPv4' or 'IPv6')
        [Parameter()]
        [ValidateSet('IPv4', 'IPv6')]
        [string] $AddressFamily
    )

    # Map AddressFamily parameter to .NET enum
    $familyEnum = $null
    if ($AddressFamily) {
        $familyEnum = if ($AddressFamily -eq 'IPv4') {
            [System.Net.Sockets.AddressFamily]::InterNetwork
        } else {
            [System.Net.Sockets.AddressFamily]::InterNetworkV6
        }
    }

    $interfaces = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()

    # Apply optional interface status filter using enum for robustness
    if ($InterfaceStatus) {
        $statusEnum = [System.Net.NetworkInformation.OperationalStatus]::$InterfaceStatus
        $interfaces = $interfaces | Where-Object { $_.OperationalStatus -eq $statusEnum }
    }

    foreach ($adapter in $interfaces) {
        $ipProps = $adapter.GetIPProperties()

        # Filter unicast addresses by address family if requested
        $unicast = $ipProps.UnicastAddresses
        if ($familyEnum) {
            $unicast = $unicast | Where-Object { $_.Address.AddressFamily -eq $familyEnum }
        }

        foreach ($addr in $unicast) {
            [IPConfig]::new($adapter, $addr, $ipProps)
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Get-NetIPConfiguration] - Done"
#endregion [functions] - [public] - [Get-NetIPConfiguration]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]
#region Class exporter
# Get the internal TypeAccelerators class to use its static methods.
$TypeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
# Ensure none of the types would clobber an existing type accelerator.
# If a type accelerator with the same name exists, throw an exception.
$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get
# Define the types to export with type accelerators.
$ExportableEnums = @(
)
$ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." }
foreach ($Type in $ExportableEnums) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        Write-Verbose "Enum already exists [$($Type.FullName)]. Skipping."
    } else {
        Write-Verbose "Importing enum '$Type'."
        $TypeAcceleratorsClass::Add($Type.FullName, $Type)
    }
}
$ExportableClasses = @(
    [IPConfig]
)
$ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." }
foreach ($Type in $ExportableClasses) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        Write-Verbose "Class already exists [$($Type.FullName)]. Skipping."
    } else {
        Write-Verbose "Importing class '$Type'."
        $TypeAcceleratorsClass::Add($Type.FullName, $Type)
    }
}

# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach ($Type in ($ExportableEnums + $ExportableClasses)) {
        $null = $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()
#endregion Class exporter
#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = 'Get-NetIPConfiguration'
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter