Files

313 lines
13 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.PubSub.Transport
{
/// <summary>
/// Specialized in creating the necessary <see cref="UdpClient"/> instances from an URL
/// </summary>
internal static class UdpClientCreator
{
public const int SIO_UDP_CONNRESET = -1744830452;
/// <summary>
/// Parse the url into an IPaddress and port number
/// </summary>
/// <returns>A new instance of <see cref="IPEndPoint"/> or null if invalid URL.</returns>
internal static IPEndPoint? GetEndPoint(string url, ILogger logger)
{
if (url != null && Uri.TryCreate(url, UriKind.Absolute, out Uri? connectionUri))
{
if (connectionUri.Scheme != Utils.UriSchemeOpcUdp)
{
logger.LogError(
"Invalid Scheme specified in URL: {Url}",
url);
return null;
}
if (connectionUri.Port <= 0)
{
logger.LogError("Invalid Port specified in URL: {Url}", url);
return null;
}
string hostName = connectionUri.Host;
if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase))
{
hostName = "127.0.0.1";
}
if (IPAddress.TryParse(hostName, out IPAddress? ipAddress))
{
return new IPEndPoint(ipAddress, connectionUri.Port);
}
try
{
IPHostEntry hostEntry = Dns.GetHostEntry(hostName);
//you might get more than one IP for a hostname since
//DNS supports more than one record
foreach (IPAddress address in hostEntry.AddressList)
{
if (address.AddressFamily == AddressFamily.InterNetwork)
{
return new IPEndPoint(address, connectionUri.Port);
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Could not resolve host name: {Name}", hostName);
}
}
return null;
}
/// <summary>
/// Creates and returns a list of <see cref="UdpClient"/> created based on configuration options
/// </summary>
/// <param name="pubSubContext">Is the method called in a publisher context or a subscriber context</param>
/// <param name="networkInterface">The configured network interface name.</param>
/// <param name="configuredEndpoint">The configured <see cref="IPEndPoint"/> that will be used for data exchange.</param>
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
/// <param name="logger">A contextual logger to log to</param>
internal static List<UdpClient> GetUdpClients(
UsedInContext pubSubContext,
string networkInterface,
IPEndPoint configuredEndpoint,
ITelemetryContext telemetry,
ILogger logger)
{
logger.LogInformation(
"networkAddressUrl.NetworkInterface = {NetworkInterface} \nconfiguredEndpoint = {ConfiguredEndpoint}",
networkInterface, configuredEndpoint);
var udpClients = new List<UdpClient>();
//validate input parameters
if (configuredEndpoint == null)
{
//log warning?
return udpClients;
}
//detect the list on network interfaces that will be used for creating the UdpClient s
var usableNetworkInterfaces = new List<NetworkInterface>();
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
if (string.IsNullOrEmpty(networkInterface))
{
logger.LogInformation(
"No NetworkInterface name was provided. Use all available NICs.");
usableNetworkInterfaces.AddRange(interfaces);
}
else
{
//the configuration contains a NetworkInterface name, try to locate it
foreach (NetworkInterface nic in interfaces)
{
if (nic.Name.Equals(networkInterface, StringComparison.OrdinalIgnoreCase))
{
usableNetworkInterfaces.Add(nic);
}
}
if (usableNetworkInterfaces.Count == 0)
{
logger.LogInformation(
"The configured value for NetworkInterface name('{Name}') could not be used.",
networkInterface);
usableNetworkInterfaces.AddRange(interfaces);
}
}
foreach (NetworkInterface nic in usableNetworkInterfaces)
{
logger.LogInformation(
"NetworkInterface name('{Name}') attempts to create instance of UdpClient.",
nic.Name);
if ((nic.NetworkInterfaceType == NetworkInterfaceType.Loopback) ||
(nic.NetworkInterfaceType == NetworkInterfaceType.Tunnel) ||
(nic.OperationalStatus != OperationalStatus.Up))
{
//ignore loop-back interface
//ignore tunnel interface
//ignore not operational interface
continue;
}
UdpClient? udpClient = CreateUdpClientForNetworkInterface(
pubSubContext,
nic,
configuredEndpoint,
telemetry,
logger);
#pragma warning disable CA1508 // Avoid dead conditional code
if (udpClient == null)
{
continue;
}
#pragma warning restore CA1508 // Avoid dead conditional code
//store UdpClient
udpClients.Add(udpClient);
logger.LogInformation(
"NetworkInterface name('{Name}') UdpClient successfully created.",
nic.Name);
}
return udpClients;
}
/// <summary>
/// Create specific <see cref="UdpClient"/> for specified <see cref="NetworkInterface"/> and <see cref="IPEndPoint"/>.
/// </summary>
/// <param name="pubSubContext">Is the method called in a publisher context or a subscriber context</param>
/// <param name="networkInterface">The network interface</param>
/// <param name="configuredEndpoint">The configured IP endpoint to use</param>
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
/// <param name="logger">A contextual logger to log to</param>
private static UdpClient? CreateUdpClientForNetworkInterface(
UsedInContext pubSubContext,
NetworkInterface networkInterface,
IPEndPoint configuredEndpoint,
ITelemetryContext telemetry,
ILogger logger)
{
UdpClient? udpClient = null;
IPInterfaceProperties ipProps = networkInterface.GetIPProperties();
IPAddress localAddress = IPAddress.Any;
foreach (UnicastIPAddressInformation address in ipProps.UnicastAddresses)
{
if (address.Address.AddressFamily == AddressFamily.InterNetwork)
{
localAddress = address.Address;
}
}
try
{
//detect the port used for binding
int port = 0;
if (pubSubContext is UsedInContext.Subscriber or UsedInContext.Discovery)
{
port = configuredEndpoint.Port;
}
if (IsIPv4MulticastAddress(configuredEndpoint.Address))
{
//instantiate multi-cast UdpClient
udpClient = new UdpClientMulticast(
localAddress,
configuredEndpoint.Address,
port,
telemetry);
}
else if (IsIPv4BroadcastAddress(configuredEndpoint.Address, networkInterface))
{
//instantiate broadcast UdpClient depending on publisher/subscriber usage context
udpClient = new UdpClientBroadcast(localAddress, port, pubSubContext, telemetry);
}
else
{
//instantiate unicast UdpClient depending on publisher/subscriber usage context
udpClient = new UdpClientUnicast(localAddress, port, telemetry);
}
if (pubSubContext is UsedInContext.Publisher or UsedInContext.Discovery)
{
//try to send 1 byte for target IP
udpClient.Send([0], 1, configuredEndpoint);
}
// On Windows Only since Linux does not support this
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Disable exceptions raised by ICMP Port Unreachable messages
udpClient.Client
.IOControl((IOControlCode)SIO_UDP_CONNRESET, [0, 0, 0, 0], null);
}
}
catch (Exception ex)
{
logger.LogError(ex,
"Cannot use Network interface '{Name}'.",
networkInterface.Name);
//cleanup
udpClient?.Dispose();
udpClient = null;
}
return udpClient;
}
/// <summary>
/// Checks if the address provided is an IPv4 multicast address
/// </summary>
private static bool IsIPv4MulticastAddress(IPAddress address)
{
if (address == null)
{
return false;
}
byte[] bytes = address.GetAddressBytes();
return bytes[0] is >= 224 and <= 239;
}
/// <summary>
/// Checks if the address provided is an IPv4 broadcast address
/// </summary>
private static bool IsIPv4BroadcastAddress(
IPAddress address,
NetworkInterface networkInterface)
{
IPInterfaceProperties ipProps = networkInterface.GetIPProperties();
foreach (UnicastIPAddressInformation localUnicastAddress in ipProps.UnicastAddresses)
{
if (localUnicastAddress.Address.AddressFamily == AddressFamily.InterNetwork)
{
byte[] subnetMask = localUnicastAddress.IPv4Mask.GetAddressBytes();
uint addressBits = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
uint invertedSubnetBits = ~BitConverter.ToUInt32(subnetMask, 0);
bool isBroadcast = (addressBits & invertedSubnetBits) == invertedSubnetBits;
if (isBroadcast)
{
return true;
}
}
}
return false;
}
}
}