//******************************************************************************************************
// Transport.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/01/2006 - Pinal C. Patel
// Original version of source created.
// 09/29/2008 - J. Ritchie Carroll
// Converted to C#.
// 08/22/2009 - Pinal C. Patel
// Modified CreateEndPoint() to try parsing IP address first before doing a DNS lookup.
// 09/08/2009 - Pinal C. Patel
// Modified CreateSocket() to create a socket for the AddressFamily of the endpoint.
// Modified CreateEndPoint() to use IPv6 if supported when no IP address is specified.
// 09/14/2009 - Stephen C. Wills
// Added new header and license agreement.
// 10/30/2009 - Pinal C. Patel
// Added IsIPv6IP() and IsMulticastIP() methods.
// Fixed issue in CreateSocket() that was breaking one-way communication support in UDP components.
// 04/29/2010 - Pinal C. Patel
// Added EndpointFormatRegex constant to be used for parsing endpoint strings.
// 08/18/2011 - J. Ritchie Carroll
// Multiple additions and updates to accomodate easier IPv6 or IPv4 selection as well as
// dual-mode socket support.
// 09/21/2011 - J. Ritchie Carroll
// Added Mono implementation exception regions.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using GSF.Diagnostics;
namespace GSF.Communication
{
#region [ Enumerations ]
///
/// IP stack enumeration.
///
public enum IPStack
{
///
/// IPv6 stack.
///
///
/// Requests to use IPv6 stack if possible.
///
IPv6,
///
/// IPv4 stack.
///
///
/// Requests to use IPv4 stack if possible.
///
IPv4,
///
/// Default stack.
///
///
/// Requests to use the default OS IP stack.
///
Default
}
#endregion
///
/// Defines helper methods related to IP socket based communications.
///
public static class Transport
{
///
/// Specifies the lowest valid port number for a .
///
public const int PortRangeLow = 0;
///
/// Specifies the highest valid port number for a .
///
public const int PortRangeHigh = 65535;
///
/// Regular expression used to validate the format for an endpoint.
///
///
///
/// Matches the following valid input:
/// - localhost:80
/// - 127.0.0.1:80
/// - [::1]:80
/// - [FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80
///
///
public const string EndpointFormatRegex = @"(?.+)\:(?\d+$)";
///
/// Creates an for the specified host name and port number.
///
/// The host name or IP address to resolve.
/// The port number to be associated with the address.
/// Desired IP stack to use.
/// An object.
public static IPEndPoint CreateEndPoint(string hostNameOrAddress, int port, IPStack stack)
{
// Determine system's default IP stack if the default stack was requested
if (stack == IPStack.Default)
stack = GetDefaultIPStack();
// Make sure system can support specified stack
if (stack == IPStack.IPv6 && !Socket.OSSupportsIPv6)
throw new NotSupportedException($"IPv6 stack is not available for socket creation on {hostNameOrAddress.ToNonNullNorWhiteSpace("localhost")}:{port}");
#if !MONO
if (stack == IPStack.IPv4 && !Socket.OSSupportsIPv4)
throw new NotSupportedException($"IPv4 stack is not available for socket creation on {hostNameOrAddress.ToNonNullNorWhiteSpace("localhost")}:{port}");
#endif
// No host name or IP address was specified, use local IPs
if (string.IsNullOrWhiteSpace(hostNameOrAddress))
return stack == IPStack.IPv6 ? new IPEndPoint(IPAddress.IPv6Any, port) : new IPEndPoint(IPAddress.Any, port);
bool ipStackMismatch = false;
// Attempt to parse provided address name as a literal IP address
if (IPAddress.TryParse(hostNameOrAddress, out IPAddress address))
{
// As long as desired IP stack matches format of specified IP address, return end point for address
if (stack == IPStack.IPv6 && address.AddressFamily == AddressFamily.InterNetworkV6 ||
stack == IPStack.IPv4 && address.AddressFamily == AddressFamily.InterNetwork)
return new IPEndPoint(address, port);
// User specified an IP address that is mismatch with the desired IP stack. If the DNS server
// responds to this IP, we can attempt to see if an IP is defined for the desired IP stack,
// otherwise this is an exception
ipStackMismatch = true;
}
try
{
// Handle "localhost" as a special case, returning proper loopback address for the desired IP stack
if (string.Compare(hostNameOrAddress, "localhost", StringComparison.OrdinalIgnoreCase) == 0)
return new IPEndPoint(stack == IPStack.IPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, port);
// Failed to parse an IP address for the desired stack - this may simply be that a host name was provided
// so we attempt a DNS lookup. Note that exceptions will occur if DNS lookup fails.
IPAddress[] dnsAddressList = Dns.GetHostEntry(hostNameOrAddress).AddressList;
if (dnsAddressList.Length > 0)
{
// Traverse address list looking for first match on desired IP stack
foreach (IPAddress dnsAddress in dnsAddressList)
{
if (stack == IPStack.IPv6 && dnsAddress.AddressFamily == AddressFamily.InterNetworkV6 ||
stack == IPStack.IPv4 && dnsAddress.AddressFamily == AddressFamily.InterNetwork)
return new IPEndPoint(dnsAddress, port);
}
// If no available matching address was found for desired IP stack, this is an IP stack mismatch
ipStackMismatch = true;
}
throw new InvalidOperationException($"No valid {stack} addresses could be found for \"{hostNameOrAddress}\"");
}
catch
{
// Spell out a specific error message for IP stack mismatches
if (ipStackMismatch)
throw new InvalidOperationException($"IP address mismatch: unable to find an {stack} address for \"{hostNameOrAddress}\"");
// Otherwise report original exception
throw;
}
}
///
/// Creates a for the specified and .
///
/// The local address where the will be bound.
/// The port number at which the will be bound.
/// One of the values.
/// Desired IP stack to use.
/// Determines if dual-mode socket is allowed when endpoint address is IPv6.
/// An object.
public static Socket CreateSocket(string address, int port, ProtocolType protocol, IPStack stack, bool allowDualStackSocket = true)
{
Socket socket;
IPEndPoint endpoint;
switch (protocol)
{
case ProtocolType.Tcp:
endpoint = CreateEndPoint(address, port, stack);
socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
#if !MONO
// If allowDualModeSocket is true and the endpoint is IPv6, we setup a dual-mode socket
// by setting the IPv6Only socket option to false
if (allowDualStackSocket && endpoint.AddressFamily == AddressFamily.InterNetworkV6 && Environment.OSVersion.Version.Major > 5)
socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
#endif
// Associate the socket with the local endpoint
socket.Bind(endpoint);
break;
case ProtocolType.Udp:
// Allow negative port number to be specified for unbound socket.
if (port >= 0)
{
endpoint = CreateEndPoint(address, port, stack);
socket = new Socket(endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
#if !MONO
// If allowDualModeSocket is true and the endpoint is IPv6, we setup a dual-mode socket
// by setting the IPv6Only socket option to false
if (allowDualStackSocket && endpoint.AddressFamily == AddressFamily.InterNetworkV6 && Environment.OSVersion.Version.Major > 5)
socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
#endif
socket.Bind(endpoint);
}
else
{
// Create a socket with no binding when -1 is used for port number
endpoint = CreateEndPoint(address, 0, stack);
socket = new Socket(endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
}
break;
default:
throw new NotSupportedException($"Communications library does not support socket creation for protocol {protocol}");
}
return socket;
}
///
/// Determines if an IP address (or DNS name) is a local IP address.
///
/// DNS name or IP address to test.
/// true if is a local IP address; otherwise false.
/// An error is encountered when resolving .
/// is an invalid IP address.
/// The length of is greater than 255 characters.
/// is null.
public static bool IsLocalAddress(string hostNameOrAddress)
{
IPAddress[] hostIPs = Dns.GetHostAddresses(hostNameOrAddress);
IEnumerable localIPs = Dns.GetHostAddresses("localhost").Concat(Dns.GetHostAddresses(Dns.GetHostName()));
// Check to see if entered host name corresponds to a local IP address
return hostIPs.Any(localIPs.Contains);
}
///
/// Gets the default IP stack for this system.
///
///
/// System's assumed default IP stack.
///
public static IPStack GetDefaultIPStack()
{
try
{
IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName());
// IP's are normally ordered with default IP stack first
if (hostEntry.AddressList.Length > 0)
return hostEntry.AddressList[0].AddressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6 ? IPStack.IPv6 : IPStack.IPv4;
}
catch (Exception ex)
{
Logger.SwallowException(ex, "DNS lookup failure for local machine");
}
// If default stack cannot be determined, assume IPv4
return IPStack.IPv4;
}
///
/// Derives the desired from the "interface" setting in the connection string key/value pairs.
/// If interface is not specified, is derived from the server value in connectionStringEntries
///
/// Connection string key/value pairs.
/// Desired based on "interface" setting.
///
/// The "interface" setting will be added to the if it
/// doesn't exist. in this case the return value will be based off of the "server" value. If the server parameter
/// doesn't exist, the return value will be .
///
public static IPStack GetInterfaceIPStack(Dictionary connectionStringEntries)
{
if (connectionStringEntries.TryGetValue("interface", out string ipAddress))
return IsIPv6IP(ipAddress) ? IPStack.IPv6 : IPStack.IPv4;
connectionStringEntries.Add("interface", string.Empty);
if (connectionStringEntries.TryGetValue("server", out ipAddress))
return IsIPv6IP(ipAddress) ? IPStack.IPv6 : IPStack.IPv4;
return IPStack.Default;
}
///
/// Determines if the specified is an IPv6 IP.
///
/// IP address to check.
/// true if the is IPv6 IP; otherwise false.
public static bool IsIPv6IP(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentNullException(nameof(ipAddress));
if (IPAddress.TryParse(ipAddress, out IPAddress address))
return address.AddressFamily == AddressFamily.InterNetworkV6;
return false;
}
///
/// Determines if the specified is a multicast IP.
///
/// IP address to check.
/// true if the is multicast IP; otherwise false.
public static bool IsMulticastIP(IPAddress ipAddress)
{
if (ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
// Check for IPv6
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
return ipAddress.IsIPv6Multicast;
// IP is IPv4, check octet for multicast range
int firstOctet = int.Parse(ipAddress.ToString().Split('.')[0]);
// Check first octet to see if IP is a Class D multicast IP
return firstOctet >= 224 && firstOctet <= 247;
}
///
/// Determines whether the specified port is valid.
///
/// The port number to be validated.
/// True if the port number is valid.
public static bool IsPortNumberValid(string port)
{
// Check to see if the specified port is a valid integer value
if (!int.TryParse(port, out int portNumber))
throw new ArgumentException("Specified port is not a valid number");
// Check to see if the port number is within the valid range
return portNumber >= PortRangeLow && portNumber <= PortRangeHigh;
}
///
/// Determines if the specified UDP destination is listening for data.
///
/// The for the UDP destination to be checked.
/// true if the UDP destination is listening for data; otherwise false.
public static bool IsDestinationReachable(IPEndPoint targetIPEndPoint)
{
try
{
// Check if the target endpoint exists by sending empty data to it and waiting for a response,
// if the endpoint doesn't exist then we'll receive a ConnectionReset socket exception
EndPoint targetEndPoint = targetIPEndPoint;
using (Socket targetChecker = new Socket(targetIPEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp))
{
targetChecker.ReceiveTimeout = 1;
targetChecker.SendTo(Array.Empty(), targetEndPoint);
targetChecker.ReceiveFrom(Array.Empty(), ref targetEndPoint);
}
}
catch (SocketException ex)
{
// Connection reset means that the target endpoint is unreachable
if (ex.SocketErrorCode == SocketError.ConnectionReset)
return false;
}
catch (Exception ex)
{
// We'll ignore any other exceptions we might encounter and assume destination is reachable
Logger.SwallowException(ex, "Failed while checking if IP end point destination is reachable via UDP");
}
return true;
}
}
}