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 } |