helpers/icmp.psm1

Function Invoke-ICMPPMTUD {

    #If you change order of params, you must change the order of invoke-command params to match
    [CmdletBinding(DefaultParameterSetName = 'PMTUD')]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [Alias("Sender","SourceIP")]
        [string] $Source,

        [Parameter(Mandatory=$true, Position=1)]
        [Alias("Receiver", "DestinationIP", "RemoteIP", "Target")]
        [string] $Destination,

        [Parameter(Mandatory=$false, Position=2)]
        [int] $StartBytes = 32,

        [Parameter(Mandatory=$false, Position=3)]
        [int] $EndBytes = 10000,

        [Parameter(Mandatory=$false, Position=4)]
        [Switch] $Reliability = $false,

        [Parameter(Mandatory=$false, Position=5)]
        [int] $Count = 1000,

        [Parameter(Mandatory=$false, Position=6)]
        [int] $testTime = 15,

        # Used for Write-Progress must also specify ParentID
        [Parameter(Mandatory=$false, Position=7)]
        [int] $ID,

        # Used for Write-Progress must also specify ID
        [Parameter(Mandatory=$false, Position=8)]
        [int] $ParentID
    )

    #region Start-Ping: This function needs to be nested for sending remotely via Invoke-Command (e.g. Function:\Invoke-ICMPPMTU)
    Function Start-Ping {
        param (
            [Parameter(Mandatory=$true, Position=0)]
            [Alias("Sender","SourceIP")]
            [string] $Source,

            [Parameter(Mandatory=$true, Position=1)]
            [Alias("Receiver", "DestinationIP", "RemoteIP", "Target")]
            [string] $Destination,

            [Parameter(Mandatory=$false, Position=2)]
            [int] $Size = 32,

            [Parameter(Mandatory=$false, Position=3)]
            [Switch] $RTT
        )

Add-Type @"
    using System;
    using System.Net;
    using System.Text;
    using System.Runtime.InteropServices;
 
    public class IcmpPing
    {
        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
        private struct ICMP_OPTIONS
        {
            public byte Ttl;
            public byte Tos;
            public byte Flags;
            public byte OptionsSize;
            public IntPtr OptionsData;
        }
 
        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
        private struct ICMP_ECHO_REPLY
        {
            public int Address;
            public int Status;
            public int RoundTripTime;
            public short DataSize;
            public short Reserved;
            public IntPtr DataPtr;
            public ICMP_OPTIONS Options;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst=9900)]
            public string Data;
        }
 
        [DllImport("Iphlpapi.dll", SetLastError = true)]
        private static extern IntPtr IcmpCreateFile();
        [DllImport("Iphlpapi.dll", SetLastError = true)]
        private static extern bool IcmpCloseHandle(IntPtr handle);
        [DllImport("Iphlpapi.dll", SetLastError = true)]
        private static extern int IcmpSendEcho2Ex(IntPtr icmpHandle, IntPtr hEvent, IntPtr apcRoutine, IntPtr apcContext, int sourceAddress, int destinationAddress, string requestData, short requestSize, ref ICMP_OPTIONS requestOptions, ref ICMP_ECHO_REPLY replyBuffer, int replySize, int timeout);
 
        public bool PingStatus(IPAddress sourceIp, IPAddress destIp, int dataSize)
        {
            IntPtr icmpHandle = IcmpCreateFile();
            ICMP_OPTIONS icmpOptions = new ICMP_OPTIONS();
            icmpOptions.Ttl = 255;
            icmpOptions.Flags = 0x02;
            ICMP_ECHO_REPLY icmpReply = new ICMP_ECHO_REPLY();
            string sData = CreateSendData(dataSize);
 
            int replies = IcmpSendEcho2Ex(icmpHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, BitConverter.ToInt32(sourceIp.GetAddressBytes(), 0), BitConverter.ToInt32(destIp.GetAddressBytes(), 0), sData, (short)sData.Length, ref icmpOptions, ref icmpReply, Marshal.SizeOf(icmpReply), 30);
            IcmpCloseHandle(icmpHandle);
 
            if (replies > 0)
            {
                if (icmpReply.DataSize != dataSize)
                {
                    return false;
                }
 
                return true;
            }
 
            return false;
        }
 
        public int PingRTT(IPAddress sourceIp, IPAddress destIp, int dataSize)
        {
            IntPtr icmpHandle = IcmpCreateFile();
            ICMP_OPTIONS icmpOptions = new ICMP_OPTIONS();
            icmpOptions.Ttl = 255;
            icmpOptions.Flags = 0x02;
            ICMP_ECHO_REPLY icmpReply = new ICMP_ECHO_REPLY();
            string sData = CreateSendData(dataSize);
 
            int replies = IcmpSendEcho2Ex(icmpHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, BitConverter.ToInt32(sourceIp.GetAddressBytes(), 0), BitConverter.ToInt32(destIp.GetAddressBytes(), 0), sData, (short)sData.Length, ref icmpOptions, ref icmpReply, Marshal.SizeOf(icmpReply), 30);
            IcmpCloseHandle(icmpHandle);
 
            if (replies > 0)
            {
                return icmpReply.RoundTripTime;
            }
 
            return -1;
        }
 
        private string CreateSendData(int length)
        {
            var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            var random = new Random();
            StringBuilder builder = new StringBuilder();
            for(int index = 0; index < length; index++)
            {
                builder.Append(chars[random.Next(chars.Length)]);
            }
            return builder.ToString();
        }
    }
"@


        $pingStatus = [ICMPPing]::new()

        if ($RTT) {
            return ($pingStatus.PingRTT($Source, $Destination, $Size))
        }
        else {
            return ($pingStatus.PingStatus($Source, $Destination, $Size))
        }


    }

#endregion
    if (-Not($Reliability)) {
        [int] $lastKnownGood = -1

        if ((Start-Ping -Destination $Destination -Source $Source -Size $StartBytes)) {
            # update LKG
            $lastKnownGood = $StartBytes

            # last failed ping size
            $lastFailed = $EndBytes

            # next ping will be somewhere between start and end
            [int]$nextPing = [math]::Round(($EndBytes - $StartBytes) / 2, 0)

            # controls whether we found the MTU or not
            $MtuFound = $false

            :FindMTU while (-NOT $MtuFound) {
                do {
                    Write-Verbose "nextPing: $nextPing, LKG: $lastKnownGood, LF: $lastFailed"
                    $PingTest = Start-Ping -Destination $Destination -Source $Source -Size $nextPing

                    if ($PingTest) { $failedCounter = 0 }
                    else { $failedCounter = $failedCounter + 1 }

                } until ($PingTest -or $failedCounter -gt 3)


                # make payload smaller
                if (-NOT $PingTest) {
                    # save the failed ping size
                    $lastFailed = $nextPing

                    # find the nextPing
                    $nextPing = [math]::Round(($nextPing + $lastKnownGood) / 2, 0)

                    if ($nextPing -ge $lastKnownGood) {
                        $nextPing = [math]::Round(($lastFailed + $nextPing) / 2, 0)
                    }

                    Write-Verbose "NextPing: $nextPing"
                }
                else { # ping worked, but we're not done; make payload larger
                    $LKG = $nextPing

                    $nextPing = [math]::Round(($lastFailed + $lastKnownGood) / 2, 0)

                    if ($nextPing -le $lastKnownGood)
                    {
                        $nextPing = [math]::Round(($lastFailed + $nextPing) / 2, 0)
                    }

                    $lastKnownGood = $LKG
                    Write-Verbose "NextPing: $nextPing"
                }

                Write-Verbose "LastFailed: $lastFailed `n`n"

                # we should reach a point where nextping should be LKG + 1... then we're done
                if ($nextPing -eq $lastKnownGood) {
                    $MtuFound = $true
                    Write-Verbose "All done!"
                }
            }
        }

        if ($lastKnownGood -ne -1) {
            <#
                MTU = Payload + headers
                MSS = Payload
 
                ICMP headers are:
 
                Ethernet = 14 Bytes
                IP = 20 Bytes
                ICMP = 8 Bytes
 
                Total = 42 Bytes
            #>


            $obj = New-Object -TypeName psobject
            $obj | Add-Member -MemberType NoteProperty -Name Connectivity -Value $true
            $obj | Add-Member -MemberType NoteProperty -Name MSS -Value $lastKnownGood
            $obj | Add-Member -MemberType NoteProperty -Name MTU -Value $($lastKnownGood + 42)

            return $obj
        }
        else { # ping failed for some reason
            Write-Verbose "There were no successful pings. Please make sure ping (ICMP Echo) is permitted to $Destination."

            $obj = New-Object -TypeName psobject
            $obj | Add-Member -MemberType NoteProperty -Name Connectivity -Value $false
            $obj | Add-Member -MemberType NoteProperty -Name MSS -Value '0'
            $obj | Add-Member -MemberType NoteProperty -Name MTU -Value '0'

            return $obj
        }
    }
    else { # If we already know the MTU, we can send a bunch at that size to see how reliable the link is
        $ICMPResponse = @()

        $testCompleted = 0
        $startTime = [System.DateTime]::Now

        do {
            #Specify the RTT Switch. We'll use this for more stuff later.
            $ICMPResponse += Start-Ping -Source $Source -Destination $Destination -Size $StartBytes -RTT
        } until([System.DateTime]::Now -ge $startTime.AddSeconds($testTime))

        Return $ICMPResponse
    }
}