Description
Summary
On the Windows command line, while sending a ping, one is able to specify the "source address" via the '-S' parameter, as show in the example below:
PS C:\> ping 192.168.1.2 -S 192.168.3.2 -w 120000
Pinging 192.168.1.2 from 192.168.3.2 with 32 bytes of data:
Reply from 192.168.1.2: bytes=32 time=7056ms TTL=126
Reply from 192.168.1.2: bytes=32 time=7855ms TTL=126
Reply from 192.168.1.2: bytes=32 time=7753ms TTL=126
Reply from 192.168.1.2: bytes=32 time=7866ms TTL=126
Ping statistics for 192.168.1.2:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 7056ms, Maximum = 7866ms, Average = 7632ms
On Linux, it appears as it behaves similarly, where the command is ping -S sourceIp destIp
The source address allows one to specify which network interface to send the ping out of.
This is incredilby useful when doing network testing, where there are multiple networks attached to a test server.
However, C#'s Ping class doesn't seem to be able to set a source address.
This means that if we are on Windows, for example, one needs to call the IcmpSendEcho2Ex
function in iphlpapi.dll and provide it the source address that way.
Its not the end-of-the-world to do that, but it would be nice if one could configure the source address in the C# layer so one doesn't have to invoke native methods.
Proposed Api Changes
Specifying the Source Address
There are 2 possible ways to get the Source address down to the function that actually performs the Ping, either add more functions to Ping.cs that have source IPAddress as
a parameter, or tack it on to PingOptions.
Of the two, I feel like adding another property to PingOptions is probably the better of the two. For one, if more functions were added, that's 8 new functions to write to get all of the different flavors to include a source address (that's not counting the async ones). Second, PingOptions is already passed down deep into the Ping classes, having to also pass a source address around that deep sounds like it will make a mess of the existing API.
So with that, PingOptions would get a new Property that is of type IpAddress. By default, it could be set to null. If it is null, it means the user does NOT want to specify a source address, while if it IS specified, it means the user would like to specify one.
Changes to the PingOptions class could be as simple adding one line of code:
public IpAddress SourceAddress { get; set; }
and the usage can be:
PingOptions options = new PingOptions{ SourceAddress = IpAddress.Parse( "192.168.1.1" ) };
Open Questions:
- There are two constructors in PingOptions right now. There is a default one, and one that specifies both the TTL and DontFragment. Does it make sense to add a third one that specifies all 3, or don't bother with adding one, and to set the SourceAddress at construction, use the example specified above?
Windows Changes
Right now, Ping.Windows.cs uses
IcmpSendEcho2 to send an Ipv4 ping. However, IcmpSendEcho2 does not
allow one to specify the source IP Address. A similar function, IcmpSendEcho2Ex,
does, however. As far as I can tell, the functions perform the same thing, except IcmpSendEcho2Ex allows one to specify a source address.
The Windows implementation would have to call the different function if the source address is not null from PingOptions. If it is null, call the old function.
if (!_ipv6)
{
if(pingOptions.SourceAddress == null)
{
return (int)Interop.IpHlpApi.IcmpSendEcho2(
_handlePingV4,
GetWaitHandle(isAsync),
IntPtr.Zero,
IntPtr.Zero,
#pragma warning disable CS0618 // Address is marked obsolete
(uint)address.Address,
#pragma warning restore CS0618
_requestBuffer,
(ushort)buffer.Length,
ref ipOptions,
_replyBuffer,
MaxUdpPacket,
(uint)timeout);
}
else
{
return (int)Interop.IpHlpApi.IcmpSendEcho2Ex(
_handlePingV4,
GetWaitHandle(isAsync),
IntPtr.Zero,
IntPtr.Zero,
#pragma warning disable CS0618 // Address is marked obsolete
(uint)sourceAddress.Address,
(uint)address.Address,
#pragma warning restore CS0618
_requestBuffer,
(ushort)buffer.Length,
ref ipOptions,
_replyBuffer,
MaxUdpPacket,
(uint)timeout);
}
}
For IPv6, it looks like Ping uses the native function Icmp6SendEcho2.
At the moment, the source address is just a byte array of size 28. If the source IP address is null, this can stay as is, but if the source address is specified, pass that in. An example could be:
IPEndPoint ep = new IPEndPoint(address, 0);
Internals.SocketAddress remoteAddr = IPEndPointExtensions.Serialize(ep);
// Start New Code
byte[] sourceAddrBuffer;
if (options.SourceAddress == null)
{
sourceAddrBuffer = new byte[28];
}
else
{
IPEndPoint sourceEp = new IPEndPoint(options.SourceAddress, 0);
Internals.SocketAddress sourceAddr = IPEndPointExtensions.Serialize(sourceEp);
sourceAddrBuffer = sourceAddr.Buffer;
}
// End New Code
return (int)Interop.IpHlpApi.Icmp6SendEcho2(
_handlePingV6,
GetWaitHandle(isAsync),
IntPtr.Zero,
IntPtr.Zero,
sourceAddrBuffer,
remoteAddr.Buffer,
_requestBuffer,
(ushort)buffer.Length,
ref ipOptions,
_replyBuffer,
MaxUdpPacket,
(uint)timeout);
Windows Uap Changes
It doesn't look like anything would have to change in Ping.Windows.Uap.cs since SendPingCore just throws a PlatformNotSupportedException.
Unix Changes
Inside of Ping.Unix.cs, it looks like if the process is running as root (or the user has permission), a raw socket is used, otherwise the "ping" command is called as a subprocess.
When ping is being called from the command line, "-S sourceAddress" needs to be added to the arguments. UnixCommandLinePing.ConstructCommandLine would have to be modified to take an optional SourceAddress, probably defaulted to null.
public static string ConstructCommandLine(int packetSize, string address, bool ipv4, int ttl = 0, PingFragmentOptions fragmentOption = PingFragmentOptions.Default, IpAddress sourceAddress = null)
{
StringBuilder sb = new StringBuilder();
sb.Append("-c 1"); // Just send a single ping ("count = 1")
// Start new code
if(sourceAddress != null)
{
sb.Append( " -S " + sourceAddress.ToString() );
}
// End new code
// ...
return sb.ToString();
}
The only thing to be mindful of is this: is "-S" the same for all the unix platforms?
When using a raw socket, the socket needs to be told about the source address. To be honest, this is where my networking knowledge is hazy, but I think the only thing that needs to be set on the Socket object to do this is Socket.LocalEndPoint. Which means, GetRawSocket can be modified to look like:
private Socket GetRawSocket(SocketConfig socketConfig)
{
IPEndPoint ep = (IPEndPoint)socketConfig.EndPoint;
// Setting Socket.DontFragment and .Ttl is not supported on Unix, so socketConfig.Options is ignored.
AddressFamily addrFamily = ep.Address.AddressFamily;
Socket socket = new Socket(addrFamily, SocketType.Raw, socketConfig.ProtocolType);
socket.ReceiveTimeout = socketConfig.Timeout;
socket.SendTimeout = socketConfig.Timeout;
if (socketConfig.Options != null && socketConfig.Options.Ttl > 0)
{
socket.Ttl = (short)socketConfig.Options.Ttl;
}
if (socketConfig.Options != null && addrFamily == AddressFamily.InterNetwork)
{
socket.DontFragment = socketConfig.Options.DontFragment;
}
// Start new code
if (socketConfig.Options != null && socketConfig.Options.SourceAddress != null)
{
IPEndPoint sourceEp = new IPEndPoint(socketConfig.Options.SourceAddress, 0);
socket.LocalEndPoint = sourceEp;
}
// End new code
// ...
return socket;