File: System\Net\NetworkInformation\Ping.PingUtility.cs
Web Access
Project: src\src\libraries\System.Net.Ping\src\System.Net.Ping.csproj (System.Net.Ping)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.NetworkInformation
{
    public partial class Ping
    {
        private Process GetPingProcess(IPAddress address, byte[] buffer, int timeout, PingOptions? options)
        {
            bool isIpv4 = address.AddressFamily == AddressFamily.InterNetwork;
            string? pingExecutable = isIpv4 ? UnixCommandLinePing.Ping4UtilityPath : UnixCommandLinePing.Ping6UtilityPath;
            if (pingExecutable == null)
            {
                throw new PlatformNotSupportedException(SR.net_ping_utility_not_found);
            }
 
            // although the ping utility supports custom pattern via -p option, it supports
            // specifying only up to 16B pattern which repeats in the payload. The option also might
            // not be present in all distributions, so we forbid ping payload in general.
            if (buffer != DefaultSendBuffer && buffer != Array.Empty<byte>())
            {
                throw new PlatformNotSupportedException(SR.net_ping_utility_custom_payload);
            }
 
            UnixCommandLinePing.PingFragmentOptions fragmentOption = UnixCommandLinePing.PingFragmentOptions.Default;
            if (options != null && address.AddressFamily == AddressFamily.InterNetwork)
            {
                fragmentOption = options.DontFragment ? UnixCommandLinePing.PingFragmentOptions.Do : UnixCommandLinePing.PingFragmentOptions.Dont;
            }
 
            string processArgs = UnixCommandLinePing.ConstructCommandLine(buffer.Length, timeout, address.ToString(), isIpv4, options?.Ttl ?? 0, fragmentOption);
 
            ProcessStartInfo psi = new ProcessStartInfo(pingExecutable, processArgs);
            psi.RedirectStandardOutput = true;
            psi.RedirectStandardError = true;
            // Set LC_ALL=C to make sure to get ping output which is not affected by locale environment variables such as LANG and LC_MESSAGES.
            psi.EnvironmentVariables["LC_ALL"] = "C";
            return new Process() { StartInfo = psi };
        }
 
        private PingReply SendWithPingUtility(IPAddress address, byte[] buffer, int timeout, PingOptions? options)
        {
            using (Process p = GetPingProcess(address, buffer, timeout, options))
            {
                p.Start();
                if (!p.WaitForExit(timeout))
                {
                    return CreatePingReply(IPStatus.TimedOut);
                }
 
                try
                {
                    string stdout = p.StandardOutput.ReadToEnd();
                    return ParsePingUtilityOutput(address, p.ExitCode, stdout);
                }
                catch (Exception)
                {
                    // If the standard output cannot be successfully parsed, throw a generic PingException.
                    throw new PingException(SR.net_ping);
                }
            }
        }
 
        private async Task<PingReply> SendWithPingUtilityAsync(IPAddress address, byte[] buffer, int timeout, PingOptions? options)
        {
            CancellationToken timeoutOrCancellationToken = _timeoutOrCancellationSource!.Token;
 
            using Process pingProcess = GetPingProcess(address, buffer, timeout, options);
            pingProcess.Start();
 
            try
            {
                await pingProcess.WaitForExitAsync(timeoutOrCancellationToken).ConfigureAwait(false);
 
                string stdout = await pingProcess.StandardOutput.ReadToEndAsync(timeoutOrCancellationToken).ConfigureAwait(false);
 
                return ParsePingUtilityOutput(address, pingProcess.ExitCode, stdout);
            }
            catch (OperationCanceledException) when (timeoutOrCancellationToken.IsCancellationRequested)
            {
                if (!pingProcess.HasExited)
                {
                    pingProcess.Kill();
                }
                if (_canceled)
                {
                    throw;
                }
                return CreatePingReply(IPStatus.TimedOut);
            }
            catch (Exception e)
            {
                // If the standard output cannot be successfully read/parsed, throw a generic PingException.
                throw new PingException(SR.net_ping, e);
            }
        }
 
        private static PingReply ParsePingUtilityOutput(IPAddress address, int exitCode, string stdout)
        {
            // Throw timeout for known failure return codes from ping functions.
            if (exitCode == 1 || exitCode == 2)
            {
                // TTL exceeded may have occurred
                if (TryParseTtlExceeded(stdout, out PingReply? reply))
                {
                    return reply!;
                }
 
                // otherwise assume timeout
                return CreatePingReply(IPStatus.TimedOut);
            }
 
            // On success, report RTT
            long rtt = UnixCommandLinePing.ParseRoundTripTime(stdout);
            return CreatePingReply(IPStatus.Success, address, rtt);
        }
 
        private static bool TryParseTtlExceeded(string stdout, out PingReply? reply)
        {
            reply = null;
            if (!stdout.Contains("Time to live exceeded", StringComparison.Ordinal))
            {
                return false;
            }
 
            // look for address in:
            // - "From 172.21.64.1 icmp_seq=1 Time to live exceeded"
            // - "From 172.21.64.1: icmp_seq=1 Time to live exceeded"
            int addressStart = stdout.IndexOf("From ", StringComparison.Ordinal) + 5;
            int addressLength = stdout.AsSpan(Math.Max(addressStart, 0)).IndexOfAny(' ', ':');
            IPAddress? address;
            if (addressStart < 5 || addressLength <= 0 || !IPAddress.TryParse(stdout.AsSpan(addressStart, addressLength), out address))
            {
                // failed to parse source address (which in case of TTL is different than the original
                // destination address), fallback to all 0
                address = new IPAddress(0);
            }
 
            reply = CreatePingReply(IPStatus.TimeExceeded, address);
            return true;
        }
    }
}