SimpleIP.psm1

# Override Write-Verbose in this module so calling function is added to the message
function script:Write-Verbose
{
    [CmdletBinding()]
    param
    (
       [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [String] $Message
    )

    begin
    {}

    process
    {
        try
        {
            $PSBoundParameters['Message'] = $((Get-PSCallStack)[1].Command) + ': ' + $PSBoundParameters['Message']
        }
        catch
        {}

        Microsoft.PowerShell.Utility\Write-Verbose @PSBoundParameters
    }

    end
    {}
}

function Convert-IPv4Address
{
    <#
        .SYNOPSIS
            Convert IP address between different formats

        .DESCRIPTION
            Convert IP address between different formats
            - Quad dot (without mask) eg. "192.168.1.2"
            - Integer (uint32) eg. 3232235778
            - Binary (32 long string) eg. "11000000101010000000000100000010"

        .PARAMETER IP
            Input IP is either:
            - Quad dot (without mask) eg. "192.168.1.2"
            - Integer (uint32) eg. 3232235778
            - Binary (32 long string) eg. "11000000101010000000000100000010"

        .PARAMETER QuadDot
            Output in quad dot format, eg. "192.168.1.2"
            This is default output

        .PARAMETER Integer
            Output integer (uint32), eg 3232235778

        .PARAMETER Binary
            Output in binary (32 long string), eg. "11000000101010000000000100000010"

        .EXAMPLE
            Convert-IPv4Address -IP 192.168.1.2 -Integer
            3232235778

        .EXAMPLE
            Convert-IPv4Address -IP 192.168.1.2 -Binary
            11000000101010000000000100000010

        .EXAMPLE
            Convert-IPv4Address -IP 11000000101010000000000100000010 -QuadDot
            192.168.1.2

        .EXAMPLE
            3232235778 | Convert-IPv4Address
            192.168.1.2

        .EXAMPLE
            Convert-IPv4Address 192.168.1.2 -Integer
            3232235778
    #>


    [OutputType([System.String], ParameterSetName = 'QuadDot')]
    [OutputType([System.UInt32], ParameterSetName = 'Integer')]
    [OutputType([System.String], ParameterSetName = 'Binary')]
    [CmdletBinding(DefaultParameterSetName = 'QuadDot')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $IP,

        [Parameter(ParameterSetName = 'QuadDot')]
        [System.Management.Automation.SwitchParameter]
        $QuadDot,

        [Parameter(Mandatory = $true, ParameterSetName = 'Integer')]
        [System.Management.Automation.SwitchParameter]
        $Integer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Binary')]
        [System.Management.Automation.SwitchParameter]
        $Binary
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            [System.UInt32] $i = 0
            if (Test-IPv4Address -IP $IP)
            {
                $IP -split '\.' | ForEach-Object -Process {
                    $i = $i * 256 + $_
                }
            }
            elseif ($IP -match '^[01]{32}$')
            {
                $i = [System.Convert]::ToUInt32($IP, 2)
            }
            else
            {
                try
                {
                    $i = $IP
                }
                catch
                {
                    throw "$IP is not a valid IPv4 address"
                }
            }

            # Return
            if ($Integer)
            {
                $i
            }
            else
            {
                $b = [System.Convert]::ToString(([System.UInt32] $i), 2).PadLeft(32, '0')
                if ($Binary)
                {
                    $b
                }
                else
                {
                    (0..3 | ForEach-Object -Process {[System.Convert]::ToByte($b[(8*$_)..(8*$_+7)] -join '', 2)}) -join '.'
                }
            }
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Convert-IPv4Mask
{
    <#
        .SYNOPSIS
            Convert IP subnet mask between different formats

        .DESCRIPTION
            Convert IP subnet mask between different formats
            - Quad dot eg. "255.255.128.0"
            - Mask length (0-32) eg. "17"
            - Mask length with slash eg. "/17"
            - Integer (uint32) eg. "4294934528"
            - Binary (32 long string) eg. "11111111111111111000000000000000"

            If input is in quad dot format ("255.0.0.0"), output defaults to mask length with a leading slash ("/8")
            - else output defaults to quad dot format
            Output can be forced to be in specific format with switches

        .PARAMETER Mask
            Input subnet mask is either:
            - Quad dot eg. "255.255.128.0"
            - Mask length (0-32) eg. "17"
            - Mask length with slash eg. "/17"
            - Integer (uint32) eg. "4294934528"
            - Binary (32 long string) eg. "11111111111111111000000000000000"

        .PARAMETER QuadDot
            Output in quad dot format, eg. "255.255.128.0"

        .PARAMETER Length
            Output is in mask length, eg "17"

        .PARAMETER LengthWithSlash
            Output is in mask length with leading slash, eg "/17"

        .PARAMETER Integer
            Output integer (uint32), eg 4294934528

        .PARAMETER Binary
            Output in binary (32 long string), eg. "11111111111111111000000000000000"

        .EXAMPLE
            Convert-IPv4Mask -Mask 255.255.128.0
            /17

        .EXAMPLE
            Convert-IPv4Mask -Mask /17 -Length
            17

        .EXAMPLE
            Convert-IPv4Mask -Mask /17
            255.255.128.0

        .EXAMPLE
            Convert-IPv4Mask -Mask 17
            255.255.128.0

        .EXAMPLE
            Convert-IPv4Mask -Mask 17 -Binary
            11111111111111111000000000000000
    #>


    [OutputType([System.String], ParameterSetName = 'Default')]
    [OutputType([System.String], ParameterSetName = 'QuadDot')]
    [OutputType([System.Byte]  , ParameterSetName = 'Length')]
    [OutputType([System.String], ParameterSetName = 'LengthWithSlash')]
    [OutputType([System.UInt32], ParameterSetName = 'Integer')]
    [OutputType([System.String], ParameterSetName = 'Binary')]

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $Mask,

        [Parameter(Mandatory = $true, ParameterSetName = 'QuadDot')]
        [System.Management.Automation.SwitchParameter]
        $QuadDot,

        [Parameter(Mandatory = $true, ParameterSetName = 'Length')]
        [System.Management.Automation.SwitchParameter]
        $Length,

        [Parameter(Mandatory = $true, ParameterSetName = 'LengthWithSlash')]
        [System.Management.Automation.SwitchParameter]
        $LengthWithSlash,

        [Parameter(Mandatory = $true, ParameterSetName = 'Integer')]
        [System.Management.Automation.SwitchParameter]
        $Integer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Binary')]
        [System.Management.Automation.SwitchParameter]
        $Binary
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $quadDotInput = $false
            [System.UInt32] $i = 0
            if ($Mask -match '^/?([0-9]|[12][0-9]|3[0-2])$')
            {
                $i = [System.Convert]::ToUInt32(('1' * $Matches[1] + '0' * (32 - $Matches[1])), 2)
            }
            else
            {
                try
                {
                    if (-not ($Mask | Convert-IPv4Address | Test-IPv4Address -Mask)) {throw}
                }
                catch
                {
                    throw "$Mask is not a valid subnet mask"
                }

                if (Test-IPv4Address -IP $Mask) {$quadDotInput = $true}
                $i = Convert-IPv4Address -IP $Mask -Integer
            }

            if (($output = $PSCmdlet.ParameterSetName) -eq 'Default')
            {
                $output = if ($quadDotInput) {'LengthWithSlash'} else {'QuadDot'}
            }

            # Return
            switch ($output)
            {
                'QuadDot'         {$i | Convert-IPv4Address}
                'Length'          {[System.Byte] [regex]::Matches(($i | Convert-IPv4Address -Binary), '1').Count}
                'LengthWithSlash' {'/' + [regex]::Matches(($i | Convert-IPv4Address -Binary), '1').Count}
                'Integer'         {$i}
                'Binary'          {$i | Convert-IPv4Address -Binary}
            }
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Convert-IPv6Address
{
    <#
        .SYNOPSIS
            Convert IPv6 address between formats

        .DESCRIPTION
            Convert IPv6 address between formats
            Also compress/compact IPv6 address.
            (IPv6 addresses can be hard to compare ("0::1" -eq "::1"),
            but they are run throug this command, they can be compared)
            Output defaults to compacted IPv6 address (eg. "::1")

        .PARAMETER IP
            Input IP is either
            - Standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")
            - [uint16[]] array with 8 elements
            - Binary (string containinging 128 "0" or "1" - spaces are allowed)

        .PARAMETER Prefix
            If prefix is not set in IP address, it must be set with this parameter

        .PARAMETER Info
            Output object with IP and prefix in different formats

        .EXAMPLE
            Convert-IPv6Address 00ab:00:0:000:00:fff::1
            ab::fff:0:1

        .EXAMPLE
            Convert-IPv6Address 00ab:00:0:000:00:fff::1/64
            ab::fff:0:1/64

        .EXAMPLE
            Convert-IPv6Address -IP a:b:c::/64 -Info
            IP : a:b:c::/64
            IPCompact : a:b:c::
            IPExpanded : 000a:000b:000c:0000:0000:0000:0000:0000
            IPIntArray : {10, 11, 12, 0...}
            IPHexArray : {a, b, c, 0...}
            IPHexArrayExpanded : {000a, 000b, 000c, 0000...}
            IPBinary : 0000000000001010 0000000000001011 0000000000001100 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000
            Cidr : a:b:c::/64
            CidrExpanded : 000a:000b:000c:0000:0000:0000:0000:0000/64
            Prefix : 64
            PrefixIntArray : {65535, 65535, 65535, 65535...}
            PrefixHexArray : {ffff, ffff, ffff, ffff...}
            PrefixHexArrayExpanded : {ffff, ffff, ffff, ffff...}
            PrefixHexString : ffff:ffff:ffff:ffff:0:0:0:0
            PrefixHexStringExpanded : ffff:ffff:ffff:ffff:0000:0000:0000:0000
            PrefixBinary : 1111111111111111 1111111111111111 1111111111111111 1111111111111111 0000000000000000 0000000000000000 0000000000000000 0000000000000000
    #>


    [OutputType([System.String],  ParameterSetName = 'Default')]
    [OutputType([PSCustomObject], ParameterSetName = 'Info')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.Object]
        $IP,

        [Parameter()]
        [Nullable[System.Byte]]
        $Prefix,

        [Parameter(Mandatory = $true, ParameterSetName = 'Info')]
        [System.Management.Automation.SwitchParameter]
        $Info
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            if ($Prefix -ne $null -and $Prefix -gt 128) {throw 'Prefix is higher than 128'}

            [System.UInt16[]] $ipIntArray = @()

            if ($IP -is [System.String] -and ($nospaceIP = $IP -replace ' ') -and $nospaceIP -match '^[01]{128}$')
            {
                $ipIntArray = (0..7).ForEach({ [System.Convert]::ToUInt16($nospaceIP.Substring(($_*16), 16), 2) })
            }
            elseif ($IP -is [System.String])
            {
                if (-not ($IP -match '^([0-9a-f:]+)(/(([1-9]?[0-9])|(1[01][0-9])|(12[0-8])))?$')) {throw "Error parsing IPv6 address $IP"}
                $ipOnly = $Matches[1]
                if ($Prefix -eq $null)
                {
                    $Prefix = $Matches[3]
                }
                elseif ($Matches[3] -ne $null -and $Prefix -ne $Matches[3])
                {
                    "Prefix set to /$($Matches[3]) in -IP but /$Prefix in -Prefix for $IP. Using /$Prefix" | Write-Warning
                }

                try
                {
                    if (
                        $ipOnly -match '[^0-9a-f:]' -or  # Something else than hex or colon
                        $ipOnly -match '::.+::' -or      # two double colon
                        $ipOnly -match ':::' -or         # tripple colon
                        $ipOnly -match '^:[^:]' -or      # start with single colon
                        $ipOnly -match '[^:]:$'          # end with single colon
                    ) {throw}
                    $ipOnly = $ipOnly -replace '::',':*:'
                    $ipSplit = $ipOnly -split ':'| Where-Object -FilterScript {$_ -ne ''}
                    if ($ipSplit.Count -gt 8) {throw}
                    $ipIntArray = $ipSplit | ForEach-Object -Process {
                        if ($_ -eq '*') { [System.UInt16[]] (@(0) * (9 - $ipSplit.Count)) }
                        else            { [System.Convert]::ToUInt16($_, 16)              }
                    }
                    if ($ipIntArray.Count -ne 8) {throw}
                }
                catch
                {
                    throw "Error parsing IPv6 address $IP"
                }
            }
            elseif ($IP -is [array] -and $IP.Count -eq 8)
            {
                try { $ipIntArray = $IP } catch { throw 'Input IP is in unknown format' }
            }
            else
            {
                throw 'Input IP is in unknown format'
            }


            $cidr = $cidrExpanded = $prefixIntArray = $prefixHexArray = $prefixHexArrayExpanded = $prefixHexString = $prefixHexStringExpanded = $prefixBinary = $null
            $ipHexArrayExpanded = $ipIntArray | ForEach-Object -Process { '{0:x4}' -f $_ }
            $ipHexArray         = $ipIntArray | ForEach-Object -Process { '{0:x}'  -f $_ }
            $ipExpanded         = $ipHexArrayExpanded -join ':'
            $ipCompact          = $ipHexArray -join ':'
            foreach ($i in (7..0)) { if ($ipCompact -ne ($ipCompact = $ipCompact -replace "(^|:)0(:0){$i}(:|`$)",'::')) {break} }
            $ipReturn           = $ipCompact
            $ipBinary = ($ipIntArray | ForEach-Object -Process {[System.Convert]::ToString(([System.UInt16] $_), 2).PadLeft(16, '0')}) -join ' '

            if ($Prefix -ne $null)
            {
                $ipReturn = $cidr        = '{0}/{1}' -f $ipCompact, $Prefix
                $cidrExpanded            = '{0}/{1}' -f $ipExpanded, $Prefix
                $prefixBinary            = '1' * $Prefix + '0' * (128 - $Prefix)
                $prefixIntArray          = (0..7).ForEach({ [System.Convert]::ToUInt16($prefixBinary.Substring(($_*16), 16), 2) })
                $prefixHexArrayExpanded  = $prefixIntArray | ForEach-Object -Process { '{0:x4}' -f $_ }
                $prefixHexArray          = $prefixIntArray | ForEach-Object -Process { '{0:x}'  -f $_ }
                $prefixHexString         = $prefixHexArray -join ':'
                $prefixHexStringExpanded = $prefixHexArrayExpanded -join ':'
                $prefixBinary            = ($prefixIntArray | ForEach-Object -Process {[System.Convert]::ToString(([System.UInt16] $_), 2).PadLeft(16, '0')}) -join ' '
            }

            $r = [PSCustomObject] @{
                IP                      = $ipReturn
                IPCompact               = $ipCompact
                IPExpanded              = $ipExpanded
                IPIntArray              = $ipIntArray
                IPHexArray              = $ipHexArray
                IPHexArrayExpanded      = $ipHexArrayExpanded
                IPBinary                = $ipBinary
                Cidr                    = $cidr
                CidrExpanded            = $cidrExpanded
                Prefix                  = $Prefix
                PrefixIntArray          = $prefixIntArray
                PrefixHexArray          = $prefixHexArray
                PrefixHexArrayExpanded  = $prefixHexArrayExpanded
                PrefixHexString         = $prefixHexString
                PrefixHexStringExpanded = $prefixHexStringExpanded
                PrefixBinary            = $prefixBinary
            }

            # Return
            if ($Info) { $r    }
            else       { $r.IP }
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Get-IPv4Address
{
    <#
        .SYNOPSIS
            Get IP subnet, mask, broadcast for an IP address

        .DESCRIPTION
            Get IP subnet, mask, broadcast for an IP address

        .PARAMETER IP
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"
            If input is IP without subnet mask (eg. "127.0.0.1") then -Mask parameter must be set

        .PARAMETER Mask
            If input IP is in format without subnet mask, this parameter must be set to either
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .PARAMETER SameIP
            Return same IP as input IP (why? maybe in a different format back)
            If input is "10.11.12.13/24", then "10.11.12.13/24" is returned

        .PARAMETER Subnet
            Return subnet
            If input is "10.11.12.13/24", then "10.11.12.0/24" is returned

        .PARAMETER Broadcast
            Return broadcast
            If input is "10.11.12.13/24", then "10.11.12.255/24" is returned

        .PARAMETER First
            Return first usable IP in subnet
            If input is "10.11.12.13/24", then "10.11.12.1/24" is returned

        .PARAMETER Last
            Return last usable IP in subnet
            If input is "10.11.12.13/24", then "10.11.12.254/24" is returned

        .PARAMETER All
            Return all usable IPs in subnet
            If input is "10.11.12.13/24", then an array with IP addresses from "10.11.12.1/24" to "10.11.12.254/24" is returned

        .PARAMETER Pool
            Treat subnet and broadcast adddresses as usable
            First IP will be same as subnet and last IP will be the same as broadcast

        .PARAMETER WithMaskLength
            Return in "127.0.0.1/8" format
            This is default output

        .PARAMETER WithMask
            Return in "127.0.0.1 255.0.0.0" format

        .PARAMETER IPOnly
            Return in "127.0.0.1" format

        .PARAMETER MaskQuadDotOnly
            Only return subnet mask in "255.0.0.0" format

        .PARAMETER MaskLengthOnly
            Only return subnet mask in "8" format

        .PARAMETER MaskLengthWithSlashOnly
            Only return subnet mask in "/8" format

        .PARAMETER Info
            Return object with different info

        .EXAMPLE
            Get-IPv4Address -IP 127.0.0.1/8 -Subnet
            127.0.0.0/24

        .EXAMPLE
            Get-IPv4Address -IP 127.0.0.1/8 -Subnet -WithMask
            127.0.0.0 255.0.0.0

        .EXAMPLE
            Get-IPv4Address -IP 127.0.0.1/8 -Broadcast -WithMask
            127.255.255.255 255.0.0.0

        .EXAMPLE
            Get-IPv4Address -IP 127.0.0.1/8 -Broadcast -IPOnly
            127.255.255.255

        .EXAMPLE
            Get-IPv4Address -IP 10.100.200.201 -Mask /30 -All -WithMask
            10.100.200.201 255.255.255.252
            10.100.200.202 255.255.255.252

        .EXAMPLE
            Get-IPv4Address -IP 192.168.0.150/255.255.255.128 -Info
            IP : 192.168.0.150
            Subnet : 192.168.0.128
            FirstIP : 192.168.0.129
            LastIP : 192.168.0.254
            Broadcast : 192.168.0.255
            Integer : @{IP=3232235670; Subnet=3232235648; FirstIP=3232235...
            Binary : @{IP=11000000101010000000000010010110; Subnet=11000...
            MaskQuadDot : 255.255.255.128
            MaskLength : 25
    #>


    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        [Parameter()]
        [ValidateScript({ (Test-IPv4Address -IP $_ -Mask -AllowLength) -or $(throw "$_ is not a valid IPv4 mask") })]
        [System.String]
        $Mask = '',

        [Parameter(ParameterSetName = 'SameIPWithMaskLength')]
        [Parameter(ParameterSetName = 'SameIPWithMask')]
        [Parameter(ParameterSetName = 'SameIPIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $SameIP,

        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetWithMaskLength')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $Subnet,

        [Parameter(Mandatory = $true, ParameterSetName = 'BroadcastWithMaskLength')]
        [Parameter(Mandatory = $true, ParameterSetName = 'BroadcastWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'BroadcastIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $Broadcast,

        [Parameter(Mandatory = $true, ParameterSetName = 'FirstWithMaskLength')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FirstWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FirstIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $First,

        [Parameter(Mandatory = $true, ParameterSetName = 'LastWithMaskLength')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LastWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LastIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $Last,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllWithMaskLength')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $All,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Pool,

        [Parameter(ParameterSetName = 'SameIPWithMaskLength')]
        [Parameter(ParameterSetName = 'SubnetWithMaskLength')]
        [Parameter(ParameterSetName = 'BroadcastWithMaskLength')]
        [Parameter(ParameterSetName = 'FirstWithMaskLength')]
        [Parameter(ParameterSetName = 'LastWithMaskLength')]
        [Parameter(ParameterSetName = 'AllWithMaskLength')]
        [System.Management.Automation.SwitchParameter]
        $WithMaskLength,

        [Parameter(Mandatory = $true, ParameterSetName = 'SameIPWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'BroadcastWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FirstWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LastWithMask')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllWithMask')]
        [System.Management.Automation.SwitchParameter]
        $WithMask,

        [Parameter(Mandatory = $true, ParameterSetName = 'SameIPIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'BroadcastIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'FirstIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LastIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'AllIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'MaskQuadDotOnly')]
        [System.Management.Automation.SwitchParameter]
        $MaskQuadDotOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'MaskLengthOnly')]
        [System.Management.Automation.SwitchParameter]
        $MaskLengthOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'MaskLengthWithSlashOnly')]
        [System.Management.Automation.SwitchParameter]
        $MaskLengthWithSlashOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'Info')]
        [System.Management.Automation.SwitchParameter]
        $Info
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            if ($Mask -eq '')
            {
                if (Test-IPv4Address -IP $IP) {throw "No mask defined for $IP"}
            }
            else
            {
                $Mask = $Mask | Convert-IPv4Mask -Length
            }

            if (-not (Test-IPv4Address -IP $IP))
            {
                ($IP, $m) = $IP -split '[/ ]'
                $m = $m | Convert-IPv4Mask -Length
                if ($Mask -eq '')
                {
                    $Mask = $m
                }
                elseif ($Mask -ne $m)
                {
                    "Mask set to /$m in -IP but /$Mask in -Mask for $IP. Using /$Mask" | Write-Warning
                }
            }

            [System.String] $maskQuadDot  = $Mask | Convert-IPv4Mask -QuadDot
            [System.UInt32] $maskInt      = $Mask | Convert-IPv4Mask -Integer
            [System.UInt32] $ipInt        = $IP | Convert-IPv4Address -Integer
            [System.UInt32] $subnetInt    = $ipInt -band $maskInt
            [System.UInt32] $broadcastInt = $ipInt -bor (-bnot $maskInt)
            [System.UInt32] $firstInt     = $subnetInt
            [System.UInt32] $lastInt      = $broadcastInt
            if (-not ($Pool -or $Mask -eq 31 -or $Mask -eq 32))
            {
                ++$firstInt
                --$lastInt
            }

            if ($Info)
            {
                [PSCustomObject] @{
                    IP          = $ipInt        | Convert-IPv4Address
                    Subnet      = $subnetInt    | Convert-IPv4Address
                    FirstIP     = $firstInt     | Convert-IPv4Address
                    LastIP      = $lastInt      | Convert-IPv4Address
                    Broadcast   = $broadcastInt | Convert-IPv4Address
                    Integer     = [PSCustomObject] @{
                        IP          = $ipInt
                        Subnet      = $subnetInt
                        FirstIP     = $firstInt
                        LastIP      = $lastInt
                        Broadcast   = $broadcastInt
                    }
                    Binary     = [PSCustomObject] @{
                        IP          = $ipInt        | Convert-IPv4Address -Binary
                        Subnet      = $subnetInt    | Convert-IPv4Address -Binary
                        FirstIP     = $firstInt     | Convert-IPv4Address -Binary
                        LastIP      = $lastInt      | Convert-IPv4Address -Binary
                        Broadcast   = $broadcastInt | Convert-IPv4Address -Binary
                    }
                    MaskQuadDot = $maskQuadDot
                    MaskLength  = $Mask
                }
            }
            else
            {
                $createScript =
                    if     ($Subnet)    { {$subnetInt} }
                    elseif ($Broadcast) { {$broadcastInt} }
                    elseif ($First)     { {$firstInt} }
                    elseif ($Last)      { {$lastInt} }
                    elseif ($All)       { {for ([uint32] $i = $firstInt; $i -le $lastInt; $i++) {$i}} }
                    else                { {$ipInt} }

                $outputScript =
                    if     ($WithMask)                { {'{0} {1}' -f $_, $maskQuadDot} }
                    elseif ($IPOnly)                  { {$_} }
                    elseif ($MaskQuadDotOnly)         { {$maskQuadDot} }
                    elseif ($MaskLengthOnly)          { {$Mask} }
                    elseif ($MaskLengthWithSlashOnly) { {'/{0}' -f $Mask} }
                    else                              { {'{0}/{1}' -f $_, $Mask} }

                $createScript.Invoke() | Convert-IPv4Address | ForEach-Object -Process $outputScript
            }
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Get-IPv4Mask
{
    <#
        .SYNOPSIS
            Get IP subnet mask for an IP address

        .DESCRIPTION
            Get IP subnet mask for an IP address

        .PARAMETER IP
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"

        .PARAMETER QuadDot
            Return subnet mask in "255.0.0.0" format

        .PARAMETER Length
            Return subnet mask in "8" format

        .PARAMETER LengthWithSlash
            Return subnet mask in "/8" format

        .EXAMPLE
            Get-IPv4Mask 9.8.7.6/22
            255.255.252.0

        .EXAMPLE
            Get-IPv4Mask 9.8.7.6/22 -LengthWithSlash
            /22

        .EXAMPLE
            Get-IPv4Mask 9.8.7.6/255.255.252.0 -Length
            22
    #>


    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        #[Parameter()]
        #[ValidateScript({ (Test-IPv4Address -IP $_ -Mask -AllowLength) -or $(throw "$_ is not a valid IPv4 mask") })]
        #[System.String]
        #$Mask = '',

        [Parameter(ParameterSetName = 'QuadDot')]
        [System.Management.Automation.SwitchParameter]
        $QuadDot,

        [Parameter(Mandatory = $true, ParameterSetName = 'Length')]
        [System.Management.Automation.SwitchParameter]
        $Length,

        [Parameter(Mandatory = $true, ParameterSetName = 'LengthWithSlash')]
        [System.Management.Automation.SwitchParameter]
        $LengthWithSlash
    )

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        $params = @{
            IP = $IP
        }
        #if ($Mask -ne '') {$params['Mask'] = $Mask}

        if     ($Length)          {$params['MaskLengthOnly']          = $true}
        elseif ($LengthWithSlash) {$params['MaskLengthWithSlashOnly'] = $true}
        else                      {$params['MaskQuadDotOnly']         = $true}

        Get-IPv4Address @params

        Write-Verbose -Message 'Process end'
    }
}

function Get-IPv4Subnet
{
    <#
        .SYNOPSIS
            Get IP subnet for an IP address

        .DESCRIPTION
            Get IP subnet for an IP address

        .PARAMETER IP
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"
            If input is IP without subnet mask (eg. "127.0.0.1") then -Mask parameter must be set

        .PARAMETER Mask
            If input IP is in format without subnet mask, this parameter must be set to either
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .PARAMETER WithMaskLength
            Return in "127.0.0.0/8" format
            This is default output

        .PARAMETER WithMask
            Return in "127.0.0.0 255.0.0.0" format

        .PARAMETER IPOnly
            Return in "127.0.0.1" format

        .EXAMPLE
            Get-IPv4Subnet 127.0.0.1/8
            127.0.0.0/8

        .EXAMPLE
            Get-IPv4Subnet -IP 10.20.30.40/28 -WithMask
            10.20.30.32 255.255.255.240

        .EXAMPLE
            Get-IPv4Subnet -IP '10.20.30.40 255.255.255.240' -IPOnly
            10.20.30.32
    #>


    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        [Parameter()]
        [ValidateScript({ (Test-IPv4Address -IP $_ -Mask -AllowLength) -or $(throw "$_ is not a valid IPv4 mask") })]
        [System.String]
        $Mask = '',

        [Parameter(ParameterSetName = 'SubnetWithMaskLength')]
        [System.Management.Automation.SwitchParameter]
        $WithMaskLength,

        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetWithMask')]
        [System.Management.Automation.SwitchParameter]
        $WithMask,

        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly
    )

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"
        Get-IPv4Address -Subnet @PSBoundParameters
        Write-Verbose -Message 'Process end'
    }
}

function Get-IPv6Address
{
    <#
        .SYNOPSIS
            Get subnet, prefix, ... for an IPv6 address

        .DESCRIPTION
            Get subnet, prefix, ... for an IPv6 address

        .PARAMETER IP
            Input IP is standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")

        .PARAMETER Prefix
            If prefix is not set in IP address, it must be set with this parameter

        .PARAMETER SameIP
            Return same IP as input IP (why? maybe in a different format back)
            If input is "7:6:5::77:88/56", then "7:6:5::77:88/56" is returned

        .PARAMETER Subnet
            Return subnet
            If input is "7:6:5::77:88/56", then "7:6:5::/56" is returned

        .PARAMETER WithPrefix
            Return in "7:6:5::/64" format
            This is default output
 
        .PARAMETER IPOnly
            Return in "7:6:5::" format

        .PARAMETER PrefixOnly
            Only return prefix in "64" format

        .PARAMETER PrefixWithSlashOnly
            Only return prefix in "/64" format

        .PARAMETER Info
            Return object with different info

        .EXAMPLE
            Get-IPv6Address -IP 007:6:5::77:88/64 -Subnet
            7:6:5::/64

        .EXAMPLE
            Get-IPv6Address -IP 007:6:5::77:88/64 -Subnet -IPOnly
            7:6:5::

        .EXAMPLE
            Get-IPv6Address -IP 007:6:5::77:88/64 -IPOnly
            7:6:5::77:88

        .EXAMPLE
            Get-IPv6Address -IP 007:6:5::77:88/64 -Info
            IP : 7:6:5::77:88/64
            Subnet : 7:6:5::/64
            FirstIP : 7:6:5::/64
            SecondIP : 7:6:5::1/64
            PenultimateIP : 7:6:5::ffff:ffff:ffff:fffe/64
            LastIP : 7:6:5::ffff:ffff:ffff:ffff/64
            Objects : @{IP=; Subnet=; FirstIP=; SecondIP=; PenultimateIP=; LastIP=}
    #>


    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $IP,

        [Parameter()]
        [Nullable[System.Byte]]
        $Prefix,

        [Parameter(ParameterSetName = 'SubnetWithPrefix')]
        [Parameter(ParameterSetName = 'SameIPIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $SameIP,

        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetWithPrefix')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $Subnet,

        #[Parameter(Mandatory = $true, ParameterSetName = 'BroadcastWithPrefix')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'BroadcastIPOnly')]
        #[System.Management.Automation.SwitchParameter]
        #$Broadcast,

        #[Parameter(Mandatory = $true, ParameterSetName = 'FirstWithPrefix')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'FirstIPOnly')]
        #[System.Management.Automation.SwitchParameter]
        #$First,

        #[Parameter(Mandatory = $true, ParameterSetName = 'LastWithPrefix')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'LastIPOnly')]
        #[System.Management.Automation.SwitchParameter]
        #$Last,

        #[Parameter(Mandatory = $true, ParameterSetName = 'AllWithPrefix')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'AllIPOnly')]
        #[System.Management.Automation.SwitchParameter]
        #$All,

        #[Parameter()]
        #[System.Management.Automation.SwitchParameter]
        #$Pool,

        [Parameter(ParameterSetName = 'SameIPWithPrefix')]
        [Parameter(ParameterSetName = 'SubnetWithPrefix')]
        #[Parameter(ParameterSetName = 'BroadcastWithPrefix')]
        #[Parameter(ParameterSetName = 'FirstWithPrefix')]
        #[Parameter(ParameterSetName = 'LastWithPrefix')]
        #[Parameter(ParameterSetName = 'AllWithPrefix')]
        [System.Management.Automation.SwitchParameter]
        $WithPrefix,

        [Parameter(Mandatory = $true, ParameterSetName = 'SameIPIPOnly')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'BroadcastIPOnly')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'FirstIPOnly')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'LastIPOnly')]
        #[Parameter(Mandatory = $true, ParameterSetName = 'AllIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'PrefixOnly')]
        [System.Management.Automation.SwitchParameter]
        $PrefixOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'PrefixWithSlashOnly')]
        [System.Management.Automation.SwitchParameter]
        $PrefixWithSlashOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'Info')]
        [System.Management.Automation.SwitchParameter]
        $Info
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $prefixParam = if ($Prefix -eq $null) { @{} } else { @{Prefix = $Prefix} }
            $ipInfo = Convert-IPv6Address -Info -IP $IP @prefixParam

            if ($ipInfo.Prefix -eq $null) {throw "No prefix defined for $IP"}
            $Prefix = $ipInfo.Prefix

            # Yeah yeah, I know that there's now broadcast IP in IPv6 and that every IP can be used!
            [uint16[]] $ipInt        = $ipInfo.IPIntArray
            [uint16[]] $prefixInt    = $ipInfo.PrefixIntArray

            # Why the f**k does binary operators not work properly with uint16
            # For IPv4 it's just "$ip -band $mask" and "$ip -bor (-bnot $mask)"
            [uint16[]] $subnetInt      = (0..7).ForEach({ [uint16] (([uint32]$ipInt[$_]) -band ([uint32]$prefixInt[$_])) })
            [uint16[]] $lastInt        = (0..7).ForEach({ [uint16] ((([uint32]$ipInt[$_]) -bor (-bnot ([uint32]$prefixInt[$_]))) -band [uint16]::MaxValue)})
            [uint16[]] $firstInt       = $subnetInt.Clone()
            [uint16[]] $secondInt      = $firstInt.Clone()
            [uint16[]] $penultimateInt = $lastInt.Clone()
            if ($Prefix -ne 128)
            {
                # FIXXXME - should we do something else if it's /128? Set to $null?
                ++$secondInt[7]
                --$penultimateInt[7]
            }

            if ($Info)
            {
                $objects = [PSCustomObject] @{
                    IP            = Convert-IPv6Address -IP $ipInt          -Prefix $Prefix -Info
                    Subnet        = Convert-IPv6Address -IP $subnetInt      -Prefix $Prefix -Info
                    FirstIP       = Convert-IPv6Address -IP $firstInt       -Prefix $Prefix -Info
                    SecondIP      = Convert-IPv6Address -IP $secondInt      -Prefix $Prefix -Info
                    PenultimateIP = Convert-IPv6Address -IP $penultimateInt -Prefix $Prefix -Info
                    LastIP        = Convert-IPv6Address -IP $lastInt        -Prefix $Prefix -Info
                }
                [PSCustomObject] @{
                    IP            = $objects.IP.IP
                    Subnet        = $objects.Subnet.IP
                    FirstIP       = $objects.FirstIP.IP
                    SecondIP      = $objects.SecondIP.IP
                    PenultimateIP = $objects.PenultimateIP.IP
                    LastIP        = $objects.LastIP.IP
                    Prefix        = $Prefix
                    Objects       = $objects
                }
            }
            else
            {
                $createScript =
                    if     ($Subnet)    { {,@($subnetInt)} }
                    #elseif ($Broadcast) { {$broadcastInt} }
                    #elseif ($First) { {$firstInt} }
                    #elseif ($Last) { {$lastInt} }
                    #elseif ($All) { {for ([uint32] $i = $firstInt; $i -le $lastInt; $i++) {$i}} }
                    else                { {,@($ipInt)} }

                $outputScript =
                    if     ($IPOnly)              { {(Convert-IPv6Address -IP $_ -Prefix $Prefix -Info).IPCompact} }
                    elseif ($PrefixOnly)          { {'{0}' -f $Prefix} }
                    elseif ($PrefixWithSlashOnly) { {'/{0}' -f $Prefix} }
                    else                          { {Convert-IPv6Address -IP $_ -Prefix $Prefix} }

                $createScript.Invoke() | ForEach-Object -Process $outputScript
            }

        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Get-IPv6Subnet
{
    <#
        .SYNOPSIS
            Get IP subnet for an IPv6 address

        .DESCRIPTION
            Get IP subnet for an IPv6 address

        .PARAMETER IP
            Input IP is standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")

        .PARAMETER Prefix
            If prefix is not set in IP address, it must be set with this parameter

        .PARAMETER WithPrefix
            Return in "7:6:5::/64" format
            This is default output
 
        .PARAMETER IPOnly
            Return in "7:6:5::" format

        .EXAMPLE
            Get-IPv6Subnet -IP 7:6:5::77:88/64
            7:6:5::/64

        .EXAMPLE
            Get-IPv6Subnet -IP 7:6:5::77:88/64 -IPOnly
            7:6:5::
    #>


    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $IP,

        [Parameter()]
        [Nullable[System.Byte]]
        $Prefix,

        [Parameter(ParameterSetName = 'SubnetWithPrefix')]
        [System.Management.Automation.SwitchParameter]
        $WithPrefix,

        [Parameter(Mandatory = $true, ParameterSetName = 'SubnetIPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly
    )

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"
        Get-IPv6Address -Subnet @PSBoundParameters
        Write-Verbose -Message 'Process end'
    }
}

function Test-IPv4Address
{
    <#
        .SYNOPSIS
            Test if a string contains a valid IP address (IPv4)

        .DESCRIPTION
            Test if a string contains a valid IP address (IPv4)
            Uses regex to test
            Returns [bool]

        .PARAMETER IP
            IP address (or subnet mask) to test is valid or not

        .PARAMETER IPOnly
            Only return True if input is valid IPv4 in quad dot format (without subnet mask)
            Eg. "127.0.0.1"
            This is default

        .PARAMETER Mask
            Only return True if input is valid IPv4 subnet mask in quad dot format
            Eg. "255.255.255.0"

        .PARAMETER AllowLength
            Used along with -Mask
            Only return true if input is subnet mask in:
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .PARAMETER AllowMask
            Return true if input is either
            - IP in quad dot, eg. "127.0.0.1"
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"

        .PARAMETER RequireMask
            Return true if input is IP with mask, either
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"

        .EXAMPLE
            Test-IPv4Address -IP 127.0.0.1
            True

        .EXAMPLE
            Test-IPv4Address -IP 127.0.0.256
            False

        .EXAMPLE
            Test-IPv4Address -IP 127.0.0.1/32
            False

        .EXAMPLE
            Test-IPv4Address -AllowMask -IP 127.0.0.1/32
            True

        .EXAMPLE
            Test-IPv4Address -AllowMask -IP "127.0.0.1 255.255.255.255"
            True

        .EXAMPLE
            Test-IPv4Address -AllowMask -IP "127.0.0.1"
            True

        .EXAMPLE
            Test-IPv4Address -RequireMask -IP 127.0.0.1/32
            True

        .EXAMPLE
            Test-IPv4Address -RequireMask -IP 127.0.0.1
            False

        .EXAMPLE
            Test-IPv4Address -Mask -IP 255.255.0.0
            True

        .EXAMPLE
            Test-IPv4Address -Mask -IP 255.0.255.0
            False

        .EXAMPLE
            Test-IPv4Address -Mask -IP 32
            False

        .EXAMPLE
            Test-IPv4Address -Mask -AllowLength -IP 255.0.255.0
            False

        .EXAMPLE
            Test-IPv4Address -Mask -AllowLength -IP 255.255.0.0
            True

        .EXAMPLE
            Test-IPv4Address -Mask -AllowLength -IP 32
            True

        .EXAMPLE
            Test-IPv4Address -Mask -AllowLength -IP /32
            True
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding(DefaultParameterSetName = 'IPOnly')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $IP,

        [Parameter(ParameterSetName = 'IPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'Mask')]
        [System.Management.Automation.SwitchParameter]
        $Mask,

        [Parameter(ParameterSetName = 'Mask')]
        [System.Management.Automation.SwitchParameter]
        $AllowLength,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllowMask')]
        [System.Management.Automation.SwitchParameter]
        $AllowMask,

        [Parameter(Mandatory = $true, ParameterSetName = 'RequireMask')]
        [System.Management.Automation.SwitchParameter]
        $RequireMask
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $i = '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
            $s = '(((255\.){3}(255|254|252|248|240|224|192|128|0+))|((255\.){2}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){2})|((255|254|252|248|240|224|192|128|0+)(\.0+){3}))'
            $b = '[0-9]|[12][0-9]|3[0-2]'
            $matchIP            = "^($i)$"
            $matchMask          = "^($s)$"
            $matchMaskAndLength = "^(($s)|(/?($b)))$"
            $matchAll           = "^($i)[/ ](($s)|($b))$"

            (
                ($PSCmdlet.ParameterSetName -eq 'IPOnly'      -and $IP -match $matchIP                                  ) -or
                ($PSCmdlet.ParameterSetName -eq 'Mask'        -and $IP -match $matchMask          -and -not $AllowLength) -or
                ($PSCmdlet.ParameterSetName -eq 'Mask'        -and $IP -match $matchMaskAndLength -and      $AllowLength) -or
                ($PSCmdlet.ParameterSetName -eq 'AllowMask'   -and $IP -match $matchIP                                  ) -or
                ($PSCmdlet.ParameterSetName -eq 'AllowMask'   -and $IP -match $matchAll                                 ) -or
                ($PSCmdlet.ParameterSetName -eq 'RequireMask' -and $IP -match $matchAll                                 )
            )
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv4AddressInSameNet
{
    <#
        .SYNOPSIS
            Test if two IP addresses are in the same subnet

        .DESCRIPTION
            Test if two IP addresses are in the same subnet

        .PARAMETER IP
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.1 255.0.0.0"
            - IP + mask length, eg. "127.0.0.1/8"
            If input is IP without subnet mask (eg. "127.0.0.1") then -Mask parameter must be set
 
        .PARAMETER IP2
            Same format as -IP

        .PARAMETER Mask
            If input IP is in format without subnet mask, this parameter must be set to either
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .PARAMETER AllowMaskMismatch
            Return true if hosts with the two IP addresses can communicate with each other directly
            (not routed), even if there's a mismatch in subnet mask between the two.

        .EXAMPLE
            Test-IPv4AddressInSameNet -IP 10.30.50.60 -IP2 10.30.50.61/24
            True

        .EXAMPLE
            Test-IPv4AddressInSameNet -IP 10.30.50.60/24 -IP2 10.30.50.61/255.255.255.0
            True

        .EXAMPLE
            Test-IPv4AddressInSameNet -IP 10.30.50.60/24 -IP2 10.30.50.61/29
            False

        .EXAMPLE
            Test-IPv4AddressInSameNet -IP 10.30.50.60/24 -IP2 10.30.50.61/29 -AllowMaskMismatch
            True
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        [Parameter(Mandatory = $true, Position=1)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP2,

        [Parameter()]
        [ValidateScript({ (Test-IPv4Address -IP $_ -Mask -AllowLength) -or $(throw "$_ is not a valid IPv4 mask") })]
        [System.String]
        $Mask = '',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $AllowMaskMismatch
   )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('AllowMaskMismatch')
            $null = $PSBoundParameters.Remove('IP')
            $null = $PSBoundParameters.Remove('IP2')
            if (
                $Mask -or
                ((Test-IPv4Address -IP $IP -RequireMask) -and (Test-IPv4Address -IP $IP2 -RequireMask))
            )
            {
                $info1 = Get-IPv4Address -Info -IP $IP @PSBoundParameters
                $info2 = Get-IPv4Address -Info -IP $IP2 @PSBoundParameters
            }
            elseif (Test-IPv4Address -IP $IP -RequireMask)
            {
                $info1 = Get-IPv4Address -Info -IP $IP @PSBoundParameters
                $null = $PSBoundParameters.Remove('Mask')
                $info2 = Get-IPv4Address -Info -IP $IP2 -Mask $info1.MaskLength @PSBoundParameters
            }
            elseif (Test-IPv4Address -IP $IP2 -RequireMask)
            {
                $info2 = Get-IPv4Address -Info -IP $IP2 @PSBoundParameters
                $null = $PSBoundParameters.Remove('Mask')
                $info1 = Get-IPv4Address -Info -IP $IP -Mask $info2.MaskLength @PSBoundParameters
            }
            else
            {
                throw "No mask defined for either IP ($IP) or IP2 ($IP2), and -Mask parameter is not set"
            }

            if ($info1.IP -eq $info2.IP)
            {
                "-IP and -IP2 is the same ($IP)" | Write-Warning
            }

            # Return
            ($info1.Subnet -eq $info2.Subnet -and $info1.MaskLength -eq $info2.MaskLength) -or
            (
                $AllowMaskMismatch -and
                $info1.Integer.IP -ge $info2.Integer.FirstIP -and
                $info1.Integer.IP -le $info2.Integer.LastIP -and
                $info2.Integer.IP -ge $info1.Integer.FirstIP -and
                $info2.Integer.IP -le $info1.Integer.LastIP
            )
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv4AddressInSubnet
{
    <#
        .SYNOPSIS
            Test if IP address is in a subnet

        .DESCRIPTION
            Test if IP address is in a subnet

        .PARAMETER Subnet
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.0 255.0.0.0"
            - IP + mask length, eg. "127.0.0.0/8"
            If input is IP without subnet mask (eg. "127.0.0.0") then -Mask parameter must be set
 
        .PARAMETER IP
            Same format as -Subnet

        .PARAMETER Mask
            If subnet is in format without subnet mask, this parameter must be set to either
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .PARAMETER AllowMaskMismatch
            Return true if IP is in subnet, even if the subnet mask is wrong.

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.50.70
            True

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.50.70/24
            True

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.50.70/29
            False

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.50.70/29 -AllowMaskMismatch
            True

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.50.70/23 -AllowMaskMismatch
            True

        .EXAMPLE
            Test-IPv4AddressInSubnet -Subnet 10.30.50.0/24 -IP 10.30.51.70/23 -AllowMaskMismatch
            False
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $Subnet,

        [Parameter(Mandatory = $true, Position=1)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        [Parameter()]
        [ValidateScript({ (Test-IPv4Address -IP $_ -Mask -AllowLength) -or $(throw "$_ is not a valid IPv4 mask") })]
        [System.String]
        $Mask = '',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $AllowMaskMismatch
   )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('AllowMaskMismatch')
            $null = $PSBoundParameters.Remove('Subnet')
            $null = $PSBoundParameters.Remove('IP')

            if (-not (Test-IPv4Subnet -Subnet $Subnet @PSBoundParameters))
            {
                throw "$Subnet (mask $Mask) is not a subnet"
            }
            elseif (
                $Mask -or
                ((Test-IPv4Address -IP $Subnet -RequireMask) -and (Test-IPv4Address -IP $IP -RequireMask))
            )
            {
                $info1 = Get-IPAddress -Info -IP $Subnet @PSBoundParameters
                $info2 = Get-IPAddress -Info -IP $IP @PSBoundParameters
            }
            elseif (Test-IPv4Address -IP $Subnet -RequireMask)
            {
                $info1 = Get-IPAddress -Info -IP $Subnet @PSBoundParameters
                $null = $PSBoundParameters.Remove('Mask')
                $info2 = Get-IPAddress -Info -IP $IP -Mask $info1.MaskLength @PSBoundParameters
            }
            else
            {
                throw "No mask defined for Subnet ($Subnet), and -Mask parameter is not set"
            }

            if ($info1.IP -eq $info2.IP)
            {
                "-Subnet and -IP is the same ($IP)" | Write-Warning
            }

            # Return
            ($info1.Subnet -eq $info2.Subnet -and $info1.MaskLength -eq $info2.MaskLength) -or
            (
                $AllowMaskMismatch -and
                $info2.Integer.IP -ge $info1.Integer.FirstIP -and
                $info2.Integer.IP -le $info1.Integer.LastIP
            )
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv4AddressIsPrivate
{
    <#
        .SYNOPSIS
            Test if IP address is in a private segment

        .DESCRIPTION
            Test if IP address is in a private segment

        .PARAMETER IP
            Input IP in quad dot format with or without subnet mask

        .PARAMETER xxx
            xxx

        .EXAMPLE
            xxx
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param
    (
        [Parameter(Mandatory = $true, Position=1)]
        [ValidateScript({ (Test-IPv4Address -IP $_ -AllowMask) -or $(throw "$_ is not a valid IPv4 address") })]
        [System.String]
        $IP,

        [Parameter(Mandatory = $true, ParameterSetName = 'Rfc1918')]
        [System.Management.Automation.SwitchParameter]
        $Rfc1918,

        [Parameter(Mandatory = $true, ParameterSetName = 'Rfc6598')]
        [System.Management.Automation.SwitchParameter]
        $Rfc6598
   )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $subnets = @()
            if ($PSCmdlet.ParameterSetName -in 'Default','Rfc1918')
            {
                $subnets += '10.0.0.0/8','172.16.0.0/12','192.168.0.0/16'
            }

            if ($PSCmdlet.ParameterSetName -in 'Default','Rfc6598')
            {
                $subnets += '100.64.0.0/10'
            }

            $return = $false

            foreach ($subnet in $subnets)
            {
                if (Test-IPv4AddressInSubnet -Subnet $subnet -IP $IP -AllowMaskMismatch -WarningAction SilentlyContinue)
                {
                    $return = $true
                    break
                }
            }

            # Return
            $return
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv4Subnet
{
    <#
        .SYNOPSIS
            Test if IP address is a valid subnet address

        .DESCRIPTION
            Test if IP address is a valid subnet address

        .PARAMETER Subnet
            Input IP in quad dot format with subnet mask, either:
            - IP + mask in quad dot, eg. "127.0.0.0 255.0.0.0"
            - IP + mask length, eg. "127.0.0.0/8"
            If input is IP without subnet mask (eg. "127.0.0.0") then -Mask parameter must be set

        .PARAMETER Mask
            If input IP is in format without subnet mask, this parameter must be set to either
            - Quad dot format, eg. "255.255.255.0"
            - Mask length (0-32), eg. "24"
            - Mask length with slash, eg. "/24"

        .EXAMPLE
            Test-IPv4Subnet -Subnet 10.20.30.0/24
            True

        .EXAMPLE
            Test-IPv4Subnet -Subnet 10.20.30.0/255.255.0.0
            False
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding(DefaultParameterSetName = 'IPOnly')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $Subnet,

        [Parameter()]
        [System.String]
        $Mask = ''
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('Subnet')
            $info = Get-IPv4Address -Info -IP $Subnet @PSBoundParameters

            # Return
            $info.IP -eq $info.Subnet
        }
        catch
        {
            # Never throw, just return false
            $false
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv6Address
{
    <#
        .SYNOPSIS
            Test if a string contains a valid IP address (IPv6)

        .DESCRIPTION
            Test if a string contains a valid IP address (IPv6)
            Returns [bool]

        .PARAMETER IP
            IP address to test is valid or not

        .PARAMETER IPOnly
            Only return True if input is valid IPv6 address (without prefix)
            Eg. "a:b::c"
            This is default

        .PARAMETER AllowPrefix
            Return true if input valid IPv6 address with or without prefix
            Eg. "a:b::c" or "a:b::c/64"

        .PARAMETER RequirePrefix
            Return true if input valid IPv6 address with prefix
            Eg. "a:b::c/64"

        .EXAMPLE
            Test-IPv6Address -IP a:b::c
            True
 
        .EXAMPLE
            Test-IPv6Address -IP a:b::c/64
            False
 
        .EXAMPLE
            Test-IPv6Address -IP a:b::x
            False
 
        .EXAMPLE
            Test-IPv6Address -IP a:b::c/64
            False
 
        .EXAMPLE
            Test-IPv6Address -AllowPrefix -IP a:b::c/64
            True
 
        .EXAMPLE
            Test-IPv6Address -AllowPrefix -IP a:b::c
            True
 
        .EXAMPLE
            Test-IPv6Address -AllowPrefix -IP a:b::x/64
            False

        .EXAMPLE
            Test-IPv6Address -RequirePrefix -IP a:b::c/64
            True
 
        .EXAMPLE
            Test-IPv6Address -RequirePrefix -IP a:b::c
            False
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding(DefaultParameterSetName = 'IPOnly')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $IP,

        [Parameter(ParameterSetName = 'IPOnly')]
        [System.Management.Automation.SwitchParameter]
        $IPOnly,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllowPrefix')]
        [System.Management.Automation.SwitchParameter]
        $AllowPrefix,

        [Parameter(Mandatory = $true, ParameterSetName = 'RequirePrefix')]
        [System.Management.Automation.SwitchParameter]
        $RequirePrefix
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            # RegEx for IPv6 is just %&(%&�&(=)%&/%&�
            # We just try to parse it instead - and don't do RegEx match as we do with IPv4!
            # And yes - there's probably lot's of weird IPv6 syntaxes that doesn't work with this!

            $ipObject = Convert-IPv6Address -IP $IP -Info

            # Return
            ($PSCmdlet.ParameterSetName -eq 'IPOnly'        -and $ipObject.Prefix -eq $null) -or
            ($PSCmdlet.ParameterSetName -eq 'RequirePrefix' -and $ipObject.Prefix -ne $null) -or
            ($PSCmdlet.ParameterSetName -eq 'AllowPrefix')
        }
        catch
        {
            # Never throw, just return false
            $false
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv6AddressInSameNet
{
    <#
        .SYNOPSIS
            Test if two IP addresses are in the same subnet (IPv6)

        .DESCRIPTION
            Test if two IP addresses are in the same subnet (IPv6)

        .PARAMETER IP
            Input IP is standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")
 
        .PARAMETER IP2
            Same format as -IP

        .PARAMETER Prefix
            If prefix is not set in IP address, it must be set with this parameter

        .PARAMETER AllowPrefixMismatch
            Return true if hosts with the two IP addresses can communicate with each other directly
            (not routed), even if there's a mismatch in prefix between the two.

        .EXAMPLE
            Test-IPv6AddressInSameNet a:2::/31 a:3::/31
            True

        .EXAMPLE
            Test-IPv6AddressInSameNet a:2::/32 a:3::/32
            False

        .EXAMPLE
            Test-IPv6AddressInSameNet a:2::/31 a:3::/30
            False

        .EXAMPLE
            Test-IPv6AddressInSameNet a:2::/31 a:3::/32 -AllowPrefixMismatch
            False

        .EXAMPLE
            Test-IPv6AddressInSameNet a:2::/31 a:3::/30 -AllowPrefixMismatch
            True
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv6Address -IP $_ -AllowPrefix) -or $(throw "$_ is not a valid IPv6 address") })]
        [System.String]
        $IP,

        [Parameter(Mandatory = $true, Position=1)]
        [ValidateScript({ (Test-IPv6Address -IP $_ -AllowPrefix) -or $(throw "$_ is not a valid IPv6 address") })]
        [System.String]
        $IP2,

        [Parameter()]
        [Nullable[System.Byte]]
        $Prefix,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $AllowPrefixMismatch
   )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('AllowPrefixMismatch')
            $null = $PSBoundParameters.Remove('IP')
            $null = $PSBoundParameters.Remove('IP2')

            if (
                $Prefix -or
                ((Test-IPv6Address -IP $IP -RequirePrefix) -and (Test-IPv6Address -IP $IP2 -RequirePrefix))
            )
            {
                $info1 = Get-IPv6Address -Info -IP $IP @PSBoundParameters
                $info2 = Get-IPv6Address -Info -IP $IP2 @PSBoundParameters
            }
            elseif (Test-IPv6Address -IP $IP -RequirePrefix)
            {
                $info1 = Get-IPv6Address -Info -IP $IP @PSBoundParameters
                $null = $PSBoundParameters.Remove('Prefix')
                $info2 = Get-IPv6Address -Info -IP $IP2 -Prefix $info1.Prefix @PSBoundParameters
            }
            elseif (Test-IPv6Address -IP $IP2 -RequirePrefix)
            {
                $info2 = Get-IPv6Address -Info -IP $IP2 @PSBoundParameters
                $null = $PSBoundParameters.Remove('Prefix')
                $info1 = Get-IPv6Address -Info -IP $IP -Prefix $info2.Prefix @PSBoundParameters
            }
            else
            {
                throw "No prefix defined for either IP ($IP) or IP2 ($IP2), and -Prefix parameter is not set"
            }

            if ($info1.IP -eq $info2.IP)
            {
                "-IP and -IP2 is the same ($IP)" | Write-Warning
            }

            # Return
            ($info1.Subnet -eq $info2.Subnet -and $info1.Prefix -eq $info2.Prefix) -or
            (
                $AllowPrefixMismatch -and
                $info1.Objects.IP.IPBinary -ge $info2.Objects.FirstIP.IPBinary -and
                $info1.Objects.IP.IPBinary -le $info2.Objects.LastIP.IPBinary -and
                $info2.Objects.IP.IPBinary -ge $info1.Objects.FirstIP.IPBinary -and
                $info2.Objects.IP.IPBinary -le $info1.Objects.LastIP.IPBinary
            )
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv6AddressInSubnet
{
    <#
        .SYNOPSIS
            Test if two IP addresses are in the same subnet (IPv6)

        .DESCRIPTION
            Test if two IP addresses are in the same subnet (IPv6)

        .PARAMETER Subnet
            Input IP is standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")
 
        .PARAMETER IP
            Same format as -Subnet

        .PARAMETER Prefix
            If prefix is not set in subnet address, it must be set with this parameter

        .PARAMETER AllowPrefixMismatch
            Return true if hosts with the two IP addresses can communicate with each other directly
            (not routed), even if there's a mismatch in prefix between the two.

        .EXAMPLE
            Test-IPv6AddressInSubnet -Subnet a:2::/31 -IP a:3::/31
            True

        .EXAMPLE
            Test-IPv6AddressInSubnet -Subnet a:2::/32 -IP a:3::/32
            False

        .EXAMPLE
            Test-IPv6AddressInSubnet -Subnet a:2::/31 -IP a:3::/30
            False

        .EXAMPLE
            Test-IPv6AddressInSubnet -Subnet a:2::/32 -IP a:3::/31 -AllowPrefixMismatch
            False

        .EXAMPLE
            Test-IPv6AddressInSubnet -Subnet a:2::/31 -IP a:3::/32 -AllowPrefixMismatch
            True
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [ValidateScript({ (Test-IPv6Address -IP $_ -AllowPrefix) -or $(throw "$_ is not a valid IPv6 address") })]
        [System.String]
        $Subnet,

        [Parameter(Mandatory = $true, Position=1)]
        [ValidateScript({ (Test-IPv6Address -IP $_ -AllowPrefix) -or $(throw "$_ is not a valid IPv6 address") })]
        [System.String]
        $IP,

        [Parameter()]
        [Nullable[System.Byte]]
        $Prefix,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $AllowPrefixMismatch
   )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('AllowPrefixMismatch')
            $null = $PSBoundParameters.Remove('Subnet')
            $null = $PSBoundParameters.Remove('IP')

            if (-not (Test-IPv6Subnet -Subnet $Subnet @PSBoundParameters))
            {
                throw "$Subnet (prefix $Prefix) is not a subnet"
            }
            elseif (
                $Prefix -or
                ((Test-IPv6Address -IP $Subnet -RequirePrefix) -and (Test-IPv6Address -IP $IP -RequirePrefix))
            )
            {
                $info1 = Get-IPv6Address -Info -IP $Subnet @PSBoundParameters
                $info2 = Get-IPv6Address -Info -IP $IP @PSBoundParameters
            }
            elseif (Test-IPv6Address -IP $Subnet -RequirePrefix)
            {
                $info1 = Get-IPv6Address -Info -IP $Subnet @PSBoundParameters
                $null = $PSBoundParameters.Remove('Prefix')
                $info2 = Get-IPv6Address -Info -IP $IP -Prefix $info1.Prefix @PSBoundParameters
            }
            else
            {
                throw "No prefix defined for Subnet ($Subnet), and -Prefix parameter is not set"
            }

            if ($info1.IP -eq $info2.IP)
            {
                "-Subnet and -IP is the same ($IP)" | Write-Warning
            }

            # Return
            ($info1.Subnet -eq $info2.Subnet -and $info1.Prefix -eq $info2.Prefix) -or
            (
                $AllowPrefixMismatch -and
                $info2.Objects.IP.IPBinary -ge $info1.Objects.FirstIP.IPBinary -and
                $info2.Objects.IP.IPBinary -le $info1.Objects.LastIP.IPBinary
            )
        }
        catch
        {
            # If error was encountered inside this function then stop doing more
            # But still respect the ErrorAction that comes when calling this function
            # And also return the line number where the original error occured
            $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString()
            Write-Verbose -Message "Encountered an error: $msg"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Test-IPv6Subnet
{
    <#
        .SYNOPSIS
            Test if IP address is a valid subnet address (IPv6)

        .DESCRIPTION
            Test if IP address is a valid subnet address (IPv6)

        .PARAMETER Subnet
            Input IP is standard IPv6 format with out prefix (eg. "a:b:00c::" or "a:b:00c::0/64")

        .PARAMETER Prefix
            If prefix is not set in subnet address, it must be set with this parameter

        .EXAMPLE
            Test-IPv6Subnet -Subnet a:0:0:b::/64
            True

        .EXAMPLE
            Test-IPv6Subnet -Subnet a:0:0:0:b::/64
            False
    #>


    [OutputType([System.Boolean])]
    [CmdletBinding(DefaultParameterSetName = 'IPOnly')]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)]
        [System.String]
        $Subnet,

        [Parameter()]
        [System.String]
        $Prefix = ''
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $null = $PSBoundParameters.Remove('Subnet')
            $info = Get-IPv6Address -Info -IP $Subnet @PSBoundParameters

            # Return
            $info.IP -eq $info.Subnet
        }
        catch
        {
            # Never throw, just return false
            $false
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

Set-Alias -Name Convert-IP -Value Convert-IPv4Address

Set-Alias -Name Convert-IPAddress -Value Convert-IPv4Address

Set-Alias -Name Convert-IPMask -Value Convert-IPv4Mask

Set-Alias -Name Get-IPAddress -Value Get-IPv4Address

Set-Alias -Name Get-Mask -Value Get-IPv4Mask

Set-Alias -Name Get-Subnet -Value Get-IPv4Subnet

Set-Alias -Name Test-IPAddress -Value Test-IPv4Address

Export-ModuleMember -Function Get-IPv4Mask
Export-ModuleMember -Function Get-IPv4Subnet
Export-ModuleMember -Function Test-IPv4Address
Export-ModuleMember -Function Test-IPv6Subnet
Export-ModuleMember -Function Convert-IPv4Mask
Export-ModuleMember -Function Test-IPv6Address
Export-ModuleMember -Function Convert-IPv4Address
Export-ModuleMember -Function Test-IPv6AddressInSameNet
Export-ModuleMember -Function Test-IPv4Subnet
Export-ModuleMember -Function Test-IPv6AddressInSubnet
Export-ModuleMember -Function Convert-IPv6Address
Export-ModuleMember -Function Get-IPv6Address
Export-ModuleMember -Function Test-IPv4AddressInSubnet
Export-ModuleMember -Function Test-IPv4AddressIsPrivate
Export-ModuleMember -Function Test-IPv4AddressInSameNet
Export-ModuleMember -Function Get-IPv6Subnet
Export-ModuleMember -Function Get-IPv4Address
Export-ModuleMember -Alias    Convert-IPMask
Export-ModuleMember -Alias    Get-IPAddress
Export-ModuleMember -Alias    Get-Mask
Export-ModuleMember -Alias    Convert-IP
Export-ModuleMember -Alias    Convert-IPAddress
Export-ModuleMember -Alias    Get-Subnet
Export-ModuleMember -Alias    Test-IPAddress