functions/Invoke-DhcpTest.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Sends DHCP DISCOVER (and optionally REQUEST) packets to test DHCP servers, without dhcptest.exe. .DESCRIPTION Implements RFC 2131/2132 DHCP DISCOVER/OFFER and optional REQUEST/ACK exchanges over System.Net.Sockets.UdpClient. Supports spoofing the client MAC address (chaddr), injecting arbitrary DHCP options, requesting specific options back from the server (option 55), and targeting either a broadcast segment or a specific server/relay IP. By default only DISCOVER/OFFER is performed — this is read-only and never causes a server to commit a lease. The REQUEST/ACK phase (which can cause the server to reserve/commit the offered address) is opt-in via -SendRequest and is gated by ShouldProcess. This function never binds the resulting address to a local network adapter or changes OS routing/interface configuration — it only sends packets and returns a result object. .PARAMETER ServerAddress DHCP server or relay IP to send to. Default '255.255.255.255' (broadcast). .PARAMETER ServerPort UDP port on the DHCP server. Default: 67. .PARAMETER ClientPort Local UDP port to bind. Must be 68 to receive the server's reply (servers always reply to port 68). Default: 68. .PARAMETER ClientMac Spoofed client MAC address (chaddr), any of xx:xx:xx:xx:xx:xx, xx-xx-xx-xx-xx-xx, or Cisco xxxx.xxxx.xxxx format. If omitted, a random locally-administered MAC is generated. .PARAMETER Bind Local IP address to bind the UDP socket to, for multi-NIC/VLAN hosts. Default: any address. .PARAMETER Hostname Convenience for DHCP option 12 (Host Name). .PARAMETER VendorClassIdentifier Convenience for DHCP option 60 (Vendor Class Identifier). .PARAMETER ClientIdentifier Convenience for DHCP option 61 (Client Identifier), sent as an ASCII string value. .PARAMETER RequestOptions DHCP option codes (0-255) to request via option 55 (Parameter Request List). Default: 1,3,6,15,51,54,58,59 (subnet mask, router, DNS, domain name, lease time, server id, renewal/rebinding time). .PARAMETER Option Additional raw DHCP options to inject, as an array of hashtables with keys Code, Type, Value. Type must be one of Byte, UInt16, UInt32, String, IPAddress, HexString, ByteArray. Example: -Option @{Code=43;Type='HexString';Value='0102FF'}, @{Code=125;Type='IPAddress';Value='10.0.0.1'} .PARAMETER TransactionId Transaction ID (xid) to use. Default: a randomly generated value. .PARAMETER SendRequest After receiving an OFFER, send a REQUEST for the offered address and wait for ACK/NAK. This is the only phase that can cause a server to actually commit/reserve a lease, so it is gated by ShouldProcess (-WhatIf/-Confirm). .PARAMETER RequestedIPAddress Overrides the address requested in the REQUEST packet (option 50). Defaults to the address offered in the DISCOVER/OFFER phase. .PARAMETER TimeoutSeconds Per-attempt wait for a reply, in seconds. Default: 5. .PARAMETER Retries Number of send attempts before giving up on a phase. Default: 3. .PARAMETER LogActivity Also write DISCOVER/OFFER/REQUEST/ACK events via Write-Log. .EXAMPLE Invoke-DhcpTest -Verbose Status : OfferReceived OfferedAddress : 192.168.1.87 ServerIdentifier : 192.168.1.1 LeaseTimeSeconds : 86400 .EXAMPLE Invoke-DhcpTest -ClientMac '02:11:22:33:44:55' -Hostname 'dhcptest-probe' -RequestOptions 1,3,6,15,51 .EXAMPLE Invoke-DhcpTest -SendRequest -Confirm:$false -Verbose Completes the full DISCOVER/OFFER/REQUEST/ACK handshake — will consume a real lease from the server's pool if it ACKs. .EXAMPLE Invoke-DhcpTest -ServerAddress 10.0.0.5 -Bind 10.0.1.20 Unicasts the DISCOVER to a specific DHCP server/relay from a specific local interface. .NOTES The BOOTP broadcast flag (0x8000) is always set in outgoing packets. With ciaddr=0 and giaddr=0 and no address bound to a real interface, an RFC-compliant server always replies via broadcast — there is no scenario in this tool where a unicast reply back to us would work, since we don't own the offered address at the network layer. Binding local UDP port 68 usually conflicts with the Windows DHCP Client service, which holds that port on any DHCP-enabled adapter. This function sets SO_REUSEADDR so it can coexist with it; if binding still fails, run PowerShell elevated and/or temporarily stop the service: Stop-Service Dhcp -Force (Start-Service Dhcp afterward). #> function Invoke-DhcpTest { [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter()][string]$ServerAddress = '255.255.255.255', [Parameter()][ValidateRange(1, 65535)][int]$ServerPort = 67, [Parameter()][ValidateRange(1, 65535)][int]$ClientPort = 68, [Parameter()][Alias('MAC')][string]$ClientMac, [Parameter()][string]$Bind, [Parameter()][string]$Hostname, [Parameter()][string]$VendorClassIdentifier, [Parameter()][string]$ClientIdentifier, [Parameter()][Alias('ParameterRequestList')][int[]]$RequestOptions = @(1, 3, 6, 15, 51, 54, 58, 59), [Parameter()][object[]]$Option = @(), [Parameter()][uint32]$TransactionId, [Parameter()][switch]$SendRequest, [Parameter()][string]$RequestedIPAddress, [Parameter()][ValidateRange(1, 60)][int]$TimeoutSeconds = 5, [Parameter()][ValidateRange(1, 10)][int]$Retries = 3, [Parameter()][switch]$LogActivity ) # ── DHCP message types (RFC 2132 §9.6) ───────────────────────────────────── $MSG_DISCOVER = [byte]1 $MSG_OFFER = [byte]2 $MSG_REQUEST = [byte]3 $MSG_ACK = [byte]5 $MSG_NAK = [byte]6 $MsgTypeNames = @{ 1 = 'Discover'; 2 = 'Offer'; 3 = 'Request'; 4 = 'Decline'; 5 = 'Ack'; 6 = 'Nak'; 7 = 'Release'; 8 = 'Inform' } $MagicCookie = [byte[]]@(99, 130, 83, 99) # ── Big-endian helpers ─────────────────────────────────────────────────── function WriteU16BE([uint16]$v) { [byte[]]@([byte]($v -shr 8), [byte]($v -band 0xFF)) } function WriteU32BE([uint32]$v) { [byte[]]@([byte](($v -shr 24) -band 0xFF), [byte](($v -shr 16) -band 0xFF), [byte](($v -shr 8) -band 0xFF), [byte]($v -band 0xFF)) } function ReadU16BE([byte[]]$b, [int]$i) { (([int]$b[$i]) -shl 8) -bor [int]$b[$i + 1] } function ReadU32BE([byte[]]$b, [int]$i) { ([uint32]$b[$i] -shl 24) -bor ([uint32]$b[$i + 1] -shl 16) -bor ([uint32]$b[$i + 2] -shl 8) -bor [uint32]$b[$i + 3] } # ── MAC helpers ─────────────────────────────────────────────────────────── function ConvertTo-MacBytes([string]$mac) { $cisco = $mac -imatch '^([0-9a-f]{4}\.){2}[0-9a-f]{4}$' $colonDash = $mac -imatch '^([0-9a-f]{2}(:|-)){5}[0-9a-f]{2}$' if (-not $cisco -and -not $colonDash) { throw "Invalid MAC address format: '$mac'. Expected xx:xx:xx:xx:xx:xx, xx-xx-xx-xx-xx-xx, or xxxx.xxxx.xxxx." } $hex = ($mac -replace '[:\-.]', '') $bytes = [byte[]]::new(6) for ($i = 0; $i -lt 6; $i++) { $bytes[$i] = [Convert]::ToByte($hex.Substring($i * 2, 2), 16) } return $bytes } function Format-MacBytes([byte[]]$b) { (($b | ForEach-Object { $_.ToString('x2') }) -join ':') } function New-RandomMacBytes() { $b = [byte[]]::new(6) (New-Object System.Random).NextBytes($b) $b[0] = [byte](($b[0] -band 0xFE) -bor 0x02) # locally-administered, unicast return $b } # ── DHCP option encode/decode ──────────────────────────────────────────── function ConvertTo-DhcpOptionBytes([int]$Code, [string]$Type, $Value) { switch ($Type) { 'Byte' { return [byte[]]@([byte]$Value) } 'UInt16' { return WriteU16BE ([uint16]$Value) } 'UInt32' { return WriteU32BE ([uint32]$Value) } 'String' { return [System.Text.Encoding]::ASCII.GetBytes([string]$Value) } 'IPAddress' { $ip = [System.Net.IPAddress]::Parse([string]$Value) return $ip.GetAddressBytes() } 'HexString' { $hex = ([string]$Value) -replace '[^0-9a-fA-F]', '' if ($hex.Length % 2 -ne 0) { throw "HexString value for option $Code has an odd number of hex digits." } $out = [byte[]]::new($hex.Length / 2) for ($i = 0; $i -lt $out.Length; $i++) { $out[$i] = [Convert]::ToByte($hex.Substring($i * 2, 2), 16) } return $out } 'ByteArray' { return [byte[]]$Value } default { throw "Unknown option Type '$Type' for option $Code. Valid: Byte, UInt16, UInt32, String, IPAddress, HexString, ByteArray." } } } function Add-DhcpOption([System.IO.MemoryStream]$ms, [int]$Code, [byte[]]$ValueBytes) { if ($ValueBytes.Length -gt 255) { throw "Option $Code value is $($ValueBytes.Length) bytes, exceeds the 255-byte TLV length limit." } $ms.WriteByte([byte]$Code) $ms.WriteByte([byte]$ValueBytes.Length) if ($ValueBytes.Length -gt 0) { $ms.Write($ValueBytes, 0, $ValueBytes.Length) } } function Read-DhcpOptions([byte[]]$pkt) { $opts = @{} $i = 240 while ($i -lt $pkt.Length) { $code = $pkt[$i] if ($code -eq 255) { break } if ($code -eq 0) { $i++; continue } if ($i + 1 -ge $pkt.Length) { break } $len = $pkt[$i + 1] $start = $i + 2 if ($start + $len -gt $pkt.Length) { break } $value = [byte[]]::new($len) if ($len -gt 0) { [Array]::Copy($pkt, $start, $value, 0, $len) } $opts[[int]$code] = $value $i = $start + $len } return $opts } function Format-IPBytes([byte[]]$b, [int]$offset) { "{0}.{1}.{2}.{3}" -f $b[$offset], $b[$offset + 1], $b[$offset + 2], $b[$offset + 3] } function ConvertFrom-DhcpOptions([hashtable]$rawOptions) { $decoded = [ordered]@{} foreach ($code in $rawOptions.Keys) { $v = $rawOptions[$code] $decoded[$code] = switch ($code) { 1 { Format-IPBytes $v 0 } 3 { Format-IPBytes $v 0 } 6 { $servers = New-Object System.Collections.Generic.List[string] for ($o = 0; $o + 3 -lt $v.Length; $o += 4) { $servers.Add((Format-IPBytes $v $o)) } , $servers.ToArray() } 15 { [System.Text.Encoding]::ASCII.GetString($v) } 51 { ReadU32BE $v 0 } 53 { $MsgTypeNames[[int]$v[0]] } 54 { Format-IPBytes $v 0 } 56 { [System.Text.Encoding]::ASCII.GetString($v) } 58 { ReadU32BE $v 0 } 59 { ReadU32BE $v 0 } default { ($v | ForEach-Object { $_.ToString('x2') }) -join '' } } } return $decoded } function Get-DhcpMessageTypeName([hashtable]$rawOptions) { if ($rawOptions.ContainsKey(53) -and $rawOptions[53].Length -ge 1) { return $MsgTypeNames[[int]$rawOptions[53][0]] } return $null } # Returns @{Data=[byte[]]; EP=[IPEndPoint]} or $null on timeout function Recv([System.Net.Sockets.UdpClient]$u, [int]$ms) { $u.Client.ReceiveTimeout = $ms $ep = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) try { $data = $u.Receive([ref]$ep) return @{ Data = $data; EP = $ep } } catch [System.Net.Sockets.SocketException] { if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::TimedOut) { return $null } throw } } function New-DhcpPacket { param( [byte]$MessageType, [uint32]$Xid, [byte[]]$ChaddrBytes, [System.Collections.Generic.List[hashtable]]$Options, [string]$RequestedIP, [string]$ServerId ) $hdr = [byte[]]::new(236) $hdr[0] = 1 # op = BOOTREQUEST $hdr[1] = 1 # htype = Ethernet $hdr[2] = 6 # hlen $hdr[3] = 0 # hops [Buffer]::BlockCopy([byte[]](WriteU32BE $Xid), 0, $hdr, 4, 4) [Buffer]::BlockCopy([byte[]](WriteU16BE 0), 0, $hdr, 8, 2) # secs [Buffer]::BlockCopy([byte[]](WriteU16BE 0x8000), 0, $hdr, 10, 2) # flags: broadcast bit always set # ciaddr/yiaddr/siaddr/giaddr left zero [Buffer]::BlockCopy($ChaddrBytes, 0, $hdr, 28, 6) # chaddr, remaining 10 bytes zero-padded $ms = [System.IO.MemoryStream]::new() $ms.Write($hdr, 0, $hdr.Length) $ms.Write($MagicCookie, 0, $MagicCookie.Length) Add-DhcpOption $ms 53 ([byte[]]@($MessageType)) if ($MessageType -eq $MSG_REQUEST) { Add-DhcpOption $ms 50 ([System.Net.IPAddress]::Parse($RequestedIP).GetAddressBytes()) Add-DhcpOption $ms 54 ([System.Net.IPAddress]::Parse($ServerId).GetAddressBytes()) } if ($RequestOptions -and $RequestOptions.Count -gt 0) { $prl = [byte[]]::new($RequestOptions.Count) for ($i = 0; $i -lt $RequestOptions.Count; $i++) { if ($RequestOptions[$i] -lt 0 -or $RequestOptions[$i] -gt 255) { throw "RequestOptions value $($RequestOptions[$i]) is out of range 0-255." } $prl[$i] = [byte]$RequestOptions[$i] } Add-DhcpOption $ms 55 $prl } foreach ($opt in $Options) { $valueBytes = ConvertTo-DhcpOptionBytes -Code $opt.Code -Type $opt.Type -Value $opt.Value Add-DhcpOption $ms $opt.Code $valueBytes } $ms.WriteByte(255) # End option $bytes = $ms.ToArray() if ($bytes.Length -lt 300) { $padded = [byte[]]::new(300) [Buffer]::BlockCopy($bytes, 0, $padded, 0, $bytes.Length) return $padded } return $bytes } # ── Argument validation / option assembly ──────────────────────────────── $xid = if ($PSBoundParameters.ContainsKey('TransactionId')) { $TransactionId } else { [uint32](Get-Random -Minimum 1 -Maximum ([int64]4294967295)) } $clientMacBytes = if ($ClientMac) { ConvertTo-MacBytes $ClientMac } else { New-RandomMacBytes } $clientMacString = Format-MacBytes $clientMacBytes $allOptions = [System.Collections.Generic.List[hashtable]]::new() $usedCodes = [System.Collections.Generic.HashSet[int]]::new() function Add-NamedOption([int]$Code, [string]$Type, $Value, [string]$Name) { if ($null -eq $Value -or $Value -eq '') { return } if (-not $usedCodes.Add($Code)) { throw "Option $Code specified both via -$Name and -Option; use only one." } $allOptions.Add(@{ Code = $Code; Type = $Type; Value = $Value }) } foreach ($opt in $Option) { if (-not $opt.ContainsKey('Code')) { throw "Each -Option entry must include a 'Code' key." } if (-not $opt.ContainsKey('Type')) { throw "Each -Option entry must include a 'Type' key." } if (-not $opt.ContainsKey('Value')) { throw "Each -Option entry must include a 'Value' key." } $code = [int]$opt.Code if ($code -le 0 -or $code -ge 255) { throw "Option code $code is invalid; valid custom option codes are 1-254." } if (-not $usedCodes.Add($code)) { throw "Option $code specified more than once." } $allOptions.Add(@{ Code = $code; Type = $opt.Type; Value = $opt.Value }) } Add-NamedOption 12 'String' $Hostname 'Hostname' Add-NamedOption 60 'String' $VendorClassIdentifier 'VendorClassIdentifier' Add-NamedOption 61 'String' $ClientIdentifier 'ClientIdentifier' $bindAddr = if ($Bind) { $parsed = $null if (-not [System.Net.IPAddress]::TryParse($Bind, [ref]$parsed)) { throw "Invalid -Bind address: '$Bind'." } $parsed } else { [System.Net.IPAddress]::Any } $serverIP = $null try { $serverIP = [System.Net.IPAddress]::Parse($ServerAddress) } catch { try { $serverIP = [System.Net.Dns]::GetHostAddresses($ServerAddress) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if (-not $serverIP) { throw "No IPv4 address." } } catch { throw "Cannot resolve -ServerAddress '$ServerAddress': $_" } } $destEP = [System.Net.IPEndPoint]::new($serverIP, $ServerPort) # ── Result object ───────────────────────────────────────────────────────── $result = [PSCustomObject]@{ Status = 'Unknown' TransactionId = '0x' + $xid.ToString('X8') ClientMac = $clientMacString ServerAddress = $ServerAddress ResponderAddress = $null ServerIdentifier = $null OfferedAddress = $null AckAddress = $null LeaseTimeSeconds = $null LeaseTime = $null SubnetMask = $null Router = $null DnsServers = $null DomainName = $null RenewalTimeT1 = $null RebindingTimeT2 = $null RequestSent = $false NakMessage = $null Options = $null Attempts = 0 ElapsedMs = $null Error = $null } # ── Socket setup ───────────────────────────────────────────────────────── $udp = [System.Net.Sockets.UdpClient]::new() try { $udp.Client.SetSocketOption([System.Net.Sockets.SocketOptionLevel]::Socket, [System.Net.Sockets.SocketOptionName]::ReuseAddress, $true) $udp.EnableBroadcast = $true try { $udp.Client.Bind([System.Net.IPEndPoint]::new($bindAddr, $ClientPort)) } catch [System.Net.Sockets.SocketException] { throw "Failed to bind UDP port $ClientPort ($($_.Exception.Message)). This usually means the Windows 'Dhcp' client service or another instance of this tool holds the port, or you are not running elevated. Try: (1) run PowerShell as Administrator, and/or (2) temporarily stop the DHCP Client service: Stop-Service Dhcp -Force (Start-Service Dhcp afterward)." } $sw = [System.Diagnostics.Stopwatch]::StartNew() try { # ── DISCOVER → OFFER ───────────────────────────────────────────── $discoverPkt = New-DhcpPacket -MessageType $MSG_DISCOVER -Xid $xid -ChaddrBytes $clientMacBytes -Options $allOptions $attempt = 0 $offer = $null while (-not $offer -and $attempt -lt $Retries) { $null = $udp.Send($discoverPkt, $discoverPkt.Length, $destEP) $result.Attempts++ Write-Verbose "Sent DHCPDISCOVER (xid=$($result.TransactionId), chaddr=$clientMacString) to $($destEP), attempt $($attempt + 1)/$Retries" $r = Recv $udp ($TimeoutSeconds * 1000) if (-not $r) { $attempt++ Write-Verbose "No reply within $TimeoutSeconds s (retry $attempt/$Retries)" continue } $rawOpts = Read-DhcpOptions $r.Data $rxXid = ReadU32BE $r.Data 4 $msgType = Get-DhcpMessageTypeName $rawOpts if ($rxXid -ne $xid -or $msgType -ne 'Offer') { Write-Verbose "Ignoring unrelated packet (xid=0x$($rxXid.ToString('X8')), type=$msgType) from $($r.EP.Address)" continue } $offer = $r } if ($LogActivity) { Write-Log -Message "DISCOVER sent, xid=$($result.TransactionId), attempts=$($result.Attempts)" -Level INFO -fileNamePrefix 'Invoke-DhcpTest' } if (-not $offer) { $result.Status = 'Timeout' $result.Error = "No DHCPOFFER received after $Retries attempt(s) ($TimeoutSeconds sec each)." $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } $offerRaw = Read-DhcpOptions $offer.Data $offerDecoded = ConvertFrom-DhcpOptions $offerRaw $offeredAddress = Format-IPBytes $offer.Data 16 $serverIdentifier = if ($offerDecoded.Contains(54)) { $offerDecoded[54] } else { $offer.EP.Address.ToString() } $result.Status = 'OfferReceived' $result.ResponderAddress = $offer.EP.Address.ToString() $result.ServerIdentifier = $serverIdentifier $result.OfferedAddress = $offeredAddress $result.LeaseTimeSeconds = if ($offerDecoded.Contains(51)) { [int]$offerDecoded[51] } else { $null } $result.LeaseTime = if ($result.LeaseTimeSeconds) { [TimeSpan]::FromSeconds($result.LeaseTimeSeconds) } else { $null } $result.SubnetMask = if ($offerDecoded.Contains(1)) { $offerDecoded[1] } else { $null } $result.Router = if ($offerDecoded.Contains(3)) { $offerDecoded[3] } else { $null } $result.DnsServers = if ($offerDecoded.Contains(6)) { $offerDecoded[6] } else { $null } $result.DomainName = if ($offerDecoded.Contains(15)) { $offerDecoded[15] } else { $null } $result.RenewalTimeT1 = if ($offerDecoded.Contains(58)) { [int]$offerDecoded[58] } else { $null } $result.RebindingTimeT2 = if ($offerDecoded.Contains(59)) { [int]$offerDecoded[59] } else { $null } $result.Options = $offerDecoded Write-Verbose "DHCPOFFER received: address=$offeredAddress server=$serverIdentifier lease=$($result.LeaseTimeSeconds)s" if (-not $SendRequest) { $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } # ── REQUEST → ACK/NAK ──────────────────────────────────────────── $requestedIP = if ($RequestedIPAddress) { $RequestedIPAddress } else { $offeredAddress } if (-not $PSCmdlet.ShouldProcess("$ServerAddress`:$ServerPort", "Request lease $requestedIP via server $serverIdentifier")) { $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } $requestPkt = New-DhcpPacket -MessageType $MSG_REQUEST -Xid $xid -ChaddrBytes $clientMacBytes -Options $allOptions -RequestedIP $requestedIP -ServerId $serverIdentifier $result.RequestSent = $true $attempt = 0 $final = $null while (-not $final -and $attempt -lt $Retries) { $null = $udp.Send($requestPkt, $requestPkt.Length, $destEP) Write-Verbose "Sent DHCPREQUEST for $requestedIP (xid=$($result.TransactionId)), attempt $($attempt + 1)/$Retries" $r = Recv $udp ($TimeoutSeconds * 1000) if (-not $r) { $attempt++ Write-Verbose "No reply within $TimeoutSeconds s (retry $attempt/$Retries)" continue } $rawOpts = Read-DhcpOptions $r.Data $rxXid = ReadU32BE $r.Data 4 $msgType = Get-DhcpMessageTypeName $rawOpts if ($rxXid -ne $xid -or ($msgType -ne 'Ack' -and $msgType -ne 'Nak')) { Write-Verbose "Ignoring unrelated packet (xid=0x$($rxXid.ToString('X8')), type=$msgType) from $($r.EP.Address)" continue } $final = @{ Reply = $r; RawOpts = $rawOpts; MsgType = $msgType } } if ($LogActivity) { Write-Log -Message "REQUEST sent for $requestedIP, xid=$($result.TransactionId)" -Level INFO -fileNamePrefix 'Invoke-DhcpTest' } if (-not $final) { $result.Status = 'NoAckResponse' $result.Error = "No DHCPACK/DHCPNAK received after $Retries attempt(s) ($TimeoutSeconds sec each)." $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } $finalDecoded = ConvertFrom-DhcpOptions $final.RawOpts $result.ResponderAddress = $final.Reply.EP.Address.ToString() $result.Options = $finalDecoded if ($final.MsgType -eq 'Nak') { $result.Status = 'NakReceived' $result.NakMessage = if ($finalDecoded.Contains(56)) { $finalDecoded[56] } else { $null } Write-Verbose "DHCPNAK received: $($result.NakMessage)" } else { $ackAddress = Format-IPBytes $final.Reply.Data 16 $result.Status = 'AckReceived' $result.AckAddress = $ackAddress $result.LeaseTimeSeconds = if ($finalDecoded.Contains(51)) { [int]$finalDecoded[51] } else { $result.LeaseTimeSeconds } $result.LeaseTime = if ($result.LeaseTimeSeconds) { [TimeSpan]::FromSeconds($result.LeaseTimeSeconds) } else { $null } $result.ServerIdentifier = if ($finalDecoded.Contains(54)) { $finalDecoded[54] } else { $result.ServerIdentifier } Write-Verbose "DHCPACK received: address=$ackAddress lease=$($result.LeaseTimeSeconds)s" } $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } catch [System.Net.Sockets.SocketException] { $result.Status = 'Error' $result.Error = $_.Exception.Message $result.ElapsedMs = $sw.Elapsed.TotalMilliseconds return $result } } finally { $udp.Close() $udp.Dispose() } } |