Public/network/Trace-NetworkRoute.ps1
|
#Requires -Version 5.1 function Trace-NetworkRoute { <# .SYNOPSIS Performs a traceroute to a target host and returns structured hop-by-hop results. .DESCRIPTION Sends ICMP packets with incrementing TTL values to trace the network path to a destination. Each hop is returned as a structured object with hop number, IP address, hostname (via reverse DNS), and round-trip latency. Uses System.Net.NetworkInformation.Ping with controlled TTL values, which is more reliable and parseable than the native tracert.exe. .PARAMETER ComputerName One or more target hostnames or IP addresses to trace. Accepts pipeline input. .PARAMETER MaxHops Maximum number of hops (TTL). Default: 30. Valid range: 1-128. .PARAMETER TimeoutMs Timeout per hop in milliseconds. Default: 3000. Valid range: 500-30000. .PARAMETER PingsPerHop Number of pings per hop for latency averaging. Default: 3. Valid range: 1-10. .PARAMETER ResolveHostnames Attempt reverse DNS lookup for each hop IP. Default: true. Disable for faster traces when hostnames are not needed. .EXAMPLE Trace-NetworkRoute -ComputerName '8.8.8.8' Traces the route to Google DNS. .EXAMPLE Trace-NetworkRoute -ComputerName 'srv01.corp.local' -MaxHops 15 Traces route to an internal server with max 15 hops. .EXAMPLE '8.8.8.8', '1.1.1.1' | Trace-NetworkRoute -ResolveHostnames:$false Traces routes to two targets without reverse DNS (faster). .OUTPUTS PSWinOps.TraceRouteHop .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-03-21 Requires: PowerShell 5.1+ / Windows only Permissions: No admin required (ICMP may be blocked by firewall) #> [CmdletBinding()] [OutputType('PSWinOps.TraceRouteHop')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName', 'Destination')] [string[]]$ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, 128)] [int]$MaxHops = 30, [Parameter(Mandatory = $false)] [ValidateRange(500, 30000)] [int]$TimeoutMs = 3000, [Parameter(Mandatory = $false)] [ValidateRange(1, 10)] [int]$PingsPerHop = 3, [Parameter(Mandatory = $false)] [bool]$ResolveHostnames = $true ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting route trace (MaxHops: $MaxHops, Timeout: ${TimeoutMs}ms)" } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Tracing route to '$targetComputer'" # Resolve target IP first try { $targetIP = [System.Net.Dns]::GetHostAddresses($targetComputer) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if (-not $targetIP) { Write-Error "[$($MyInvocation.MyCommand)] Cannot resolve '$targetComputer' to an IPv4 address" continue } $targetIPString = $targetIP.ToString() } catch { Write-Error "[$($MyInvocation.MyCommand)] DNS resolution failed for '$targetComputer': $_" continue } $pingSender = New-Object System.Net.NetworkInformation.Ping $buffer = [byte[]]::new(32) $timestamp = Get-Date -Format 'o' $reachedTarget = $false for ($ttl = 1; $ttl -le $MaxHops; $ttl++) { $hopIP = $null $latencies = [System.Collections.Generic.List[double]]::new() $timedOut = $true $pingOptions = New-Object System.Net.NetworkInformation.PingOptions($ttl, $true) for ($p = 0; $p -lt $PingsPerHop; $p++) { try { $reply = $pingSender.Send($targetIPString, $TimeoutMs, $buffer, $pingOptions) if ($reply.Status -eq [System.Net.NetworkInformation.IPStatus]::TtlExpired -or $reply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) { $timedOut = $false $hopIP = $reply.Address.ToString() $latencies.Add($reply.RoundtripTime) } } catch { Write-Verbose "[$($MyInvocation.MyCommand)] Ping to '$targetIPString' (TTL=$ttl, attempt $($p+1)) failed: $_" } } # Resolve hostname if requested $hostname = $null if ($hopIP -and $ResolveHostnames) { try { $hostEntry = [System.Net.Dns]::GetHostEntry($hopIP) $hostname = $hostEntry.HostName } catch { $hostname = $null } } # Compute latency stats for this hop $avgMs = $null $minMs = $null $maxMs = $null if ($latencies.Count -gt 0) { $avgMs = [math]::Round(($latencies | Measure-Object -Average).Average, 1) $minMs = [math]::Round(($latencies | Measure-Object -Minimum).Minimum, 1) $maxMs = [math]::Round(($latencies | Measure-Object -Maximum).Maximum, 1) } [PSCustomObject]@{ PSTypeName = 'PSWinOps.TraceRouteHop' Destination = $targetComputer Hop = $ttl IPAddress = if ($hopIP) { $hopIP } else { '*' } Hostname = if ($hostname) { $hostname } else { '' } AvgMs = $avgMs MinMs = $minMs MaxMs = $maxMs Status = if ($timedOut) { 'TimedOut' } elseif ($hopIP -eq $targetIPString) { 'Reached' } else { 'Hop' } Timestamp = $timestamp } # Stop if we reached the destination if ($hopIP -eq $targetIPString) { $reachedTarget = $true break } } $pingSender.Dispose() if (-not $reachedTarget) { Write-Warning "[$($MyInvocation.MyCommand)] Destination '$targetComputer' not reached within $MaxHops hops" } } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed tracing '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed route trace" } } |