mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
## Summary Fixes #4285 — on macOS the `Opc.Ua.Tools.Tests` PubSub runtime-tool tests failed with `SocketException` ("Socket is already connected" on the publisher side, "No route to host" on the subscriber side) when using unicast loopback UDP (`opc.udp://127.0.0.1:{port}`). ## Root cause `UdpDatagramTransport.BindForUnicast` bound sockets to `IPAddress.Any` (`0.0.0.0` / `::`) and then either `Connect`'d or `SendTo`'d a loopback destination. macOS (Darwin) is stricter than Windows/Linux about routing loopback traffic from a wildcard-bound socket: - **Publisher (connected send):** `Connect(127.0.0.1)` after `Bind(0.0.0.0:0)` throws `EISCONN` ("Socket is already connected"). - **Subscriber (receive / bidirectional):** binding to `0.0.0.0` and exchanging datagrams with `127.0.0.1` fails with "No route to host". ## Fix In `src/Opc.Ua.PubSub.Udp/UdpDatagramTransport.cs`: - Add `SelectUnicastBindAddress()` — when the endpoint address is a loopback address, bind to the matching loopback address (`127.0.0.1` / `::1`) so traffic is routed over the loopback interface. Non-loopback endpoints keep binding to the wildcard address so datagrams can still arrive on any interface (no behavior change off loopback). - Guard `socket.Connect(...)` against `SocketError.IsConnected` (EISCONN) and treat it as a benign already-connected socket (logged at Debug), as suggested in the issue. This is platform-agnostic: on Windows/Linux the loopback bind behaves identically, and on macOS it removes both socket errors. ## Testing - `dotnet build src/Opc.Ua.PubSub.Udp` — clean (0 warnings, 0 errors). - `dotnet test tests/Opc.Ua.PubSub.Udp.Tests` (`UdpDatagramTransportUnicastTests`) — the existing loopback unicast publish/receive integration tests pass on net10.0. macOS validation still relies on CI (change authored on Windows), but the binding change directly targets the reported Darwin behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -558,7 +558,9 @@ namespace Opc.Ua.PubSub.Udp
|
||||
ReadOnlyMemory<byte> payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (socket.AddressFamily == AddressFamily.InterNetwork)
|
||||
if (socket.AddressFamily == AddressFamily.InterNetwork &&
|
||||
socket.LocalEndPoint is IPEndPoint localEndPoint &&
|
||||
!IPAddress.IsLoopback(localEndPoint.Address))
|
||||
{
|
||||
await SendOnceAsync(
|
||||
socket,
|
||||
@@ -569,6 +571,9 @@ namespace Opc.Ua.PubSub.Udp
|
||||
return;
|
||||
}
|
||||
|
||||
// A socket bound to loopback cannot route the standard multicast discovery endpoint
|
||||
// on Windows. Use a wildcard-bound IPv4 socket for discovery while retaining the
|
||||
// loopback binding that unicast data traffic requires on macOS.
|
||||
using Socket discoverySocket = new(
|
||||
AddressFamily.InterNetwork,
|
||||
SocketType.Dgram,
|
||||
@@ -941,27 +946,63 @@ namespace Opc.Ua.PubSub.Udp
|
||||
|
||||
private void BindForUnicast(Socket socket)
|
||||
{
|
||||
IPAddress localAddress = SelectUnicastBindAddress();
|
||||
if ((HasSendDirection && !HasReceiveDirection) || (m_useConnectedUnicastClient && HasSendDirection))
|
||||
{
|
||||
EndPoint bindEndPoint = Endpoint.Address.AddressFamily == AddressFamily.InterNetworkV6
|
||||
? new IPEndPoint(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPoint(IPAddress.Any, 0);
|
||||
socket.Bind(bindEndPoint);
|
||||
socket.Bind(new IPEndPoint(localAddress, 0));
|
||||
IPEndPoint remote = new(Endpoint.Address, Endpoint.Port);
|
||||
socket.Connect(remote);
|
||||
try
|
||||
{
|
||||
socket.Connect(remote);
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.IsConnected)
|
||||
{
|
||||
if (socket.RemoteEndPoint is IPEndPoint existing && existing.Equals(remote))
|
||||
{
|
||||
// macOS (Darwin) can report EISCONN when connecting a datagram socket
|
||||
// that the OS already associated with the destination. The socket is
|
||||
// connected to the intended peer, so treat this as success.
|
||||
if (m_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
m_logger.UdpUnicastAlreadyConnected(ex, m_connection.Name, remote.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
m_sendDestination = remote;
|
||||
m_socketIsConnected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EndPoint bindEndPoint = Endpoint.Address.AddressFamily == AddressFamily.InterNetworkV6
|
||||
? new IPEndPoint(IPAddress.IPv6Any, Endpoint.Port)
|
||||
: new IPEndPoint(IPAddress.Any, Endpoint.Port);
|
||||
socket.Bind(bindEndPoint);
|
||||
socket.Bind(new IPEndPoint(localAddress, Endpoint.Port));
|
||||
m_sendDestination = new IPEndPoint(Endpoint.Address, Endpoint.Port);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the local address a unicast socket binds to. Loopback endpoints bind to the
|
||||
/// matching loopback address so that traffic is routed over the loopback interface. On
|
||||
/// macOS binding to <see cref="IPAddress.Any"/> and then sending to / connecting a loopback
|
||||
/// destination fails with "No route to host" / "Socket is already connected"; binding to the
|
||||
/// loopback address avoids both. Non-loopback endpoints keep binding to the wildcard address
|
||||
/// so datagrams can arrive on any interface.
|
||||
/// </summary>
|
||||
private IPAddress SelectUnicastBindAddress()
|
||||
{
|
||||
if (Endpoint.Address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return IPAddress.IsLoopback(Endpoint.Address)
|
||||
? IPAddress.IPv6Loopback
|
||||
: IPAddress.IPv6Any;
|
||||
}
|
||||
return IPAddress.IsLoopback(Endpoint.Address)
|
||||
? IPAddress.Loopback
|
||||
: IPAddress.Any;
|
||||
}
|
||||
|
||||
private void JoinMulticastGroup(Socket socket)
|
||||
{
|
||||
if (Endpoint.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
@@ -1338,6 +1379,14 @@ namespace Opc.Ua.PubSub.Udp
|
||||
this ILogger logger,
|
||||
Exception exception,
|
||||
string? connection);
|
||||
|
||||
[LoggerMessage(EventId = PubSubUdpEventIds.UdpDatagramTransport + 18, Level = LogLevel.Debug,
|
||||
Message = "Unicast socket for connection '{Connection}' already connected to {Endpoint}; continuing.")]
|
||||
public static partial void UdpUnicastAlreadyConnected(
|
||||
this ILogger logger,
|
||||
Exception exception,
|
||||
string? connection,
|
||||
string endpoint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user