-
-
Save tylerapplebaum/dc527a3bd875f11871e2 to your computer and use it in GitHub Desktop.
<# | |
.SYNOPSIS | |
An MTR clone for PowerShell. | |
Written by Tyler Applebaum. | |
Version 2.1 | |
.LINK | |
https://gist.github.com/tylerapplebaum/dc527a3bd875f11871e2 | |
http://www.team-cymru.org/IP-ASN-mapping.html#dns | |
.DESCRIPTION | |
Runs a traceroute to a specified target; sends ICMP packets to each hop to measure loss and latency. | |
Big shout out to Team Cymru for the ASN resolution. | |
Thanks to DrDrrae for a bugfix on PowerShell v5 | |
.PARAMETER Target | |
Input must be in the form of an IP address or FQDN. Should be compatible with most TLDs. | |
.PARAMETER PingCycles | |
Specifies the number of ICMP packets to send per hop. Default is 10. | |
.PARAMETER DNSServer | |
An optional parameter to specify a different DNS server than configured on your network adapter. | |
.INPUTS | |
System.String, System.Int32 | |
.OUTPUTS | |
PSObject containing the traceroute results. Also saves a file to the desktop. | |
.EXAMPLE | |
PS C:\> Get-Traceroute 8.8.4.4 -b 512 | |
Runs a traceroute to 8.8.4.4 with 512-byte ICMP packets. | |
.EXAMPLE | |
PS C:\> Get-Traceroute amazon.com -s 75.75.75.75 -f amazon.com -h 45 | |
Runs a traceroute to amazon.com using 75.75.75.75 as the DNS resolver and saves the output as amazon.com.txt. | |
Increase max hop count from default of 30 to 45. | |
#> | |
#Requires -version 4 | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$True,ValueFromPipeline=$True)] | |
[String]$Target, | |
[Parameter(ValueFromPipeline)] | |
[Alias("c")] | |
[ValidateRange(5,100)] | |
[int]$PingCycles = 10, #Default to 10 pings per hop; minimum of 5, maximum of 100 | |
[Parameter(ValueFromPipeline)] | |
[Alias("b")] | |
[ValidateRange(32,1000)] | |
[int]$BufLen = 32, #Default to 32 bytes of data in the ICMP packet, maximum of 1000 bytes | |
[Parameter(ValueFromPipeline)] | |
[Alias("s")] | |
[IPAddress]$DNSServer = $Null, | |
[Parameter(ValueFromPipeline)] | |
[Alias("h")] | |
[ValidateRange(30,120)] | |
[Int]$Hops = 60, | |
[Parameter(ValueFromPipeline)] | |
[Alias("f")] | |
[String]$Filename = "Traceroute_$Target" | |
) | |
Function script:Set-Variables { | |
$PerTraceArr = @() | |
$script:ASNOwnerArr = @() | |
$ASNOwnerObj = New-Object PSObject | |
$ASNOwnerObj | Add-Member NoteProperty "ASN"("AS0") | |
$ASNOwnerObj | Add-Member NoteProperty "ASN Owner"("EvilCorp") | |
$ASNOwnerArr += $ASNOwnerObj #Add some values so the array isn't empty when first checked. | |
$script:i = 0 | |
$script:x = 0 | |
$script:z = 0 | |
$script:WHOIS = ".origin.asn.cymru.com" | |
$script:ASNWHOIS = ".asn.cymru.com" | |
} #End Set-Variables | |
Function script:Set-WindowSize { | |
$Window = $Host.UI.RawUI | |
If ($Window.BufferSize.Width -lt 175 -OR $Window.WindowSize.Width -lt 175) { | |
$NewSize = $Window.BufferSize | |
$NewSize.Height = 3000 | |
$NewSize.Width = 175 | |
$Window.BufferSize = $NewSize | |
$NewSize = $Window.WindowSize | |
$NewSize.Height = 50 | |
$NewSize.Width = 175 | |
$Window.WindowSize = $NewSize | |
} | |
} #End Set-WindowSize | |
Function script:Get-Traceroute { | |
$script:TraceResults = Test-NetConnection $Target -Hops $Hops -InformationLevel Detailed -TraceRoute | Select -ExpandProperty TraceRoute | |
} #End Get-Traceroute | |
Function script:Resolve-ASN { | |
$HopASN = $null #Reset to null each time | |
$HopASNRecord = $null #Reset to null each time | |
If ($Hop -notlike "TimedOut" -AND $Hop -notmatch "^(?:10|127|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\..*") { #Don't waste a lookup on RFC1918 IPs | |
$HopSplit = $Hop.Split('.') | |
$HopRev = $HopSplit[3] + '.' + $HopSplit[2] + '.' + $HopSplit[1] + '.' + $HopSplit[0] | |
$HopASNRecord = Resolve-DnsName -Server $DNSServer -Type TXT -Name $HopRev$WHOIS -ErrorAction SilentlyContinue | Select Strings | |
} | |
Else { | |
$HopASNRecord = $null | |
} | |
If ($HopASNRecord.Strings -AND $HopASNRecord.Strings.GetType().IsArray){ #Check for array; | |
$HopASN = "AS"+$HopASNRecord.Strings[0].Split('|').Trim()[0] | |
Write-Verbose "Object found $HopASN" | |
} | |
ElseIf ($HopASNRecord.Strings -AND $HopASNRecord.Strings.GetType().FullName -like "System.String"){ #Check for string; normal case. | |
$HopASN = "AS"+$HopASNRecord.Strings[0].Split('|').Trim()[0] | |
Write-Verbose "String found $HopASN" | |
} | |
Else { | |
$HopASN = "-" | |
} | |
} #End Resolve-ASN | |
Function script:Resolve-ASNOwner { | |
If ($HopASN -notlike "-") { | |
$IndexNo = $ASNOwnerArr.ASN.IndexOf($HopASN) | |
Write-Verbose "Current object: $ASNOwnerObj" | |
If (!($ASNOwnerArr.ASN.Contains($HopASN)) -OR ($ASNOwnerArr."ASN Owner"[$IndexNo].Contains('-'))){ #Keep "ASNOwnerArr.ASN" in double quotes so it will be treated as a string and not an object | |
Write-Verbose "ASN $HopASN not previously resolved; performing lookup" #Check the previous lookups before running this unnecessarily | |
$HopASNOwner = Resolve-DnsName -Server $DNSServer -Type TXT -Name $HopASN$ASNWHOIS -ErrorAction SilentlyContinue | Select Strings | |
If ($HopASNOwner.Strings -AND $HopASNOwner.Strings.GetType().IsArray){ #Check for array; | |
$HopASNOwner = $HopASNOwner.Strings[0].Split('|').Trim()[4].Split('-')[0] | |
Write-Verbose "Object found $HopASNOwner" | |
} | |
ElseIf ($HopASNRecord.Strings -AND $HopASNRecord.Strings.GetType().FullName -like "System.String"){ #Check for string; normal case. | |
$HopASNOwner = $HopASNOwner.Strings[0].Split('|').Trim()[4].Split('-')[0] | |
Write-Verbose "String found $HopASNOwner" | |
} | |
Else { | |
$HopASNOwner = "-" | |
} | |
$ASNOwnerObj | Add-Member NoteProperty "ASN"($HopASN) -Force | |
$ASNOwnerObj | Add-Member NoteProperty "ASN Owner"($HopASNOwner) -Force | |
$ASNOwnerArr += $ASNOwnerObj #Add our new value to the cache | |
} | |
Else { #We get to use a cached entry and save Team Cymru some lookups | |
Write-Verbose "ASN Owner found in cache" | |
$HopASNOwner = $ASNOwnerArr[$IndexNo]."ASN Owner" | |
} | |
} | |
Else { | |
$HopASNOwner = "-" | |
Write-Verbose "ASN Owner lookup not performed - RFC1918 IP found or hop TimedOut" | |
} | |
} #End Resolve-ASNOwner | |
Function script:Resolve-DNS { | |
$HopNameArr = $null | |
$script:HopName = New-Object psobject | |
If ($Hop -notlike "TimedOut" -and $Hop -notlike "0.0.0.0") { | |
$z++ #Increment the count for the progress bar | |
$script:HopNameArr = Resolve-DnsName -Server $DNSServer -Type PTR $Hop -ErrorAction SilentlyContinue | Select NameHost | |
Write-Verbose "Hop = $Hop" | |
If ($HopNameArr.NameHost -AND $HopNameArr.NameHost.GetType().IsArray) { #Check for array first; sometimes resolvers are stupid and return NS records with the PTR in an array. | |
$script:HopName | Add-Member -MemberType NoteProperty -Name NameHost -Value $HopNameArr.NameHost[0] #If Resolve-DNS brings back an array containing NS records, select just the PTR | |
Write-Verbose "Object found $HopName" | |
} | |
ElseIf ($HopNameArr.NameHost -AND $HopNameArr.NameHost.GetType().FullName -like "System.String") { #Normal case. One PTR record. Will break up an array of multiple PTRs separated with a comma. | |
$script:HopName | Add-Member -MemberType NoteProperty -Name NameHost -Value $HopNameArr.NameHost.Split(',')[0].Trim() #In the case of multiple PTRs select the first one | |
Write-Verbose "String found $HopName" | |
} | |
ElseIf ($HopNameArr.NameHost -like $null) { #Check for null last because when an array is returned with PTR and NS records, it contains null values. | |
$script:HopName | Add-Member -MemberType NoteProperty -Name NameHost -Value $Hop #If there's no PTR record, set name equal to IP | |
Write-Verbose "HopNameArr apparently empty for $HopName" | |
} | |
Write-Progress -Activity "Resolving PTR Record" -Status "Looking up $Hop, Hop #$z of $($TraceResults.length)" -PercentComplete ($z / $($TraceResults.length)*100) | |
} | |
Else { | |
$z++ | |
$script:HopName | Add-Member -MemberType NoteProperty -Name NameHost -Value $Hop #If the hop times out, set name equal to TimedOut | |
Write-Verbose "Hop = $Hop" | |
} | |
} #End Resolve-DNS | |
Function script:Get-PerHopRTT { | |
$PerHopRTTArr = @() #Store all RTT values per hop | |
$SAPSObj = $null #Clear the array each cycle | |
$SendICMP = New-Object System.Net.NetworkInformation.Ping | |
$i++ #Advance the count | |
$x = 0 #Reset x for the next hop count. X tracks packet loss percentage. | |
$BufferData = "a" * $BufLen #Send the UTF-8 letter "a" | |
$ByteArr = [Text.Encoding]::UTF8.GetBytes($BufferData) | |
If ($Hop -notlike "TimedOut" -and $Hop -notlike "0.0.0.0" -and $Hop -notlike "::") { #Normal case, attempt to ping hop | |
For ($y = 1; $y -le $PingCycles; $y++){ | |
$HopResults = $SendICMP.Send($Hop,1000,$ByteArr) #Send the packet with a 1 second timeout | |
$HopRTT = $HopResults.RoundtripTime | |
$PerHopRTTArr += $HopRTT #Add RTT to HopRTT array | |
If ($HopRTT -eq 0) { | |
$x = $x + 1 | |
} | |
Write-Progress -Activity "Testing Packet Loss to Hop #$z of $($TraceResults.length)" -Status "Sending ICMP Packet $y of $PingCycles to $Hop - Result: $HopRTT ms" -PercentComplete ($y / $PingCycles*100) | |
} #End for loop | |
$PerHopRTTArr = $PerHopRTTArr | Where-Object {$_ -gt 0} #Remove zeros from the array | |
$HopRTTMin = "{0:N0}" -f ($PerHopRTTArr | Measure-Object -Minimum).Minimum | |
$HopRTTMax = "{0:N0}" -f ($PerHopRTTArr | Measure-Object -Maximum).Maximum | |
$HopRTTAvg = "{0:N0}" -f ($PerHopRTTArr | Measure-Object -Average).Average | |
$HopLoss = "{0:N1}" -f (($x / $PingCycles) * 100) + "`%" | |
$HopText = [string]$HopRTT + "ms" | |
If ($HopLoss -like "*100*") { #100% loss, but name resolves | |
$HopResults = $null | |
$HopRTT = $null | |
$HopText = $null | |
$HopRTTAvg = "-" | |
$HopRTTMin = "-" | |
$HopRTTMax = "-" | |
} | |
} #End main ping loop | |
Else { #Hop TimedOut - no ping attempted | |
$HopResults = $null | |
$HopRTT = $null | |
$HopText = $null | |
$HopLoss = "100.0%" | |
$HopRTTAvg = "-" | |
$HopRTTMin = "-" | |
$HopRTTMax = "-" | |
} #End TimedOut condition | |
$script:SAPSObj = [PSCustomObject]@{ | |
"Hop" = $i | |
"Hop Name" = $HopName.NameHost | |
"ASN" = $HopASN | |
"ASN Owner" = $HopASNOwner | |
"`% Loss" = $HopLoss | |
"Hop IP" = $Hop | |
"Avg RTT" = $HopRTTAvg | |
"Min RTT" = $HopRTTMin | |
"Max RTT" = $HopRTTMax | |
} | |
$PerTraceArr += $SAPSObj #Add the object to the array | |
} #End Get-PerHopRTT | |
. Set-Variables | |
. Set-WindowSize | |
. Get-Traceroute | |
ForEach ($Hop in $TraceResults) { | |
. Resolve-ASN | |
. Resolve-ASNOwner | |
. Resolve-DNS | |
. Get-PerHopRTT | |
} | |
$PerTraceArr | Format-Table -Autosize | |
$PerTraceArr | Format-Table -Autosize | Out-File $env:UserProfile\Desktop\$Filename.txt -encoding UTF8 |
Next step is to convert Get-PerHopRTT to a job that runs in parallel. Spawn one job for each hop so that the traceroute finishes faster.
Added ICMP payload parameter (-b)
Added REGEX to prevent ASN lookups for RFC1918 IP addresses.
Feature requests:
- Parameterize output filename
- Add ASN Owner to output (requires another DNS query to Cymru)
- Remove regex for IP/domain validation (it is unnecessary)
Features above added! Output now looks like below:
PS C:\> Get-Traceroute.ps1 github.com
Hop Hop Name ASN ASN Owner % Loss Hop IP Avg RTT Min RTT Max RTT
--- -------- --- --------- ------ ------ ------- ------- -------
1 vlan255-switch01.supersecretdomain.org - - 0.0% 172.31.255.1 3 2 4
2 v106-switch02.supersecretdomain.org - - 0.0% 10.98.0.3 3 3 4
3 switch-edge.otherdomain.org AS00000 ECORP 0.0% 85.95.202.17 24 4 117
4 rtr-inet1.otherdomain.org AS00000 ECORP 0.0% 85.95.202.1 3 3 4
5 rtr-inet2.otherdomain.org AS00000 ECORP 0.0% 85.95.202.3 4 3 4
6 v224.core0.pdx2.he.net AS6939 HURRICANE 0.0% 65.60.226.224 9 3 18
7 100ge14-1.core1.sea1.he.net AS6939 HURRICANE 0.0% 184.105.64.137 17 7 66
8 206.111.7.69.ptr.us.xo.net AS2828 XO 0.0% 206.111.7.69 8 7 9
9 vb2000d1.rar3.seattle-wa.us.xo.net AS2828 XO 0.0% 207.88.13.142 9 7 11
10 ae0.rcb1.saltlake2-ut.us.xo.net AS2828 XO 100.0% 207.88.12.77 - - -
11 207.88.12.144.ptr.us.xo.net AS2828 XO 100.0% 207.88.12.144 - - -
12 207.88.12.190.ptr.us.xo.net AS2828 XO 100.0% 207.88.12.190 - - -
13 te0-12-0-0.rar3.sanjose-ca.us.xo.net AS2828 XO 100.0% 207.88.12.189 - - -
14 207.88.12.164.ptr.us.xo.net AS2828 XO 100.0% 207.88.12.164 - - -
15 207.88.12.213.ptr.us.xo.net AS2828 XO 100.0% 207.88.12.213 - - -
16 207.88.12.214.ptr.us.xo.net AS2828 XO 100.0% 207.88.12.214 - - -
17 207.88.14.191.ptr.us.xo.net AS2828 XO 0.0% 207.88.14.191 75 75 77
18 209.48.43.106 AS2828 XO 100.0% 209.48.43.106 - - -
19 TimedOut - - 100.0% TimedOut - - -
20 TimedOut - - 100.0% TimedOut - - -
21 192.30.253.112 AS36459 GITHUB 0.0% 192.30.253.112 74 74 74
An important fact pointed out by a dedicated user:
Get-Traceroute.ps1 requires PS 4.0 under Windows 8 or higher
An untested workaround by Anton Krouglov here: https://stackoverflow.com/questions/21252824/how-do-i-get-powershell-4-cmdlets-such-as-test-netconnection-to-work-on-windows may be of interest.
Going to either fork or update this to be Win7 / PowerShellv2 compatible.
Starting by eliminating Test-NetConnection and writing my own traceroute implementation in .Net.
$Target = "github.com"
$TraceArr = @() #Store all RTT values per hop
$SendICMP = New-Object System.Net.NetworkInformation.Ping
$BufferData = "a" * $BufLen #Send the UTF-8 letter "a"
$ByteArr = [Text.Encoding]::UTF8.GetBytes($BufferData)
$PingOptions = New-Object System.Net.NetworkInformation.PingOptions
$z = 1
Do {
$PingOptions.TTL = $z
$ICMPResults = $SendICMP.Send($Target,1000,$ByteArr,$PingOptions)
Write-Output $ICMPResults.Address.IPAddressToString
Write-Output $ICMPResults.Status
$TraceArr += $ICMPResults.Address.IPAddressToString
$z++
Write-Host $z
}
Until ($ICMPResults.Status -eq "Success")
Write-Output $TraceArr
Resolving DNS records in a timely manner is not easy. New function for the PSv2 compatible fork -
Function script:Resolve-DNS {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$True)]
[Int]$Timeout = 15
)
$ResolveDNSSource = @"
//https://web.archive.org/web/20110207162807/http://www.codekeep.net/snippets/de6ba175-0dad-41e3-8f8f-9d652572329c.aspx
using System;
using System.Net;
public class ResolveDNS {
public delegate IPHostEntry GetHostEntryHandler(string ip);
public string ReverseDNS(string ip, int timeout)
{
try
{
GetHostEntryHandler callback = new GetHostEntryHandler(Dns.GetHostEntry);
IAsyncResult result = callback.BeginInvoke(ip, null, null);
if (result.AsyncWaitHandle.WaitOne(timeout * 1000, false))
{
// Received response within timeout limit
return callback.EndInvoke(result).HostName;
}
else
{
// Did not receive response within timeout limit,
// send back IP Address instead of hostname
return ip;
}
}
catch (Exception)
{
// Error occurred, send back IP Address instead of hostname
return ip;
}
}
}
"@
Add-Type -TypeDefinition $ResolveDNSSource
$Resolver = New-Object ResolveDNS
If ($Hop -notlike "TimedOut" -and $Hop -notlike "0.0.0.0") {
$z++ #Increment the count for the progress bar
Write-Progress -Activity "Resolving PTR Record" -Status "Looking up $Hop, Hop #$z of $($TraceResults.length)" -PercentComplete ($z / $($TraceResults.length)*100)
$HopName = $Resolver.ReverseDNS($Hop,$Timeout)
Write-Verbose "HopName = $HopName"
Write-Verbose "Hop = $Hop"
}
Else {
$z++
$HopName = $Hop #If the hop times out, set name equal to TimedOut
Write-Verbose "Hop = $Hop"
}
} #End Resolve-DNS
Output -
Hi, is it possible to get the every sent packet as a row in a table with timestamp as first column and the HOPs are the column headers ?
Hey, @dexit. Maybe so. I will take a look at that.
@dexit can you provide a mockup of what exactly you're looking for?
It seems like this script doesn't have IPv6 support?
@catcosmicice - correct. I'm happy to take PRs and give contributors credit!
@catcosmicice - correct. I'm happy to take PRs and give contributors credit!
I'd be glad to but I don't really know PowerShell nor Networking 😅
Did a tiny bit of work to get IPv6 working more smoothly. Also added new Hops
parameter to go beyond the standard default of 30 hops.
@catcosmicice - IPv6 should be working. Please give it a shot.
Added some Write-Verbose lines for debugging.
In some cases, DNS resolvers will return some extra garbage in the record, causing it to be interpreted as an object instead of a string. This is now accounted for with some conditional statements when resolving DNS records.