DHCPv4.class.ps1

Write-Devel "Class PSScriptRoot = $PSScriptRoot"
# https://www.ietf.org/rfc/rfc2131.txt
# DHCP Packet Format (RFC 2131 - http://www.ietf.org/rfc/rfc2131.txt):
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | op (1) | htype (1) | hlen (1) | hops (1) |
# +---------------+---------------+---------------+---------------+
# | xid (4) |
# +-------------------------------+-------------------------------+
# | secs (2) | flags (2) |
# +-------------------------------+-------------------------------+
# | ciaddr (4) |
# +---------------------------------------------------------------+
# | yiaddr (4) |
# +---------------------------------------------------------------+
# | siaddr (4) |
# +---------------------------------------------------------------+
# | giaddr (4) |
# +---------------------------------------------------------------+
# | |
# | chaddr (16) |
# | |
# | |
# +---------------------------------------------------------------+
# | |
# | sname (64) |
# +---------------------------------------------------------------+
# | |
# | file (128) |
# +---------------------------------------------------------------+
# | |
# | options (variable) |
# +---------------------------------------------------------------+

# FIELD OCTETS DESCRIPTION
# ----- ------ -----------
# op 1 Message op code / message type. 1 = BOOTREQUEST, 2 = BOOTREPLY
# htype 1 Hardware address type, see ARP section in "Assigned Numbers" RFC; e.g., '1' = 10mb ethernet.
# hlen 1 Hardware address length (e.g. '6' for 10mb ethernet).
# hops 1 Client sets to zero, optionally used by relay agents when booting via a relay agent.
# xid 4 Transaction ID, a random number chosen by the client, used by the client and server to associate messages and responses between a client and a server.
# secs 2 Filled in by client, seconds elapsed since client began address acquisition or renewal process.
# flags 2 Flags (see figure 2).
# ciaddr 4 Client IP address; only filled in if client is in BOUND, RENEW or REBINDING state and can respond to ARP requests.
# yiaddr 4 'your' (client) IP address.
# siaddr 4 IP address of next server to use in bootstrap; returned in DHCPOFFER, DHCPACK by server.
# giaddr 4 Relay agent IP address, used in booting via a relay agent.
# chaddr 16 Client hardware address.
# sname 64 Optional server host name, null terminated string.
# file 128 Boot file name, null terminated string; "generic" name or null in DHCPDISCOVER, fully qualified directory-path name in DHCPOFFER.
# options var Optional parameters field. See the options documents for a list of defined options.

# . $PSScriptRoot/DHCP.enum.ps1

class DHCPv4Packet {

    <#
 
       ### ######## ######## ######## #### ######## ## ## ######## ######## ######
      ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
     ## ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ######## ## ######## ## ## ## ###### ######
    ######### ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## #### ######## ####### ## ######## ######
 
    #>



    # Message op code / message type. 1 = BOOTREQUEST, 2 = BOOTREPLY, etc...
    # @see DHCP.enum.ps1
    [ValidateRange(1,8)]
    [UInt16]$op
    # Hardware address type, see ARP section in "Assigned Numbers" RFC; e.g., '1' = 10mb ethernet.
    [UInt16]$htype
    # Hardware address length (e.g. '6' for 10mb ethernet).
    [UInt16]$hlen
    # Client sets to zero, optionally used by relay agents when booting via a relay agent.
    [UInt16]$hops
    # Transaction ID, a random number chosen by the client, used by the client and server to associate messages and responses between a client and a server.
    [UInt16[]]$xid
    # Filled in by client, seconds elapsed since client began address acquisition or renewal process.
    [UInt16]$secs
    # Flags (see figure 2).
    [UInt16]$flags
    # Client IP address; only filled in if client is in BOUND, RENEW or REBINDING state and can respond to ARP requests.
    [ipAddress]$ciaddr
    # 'your' (client) IP address.
    [ipAddress]$yiaddr
    # IP address of next server to use in bootstrap; returned in DHCPOFFER, DHCPACK by server.
    [ipAddress]$siaddr
    # Relay agent IP address, used in booting via a relay agent.
    [ipAddress]$giaddr
    # Client hardware address.
    [string]$chaddr
    # Optional server host name, null terminated string.
    [string]$sname
    # Boot file name, null terminated string; "generic" name or null in DHCPDISCOVER, fully qualified directory-path name in DHCPOFFER.
    [string]$file
    # magic cookie
    [UInt16[]]$magicCookie
    # Optional parameters field. See the options documents for a list of defined options.
    [array]$options

    [array]$requestList

    <#
 
     ###### ####### ## ## ###### ######## ######## ## ## ###### ######## ####### ######## ######
    ## ## ## ## ### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## #### ## ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## ###### ## ######## ## ## ## ## ## ## ######## ######
    ## ## ## ## #### ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
     ###### ####### ## ## ###### ## ## ## ####### ###### ## ####### ## ## ######
 
    #>


    hidden constructorPreHook () {
        # $this.xid = @([DHCPv4Packet]::GenerateXID())
        $this.options = @()
        $this.requestList = @()
    }

    hidden constructorPostHook () {
        if ([string]::IsNullOrEmpty($this.xid)) { $this.xid = [DHCPv4Packet]::GenerateXID() }
    }

    DHCPv4Packet () {
        $this.constructorPreHook()
        $this.constructorPostHook()
    }

    DHCPv4Packet ([hashtable]$hash) {
        $this.constructorPreHook()
        $this.Bind($hash)
        $this.constructorPostHook()
    }

    DHCPv4Packet ([Byte[]]$bytes) {
        $this.constructorPreHook()
        $this.Bind($bytes)
        $this.constructorPostHook()
    }

    <#
 
    ## ## ######## ######## ## ## ####### ######## ######
    ### ### ## ## ## ## ## ## ## ## ## ##
    #### #### ## ## ## ## ## ## ## ## ##
    ## ### ## ###### ## ######### ## ## ## ## ######
    ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ######## ## ## ## ####### ######## ######
 
    #>


    ## Bind hash fields to object attributes
    ## Bind hidden members as well
    [void]Bind([hashtable]$hash) {
        $this | Get-Member -MemberType Property -Force | ForEach-Object {
            $property = $_
            $key = $property.name
            if ($hash.ContainsKey($key)) {
                $this.$key = $hash.$key
            }
        }
    }

    ## Bind packet from bytes
    [void]Bind([byte[]]$bytes) {
        $this.op = [UInt16]$bytes[0]
        $this.htype = [UInt16]$bytes[1]
        $this.hlen = [UInt16]$bytes[2]
        $this.hops = [UInt16]$bytes[3]
        $this.xid = @([Uint16]$bytes[4], [Uint16]$bytes[5], [Uint16]$bytes[6], [Uint16]$bytes[7])
        $this.secs = [UInt16]("0x" + (-join $bytes[8..9].ForEach('ToString', 'X2')))
        $this.flags = [UInt16]("0x" + (-join $bytes[10..11].ForEach('ToString', 'X2')))
        $this.ciaddr = (-join $bytes[12..15].ForEach('ToString', 'X2') | ConvertFrom-HexIP)
        $this.yiaddr = (-join $bytes[16..19].ForEach('ToString', 'X2') | ConvertFrom-HexIP)
        $this.siaddr = (-join $bytes[20..23].ForEach('ToString', 'X2') | ConvertFrom-HexIP)
        $this.giaddr = (-join $bytes[24..27].ForEach('ToString', 'X2') | ConvertFrom-HexIP)
        $this.chaddr = $bytes[28..33].ForEach('ToString', 'x2') -join ":"
        # skip the gap
        $this.sname = (-join $bytes[44..107].ForEach('ToString', 'X2')).TrimEnd('0')
        $this.file = (-join $bytes[108..235].ForEach('ToString', 'X2')).TrimEnd('0')
        $this.magicCookie = @([Uint16]$bytes[236], [Uint16]$bytes[237], [Uint16]$bytes[238], [Uint16]$bytes[239])
        if ((-join $this.magicCookie) -ne (-join $Global:DHCP_MAGIC_COOKIE)) {
            Write-Warning "Malformed Magic Cookie. Got $($this.magicCookie) / expected $($Global:DHCP_MAGIC_COOKIE)"
        } else {
            # if we got Magic Cookie, then the options follows
            # this syntax goes backward from 240 back to 0
            # $this.options = [DHCPv4Packet]::ParseOptions($bytes[240..-1])
            # this syntax is ok from 240 till end of array
            $this.options = [DHCPv4Packet]::ParseOptions($bytes[240..($bytes.length - 1)])
        }
    }

    ## Add an option
    [void]AddOption([uint16]$code, $value) {
        $this.options += ,(@($code, $value))
    }

    ## Add an option to the request list
    [void]AddRequestList([uint16]$code) {
        $this.requestList += $code
    }

    ## Export object as a hashtable
    ## Do not export hidden members
    [hashtable]Export() {
        $hash = @{}
        $this | Get-Member -MemberType Property | ForEach-Object {
            $property = $_
            $key = $property.name
            if ($null -ne $this.$key) {
                $hash.$key = $this.$key
            }
        }
        return $hash
    }

    ## Export object to a file
    ## Do not export hidden members
    [void]ExportToYaml([string]$Filename) {
        $this.Export() | ConvertTo-Yaml | Out-File $Filename -Encoding utf8NoBOM -Confirm:$false
    }

    ## Export object to a file
    ## Do not export hidden members
    [void]ExportToJson([string]$Filename) {
        $this.Export() | ConvertTo-Json | Out-File $Filename -Encoding utf8NoBOM -Confirm:$false
    }

    ## convert packet to ascii codes
    [uint16[]]ToInt() {
        $array = @()
        $array += $this.op, $this.htype, $this.hlen, $this.hops
        $array += $this.xid
        # $array += $this.secs, $this.flags
        # secs if 2 bytes long but is an int, so we trick it by converting it to hex, split the result into 2 bytes, and back into an array of 2 int
        $array += (("{0:x4}" -f $this.secs) -replace '..', '0x$& ').trim() -split " " | ForEach-Object { [uint16]$_ }
        $array += (("{0:x4}" -f $this.flags) -replace '..', '0x$& ').trim() -split " " | ForEach-Object { [uint16]$_ }
        $array += $this.ciaddr.GetAddressBytes()
        $array += $this.yiaddr.GetAddressBytes()
        $array += $this.siaddr.GetAddressBytes()
        $array += $this.giaddr.GetAddressBytes()
        # macAddress. According to spec https://www.ietf.org/rfc/rfc2131.txt chaddr field is 16 bytes long
        # so fill the remaining bytes with PAD
        $array += $this.chaddr.Split("-").Split(":") | ForEach-Object { [convert]::ToByte($_,16) }
        for ($i = $this.chaddr.Split("-").Split(":").length; $i -lt 16; $i++) { $array += $Global:PAD }
        # sname. According to spec https://www.ietf.org/rfc/rfc2131.txt sname field is 64 bytes long
        # so fill the remaining bytes with PAD
        $array += $this.sname.ToCharArray() | ForEach-Object { [UInt16]$_ }
        for ($i = $this.sname.length; $i -lt 64; $i++) { $array += $Global:PAD }
        # file. According to spec https://www.ietf.org/rfc/rfc2131.txt file field is 128 bytes long
        # so fill the remaining bytes with PAD
        $array += $this.file.ToCharArray() | ForEach-Object { [UInt16]$_ }
        for ($i = $this.file.length; $i -lt 128; $i++) { $array += $Global:PAD }
        # options
        if ($null -ne $this.options) {
            $array += $Global:DHCP_MAGIC_COOKIE
            foreach ($opt in $this.options) {
                if ([string]::IsNullOrEmpty($opt)) { continue }
                $length = 0
                $value = @()
                # option code
                $array += $opt[0]
                # write-devel "code = $opt[0]"
                # write-devel "label = $($Global:aDHCPv4Options[$opt[0]].label)"
                # option value
                switch ($opt[1].GetType().Name) {
                    'ipAddress' {
                        $value = $opt[1].GetAddressBytes()
                    }
                    'String' {
                        $value = $opt[1].ToCharArray() | ForEach-Object { [UInt16]$_ }
                    }
                    default {
                        $value = $opt[1]
                    }
                }
                # Write-Devel "value = $value"
                # option length
                if ($Global:aDHCPv4Options[$opt[0]].length -gt 0) {
                    $length = $Global:aDHCPv4Options[$opt[0]].length
                } else {
                    $length = $value.length
                }
                # Write-Devel "length = $length"
                $array += $length
                $array += $value
                # for ($i = $value.length; $i -lt $length; $i++) { write-devel "padding i=$i"; $array += $Global:PAD }
            }
            if ($null -ne $this.requestList) {
                $array += [DHCPv4OptionCode]::ParameterRequestList
                $array += $this.requestList.count
                $array += $this.requestList
            }
            $array += $Global:END
        }
        return $array
    }

    ## convert packet source to bytes ready to send over the network
    [byte[]]ToBytes() {
        # $string = "{0:x1}{0:x1}{0:x1}{0:x1}" -f $this.op, $this.htype, $this.hlen, $this.hops
        # $enc = [system.Text.Encoding]::UTF8
        # [byte[]]$bytes = $enc.GetBytes($string)
        [byte[]]$bytes = [byte[]] -split ($this.ToHexString() -replace '..', '0x$& ')
        return $bytes
    }

    # ToString() override
    [String]ToHexString() {
        [string]$string = ""
        foreach ($i in $this.ToInt()) {
            $b = $i.ToString("X")
            if (($b.length % 2) -eq 1) { $b = "0$b" }
            $string += $b
        }
        return $string
    }
    ## ToString() override
    # [String]ToHexString()
    # {
    # [string]$string = "{0:x2}{1:x2}{2:x2}{3:x2}" -f $this.op, $this.htype, $this.hlen, $this.hops
    # $string += "{0:x4}" -f $this.xid
    # $string += "{0:x4}{1:x4}" -f $this.secs, $this.flags
    # $string += "{0:x16}" -f ($this.ciaddr | ConvertTo-HexIP)
    # $string += "{0:x16}" -f ($this.yiaddr | ConvertTo-HexIP)
    # $string += "{0:x16}" -f ($this.siaddr | ConvertTo-HexIP)
    # $string += "{0:x16}" -f ($this.giaddr | ConvertTo-HexIP)
    # $string += ($this.chaddr -replace "[:-]").PadRight(32,"0")
    # # $string += ("{0:x}{1:x}" -f $this.sname, $Script:END).PadRight(128,"0")
    # # @TODO replace FF with $Script:END
    # # @TODO replace 0 with $Script:PAD
    # $string += (($this.sname.EnumerateRunes() | ForEach-Object { "{0:x2}" -f $_.Value }) + "FF").PadRight(128,"0")
    # # $string += ("{0:x}{1:x}" -f $this.file, $Script:END).PadRight(256,"0")
    # # @TODO replace FF with $Script:END
    # # @TODO replace 00 with $Script:PAD
    # $string += (($this.file.EnumerateRunes() | ForEach-Object { "{0:x2}" -f $_.Value }) + "FF").PadRight(256,"0")
    # if ($null -ne $this.options) {
    # $string += -join ($Script:DHCP_MAGIC_COOKIE | ForEach-Object { "{0:x2}" -f $_ })
    # foreach ($opt in $this.options) {
    # $string += "{0:x2}" -f $opt[0]
    # $value = ""
    # switch ($opt[1].GetType().Name) {
    # 'String' {
    # $value += $opt[1].EnumerateRunes() | ForEach-Object { "{0:x2}" -f $_.Value }
    # }
    # default {
    # $value = "{0:x2}" -f $opt[1]
    # }
    # }
    # if ($Script:aDHCPv4Options[$opt].length -gt 0) {
    # $length = $Script:aDHCPv4Options[$opt].length
    # } else {
    # $length = $value.length
    # # round to next even length
    # if (($length % 2) -gt 0) { $length++ }
    # }
    # $string += "{0:x2}" -f $length
    # $string += $value.PadLeft($length, "0")
    # }
    # }
    # return $string.ToUpper()
    # }

    ## Send packet over the network, port 67
    [void]Send() {
        # $client = new-object net.sockets.udpclient(67)
        # $send = $this.ToBytes()
        # $bytesSent = $client.send($send, $send.length, "255.255.255.255", 67)
        # $client.close()
        # Write-Debug "$bytesSent bytes sent."
        $this.Send([net.ipAddress]::Broadcast)
    }

    ## Send packet over the network, port 67
    # [void]Send([ipAddress]$ip) {
    # $client = new-object net.sockets.udpclient(67)
    # $send = $this.ToBytes()
    # $bytesSent = $client.send($send, $send.length, $ip, 67)
    # $client.close()
    # Write-Debug "$bytesSent bytes sent."
    # }

    # Send packet over the network, port 67
    # and receive response on port 68
    [byte[]]Send([ipAddress]$ip) {
        $client = new-object net.sockets.udpclient(67)
        $client.Client.ReceiveTimeout = 100000
        $send = $this.ToBytes()
        Write-Devel "$(($this.xid | % { $_.ToString("x") }) -join '') : $($this.chaddr)"
        $bytesSent = $client.send($send, $send.length, $ip, 67)
        Write-Debug "$bytesSent bytes sent."
        [byte[]]$bytesReceived = @()
        $i = 0
        while ($true) {
            $ipEP = new-object net.ipEndPoint([net.ipAddress]::any, 68)
            [byte[]]$bytesReceived = $client.receive([ref]$ipEP)
            Write-Debug "$($bytesReceived.count) bytes received."
            [DHCPv4Packet]$response = [DHCPv4Packet]::New($bytesReceived)
            Write-Devel "${i}: $(($response.xid | % { $_.ToString("x") }) -join '') : $($response.chaddr)"
            $i++
            if ($i -gt 10) { break }
        }

        $client.close()
        # $a = new-object system.text.asciiencoding
        return $bytesReceived
    }

    # Sniff DHCP packets from the network, port 68
    [void]Sniff() {
        $this.Sniff(10)
    }

    # Sniff DHCP packets from the network, port 68
    [void]Sniff([int]$MaxRetries) {
        $client = new-object net.sockets.udpclient(67)
        $client.Client.ReceiveTimeout = 100000
        [byte[]]$bytesReceived = @()
        $i = 0
        try {
            while ($true) {
                $ipEP = new-object net.ipEndPoint([net.ipAddress]::any, 68)
                [byte[]]$bytesReceived = $client.receive([ref]$ipEP)
                # Write-Debug "$($bytesReceived.count) bytes received."
                [DHCPv4Packet]$response = [DHCPv4Packet]::New($bytesReceived)
                # Write-Devel "${i}: $(($response.xid | % { $_.ToString("x") }) -join '') : $($response.chaddr)"
                if ($Global:DEVEL) { $response | format-table | Out-Host }
                $i++
                if ($i -gt $MaxRetries) { break }
            }
        } catch {

        } finally {
            $client.close()
        }
    }

    <#
 
     ###### ######## ### ######## #### ###### ## ## ######## ######## ## ## ####### ######## ######
    ## ## ## ## ## ## ## ## ## ### ### ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## ## #### #### ## ## ## ## ## ## ## ## ##
     ###### ## ## ## ## ## ## ## ### ## ###### ## ######### ## ## ## ## ######
          ## ## ######### ## ## ## ## ## ## ## ## ## ## ## ## ## ##
    ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
     ###### ## ## ## ## #### ###### ## ## ######## ## ## ## ####### ######## ######
 
    #>


    # static [string]GenerateXID() {
    # $x = -join ((48..57) + (65..70) | Get-Random -Count 8 | ForEach-Object {[char]$_})
    # return $x
    # }

    static [array]GenerateXID() {
        $x = (0..255) | Get-Random -Count 4
        return $x
    }

    static [array]ParseOptions([byte[]]$bytes) {
        $a = @()
        # $o = @()
        $i = 0
        [UInt16]$code = $length = $opt_end = 0
        $data = @()
        $value = $null
        while ($i -lt $bytes.count) {
            # $o = @()
            # edevel "i = $i"
            $code = $bytes[$i++]
            $length = $bytes[$i++]
            $opt_end = $i + $length
            $data = @()
            # edevel "i = $i / code = $code / length = $length / opt_end = $opt_end"
            while ($i -lt $opt_end) {
                $data += $bytes[$i++]
                # edevel "i = $i / data = $(-join $data)"
            }
            switch ($code) {
                [DHCPv4OptionCode]::SubnetMask {
                    $value = ([ipAddress]$data).IPAddressToString
                }
                default {
                    $value = $data
                }
            }
            $a += @($code, $value)
        }
        return $a
    }
}

Function Convert-ByteArrayToHex {

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true)]
        [Byte[]]
        $Bytes
    )

    $HexString = [System.Text.StringBuilder]::new($Bytes.Length * 2)

    ForEach($byte in $Bytes){
        $HexString.AppendFormat("{0:x2}", $byte) | Out-Null
    }

    $HexString.ToString()
}

Function Convert-HexToByteArray {

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true)]
        [String]
        $HexString
    )

    $Bytes = [byte[]]::new($HexString.Length / 2)

    For($i=0; $i -lt $HexString.Length; $i+=2){
        $Bytes[$i/2] = [convert]::ToByte($HexString.Substring($i, 2), 16)
    }

    $Bytes
}