mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-24 11:01:55 +08:00
313 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|