Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Use unprivileged socket to send ping on macOS/iOS/tvOS/Mac Catalyst (#…
…52240)

Revival of #42006 with some feedback incorporated.

This improves Ping behavior on macOS/iOS family. For unprivileged users, it allows to send/receive buffer with custom content and removes craft around spawning new process. (uses [this example](https://developer.apple.com/library/archive/samplecode/SimplePing/Introduction/Intro.html)).

It adds new macOS/iOS/tvOS targets for the assembly that exclude the process creation code.

Fixes #36941
Fixes #51395

Co-authored-by: Tomas Weinfurt <tweinfurt@yahoo.com>
  • Loading branch information
filipnavara and wfurt committed May 7, 2021
1 parent 594e87a commit e98614e
Show file tree
Hide file tree
Showing 8 changed files with 547 additions and 472 deletions.
Expand Up @@ -32,10 +32,7 @@ public static partial class PlatformDetection
public static bool IsFedora => IsDistroAndVersion("fedora");

// OSX family
public static bool IsOSXLike =>
RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) ||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
RuntimeInformation.IsOSPlatform(OSPlatform.Create("TVOS"));
public static bool IsOSXLike => IsOSX || IsiOS || IstvOS || IsMacCatalyst;
public static bool IsOSX => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsNotOSX => !IsOSX;
public static bool IsMacOsMojaveOrHigher => IsOSX && Environment.OSVersion.Version >= new Version(10, 14);
Expand Down
12 changes: 10 additions & 2 deletions src/libraries/System.Net.Ping/src/System.Net.Ping.csproj
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)</TargetFrameworks>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-iOS;$(NetCoreAppCurrent)-tvOS;$(NetCoreAppCurrent)</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<GeneratePlatformNotSupportedAssemblyMessage Condition="'$(TargetsAnyOS)' == 'true'">SR.SystemNetPing_PlatformNotSupported</GeneratePlatformNotSupportedAssemblyMessage>
<TargetsApple Condition="'$(TargetsOSX)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true'">true</TargetsApple>
</PropertyGroup>
<ItemGroup Condition="'$(TargetsAnyOS)' != 'true'">
<Compile Include="System\Net\NetworkInformation\IPStatus.cs" />
Expand Down Expand Up @@ -34,7 +35,7 @@
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
<Compile Include="System\Net\NetworkInformation\IcmpV4MessageConstants.cs" />
<Compile Include="System\Net\NetworkInformation\IcmpV6MessageConstants.cs" />
<Compile Include="System\Net\NetworkInformation\Ping.Unix.cs" />
<Compile Include="System\Net\NetworkInformation\Ping.RawSocket.cs" />
<!-- System.Net Common -->
<Compile Include="$(CommonPath)System\Net\RawSocketPermissions.cs"
Link="Common\System\Net\RawSocketPermissions.cs" />
Expand All @@ -58,6 +59,13 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SocketAddress.cs"
Link="Common\Interop\Unix\System.Native\Interop.SocketAddress.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsUnix)' == 'true' AND '$(TargetsApple)' != 'true'">
<Compile Include="System\Net\NetworkInformation\Ping.Unix.cs" />
<Compile Include="System\Net\NetworkInformation\Ping.PingUtility.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsUnix)' == 'true' AND '$(TargetsApple)' == 'true'">
<Compile Include="System\Net\NetworkInformation\Ping.OSX.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Compile Include="System\Net\NetworkInformation\Ping.Windows.cs" />
<!-- System.Net Common -->
Expand Down
@@ -0,0 +1,37 @@
// 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 static bool SendIpHeader => true;
private static bool NeedsConnect => false;

private PingReply SendPingCore(IPAddress address, byte[] buffer, int timeout, PingOptions? options)
=> SendIcmpEchoRequestOverRawSocket(address, buffer, timeout, options);

private async Task<PingReply> SendPingAsyncCore(IPAddress address, byte[] buffer, int timeout, PingOptions? options)
{
Task<PingReply> t = SendIcmpEchoRequestOverRawSocketAsync(address, buffer, timeout, options);
PingReply reply = await t.ConfigureAwait(false);

if (_canceled)
{
throw new OperationCanceledException();
}

return reply;
}
}
}
@@ -0,0 +1,115 @@
// 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);
}

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) || p.ExitCode == 1 || p.ExitCode == 2)
{
return CreateTimedOutPingReply();
}

try
{
string output = p.StandardOutput.ReadToEnd();
return ParsePingUtilityOutput(address, output);
}
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)
{
using (Process p = GetPingProcess(address, buffer, timeout, options))
{
var processCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
p.EnableRaisingEvents = true;
p.Exited += (s, e) => processCompletion.SetResult();
p.Start();

try
{
await processCompletion.Task.WaitAsync(TimeSpan.FromMilliseconds(timeout)).ConfigureAwait(false);
}
catch (TimeoutException)
{
p.Kill();
return CreateTimedOutPingReply();
}

if (p.ExitCode == 1 || p.ExitCode == 2)
{
// Throw timeout for known failure return codes from ping functions.
return CreateTimedOutPingReply();
}

try
{
string output = await p.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
return ParsePingUtilityOutput(address, output);
}
catch (Exception)
{
// If the standard output cannot be successfully parsed, throw a generic PingException.
throw new PingException(SR.net_ping);
}
}
}

private PingReply ParsePingUtilityOutput(IPAddress address, string output)
{
long rtt = UnixCommandLinePing.ParseRoundTripTime(output);
return new PingReply(
address,
null, // Ping utility cannot accommodate these, return null to indicate they were ignored.
IPStatus.Success,
rtt,
Array.Empty<byte>()); // Ping utility doesn't deliver this info.
}
}
}

0 comments on commit e98614e

Please sign in to comment.