From aa4c736675e60ce769bed07b8d79a4ab6f6248f9 Mon Sep 17 00:00:00 2001 From: Eric Eilebrecht Date: Thu, 1 Sep 2016 15:27:43 -0700 Subject: [PATCH] Rework ExclusiveAddressUse and ReuseAddress on non-Windows platforms The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket. This is only needed on Windows because it otherwise (at least in some versions/configurations) allows any socket with the ReuseAddress option set to "steal" the address from a socket that did not have *any* options set. On Unix, we previously treated this as an "unsupported" option. However, it is recommended to set this option to "true" on Windows, to avoid malicious theft of a service's address, so we need to support the option, in some fashion, on Unix, so that it's possible to write portable code that works reliably everywhere. Since the *only* behavior on Linux/OSX is equivalent to "ExclusiveAddressUse=true" on Windows, we implement this option as a no-op if it's set to "true," and as an unsupported option if set to "false." (It's possible that we could come up with a better failure for the "false" case, but I'm treating it as "unsupported" for compatiblity with the 1.0 release). Another related option is ReuseAddress. On Windows, this option allows a socket's address *and* port to be reused. It's equivalent to *two* native options on Unix: SO_REUSEADDR and SO_REUSEPORT. Again, for portability, we need an option that will work roughly the same way on all platforms. We could introduce a new option (ReuseAddressAndPort?) but existing code is already using the current ReuseAddress option. So this change makes ReuseAddress set both SO_REUSEADDR and SO_REUSEPORT on Unix. If we need to support these options individually, on Unix only, in the future, we'll need to introduce two new options (maybe ReuseAddressOnly and ReusePort) which will likely need to be treated as "unsupported" options on Windows. For now, no need for managed support for more fine-grained options has been demonstrated. For more information on the underlying native options on Windows, Linux, OSX, etc., see this great writeup [on stackoverflow](http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t). Also, the Windows docs [discuss the SO_EXCLUSIVEADDR option](https://msdn.microsoft.com/en-us/library/windows/desktop/cc150667(v=vs.85).aspx) in depth. --- .../Unix/System.Native/pal_networking.cpp | 86 +++++++++++++++++++ .../Unix/System.Native/pal_networking.h | 2 +- .../FunctionalTests/SocketOptionNameTest.cs | 60 +++++++++++++ .../tests/FunctionalTests/TcpClientTest.cs | 31 +++++-- 4 files changed, 172 insertions(+), 7 deletions(-) diff --git a/src/Native/Unix/System.Native/pal_networking.cpp b/src/Native/Unix/System.Native/pal_networking.cpp index cc13da3b2e53..16c0f87f8d4f 100644 --- a/src/Native/Unix/System.Native/pal_networking.cpp +++ b/src/Native/Unix/System.Native/pal_networking.cpp @@ -2071,6 +2071,51 @@ extern "C" Error SystemNative_GetSockOpt( int fd = ToFileDescriptor(socket); + // + // Handle some special cases for compatibility with Windows + // + if (socketOptionLevel == PAL_SOL_SOCKET) + { + if (socketOptionName == PAL_SO_EXCLUSIVEADDRUSE) + { + // + // SO_EXCLUSIVEADDRUSE makes Windows behave like Unix platforms do WRT the SO_REUSEADDR option. + // So, for non-Windows platforms, we act as if SO_EXCLUSIVEADDRUSE is always enabled. + // + if (*optionLen != sizeof(int32_t)) + { + return PAL_EINVAL; + } + + *reinterpret_cast(optionValue) = 1; + return PAL_SUCCESS; + } + else if (socketOptionName == PAL_SO_REUSEADDR) + { + // + // On Windows, SO_REUSEADDR allows the address *and* port to be reused. It's equivalent to + // SO_REUSEADDR + SO_REUSEPORT other systems. Se we only return "true" if both of those options are true. + // + auto optLen = static_cast(*optionLen); + + int err = getsockopt(fd, SOL_SOCKET, SO_REUSEADDR, optionValue, &optLen); + + if (err == 0 && *reinterpret_cast(optionValue) != 0) + { + err = getsockopt(fd, SOL_SOCKET, SO_REUSEPORT, optionValue, &optLen); + } + + if (err != 0) + { + return SystemNative_ConvertErrorPlatformToPal(errno); + } + + assert(optLen <= static_cast(*optionLen)); + *optionLen = static_cast(optLen); + return PAL_SUCCESS; + } + } + int optLevel, optName; if (!TryGetPlatformSocketOption(socketOptionLevel, socketOptionName, optLevel, optName)) { @@ -2099,6 +2144,47 @@ SystemNative_SetSockOpt(intptr_t socket, int32_t socketOptionLevel, int32_t sock int fd = ToFileDescriptor(socket); + // + // Handle some special cases for compatibility with Windows + // + if (socketOptionLevel == PAL_SOL_SOCKET) + { + if (socketOptionName == PAL_SO_EXCLUSIVEADDRUSE) + { + // + // SO_EXCLUSIVEADDRUSE makes Windows behave like Unix platforms do WRT the SO_REUSEADDR option. + // So, on Unix platforms, we consider SO_EXCLUSIVEADDRUSE to always be set. We allow manually setting this + // to "true", but not "false." + // + if (optionLen != sizeof(int32_t)) + { + return PAL_EINVAL; + } + + if (*reinterpret_cast(optionValue) == 0) + { + return PAL_ENOTSUP; + } + else + { + return PAL_SUCCESS; + } + } + else if (socketOptionName == PAL_SO_REUSEADDR) + { + // + // On Windows, SO_REUSEADDR allows the address *and* port to be reused. It's equivalent to + // SO_REUSEADDR + SO_REUSEPORT other systems. + // + int err = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, optionValue, static_cast(optionLen)); + if (err != 0) + { + err = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, optionValue, static_cast(optionLen)); + } + return err == 0 ? PAL_SUCCESS : SystemNative_ConvertErrorPlatformToPal(errno); + } + } + int optLevel, optName; if (!TryGetPlatformSocketOption(socketOptionLevel, socketOptionName, optLevel, optName)) { diff --git a/src/Native/Unix/System.Native/pal_networking.h b/src/Native/Unix/System.Native/pal_networking.h index 0e5497c319a3..7f5456153f83 100644 --- a/src/Native/Unix/System.Native/pal_networking.h +++ b/src/Native/Unix/System.Native/pal_networking.h @@ -141,7 +141,7 @@ enum SocketOptionName : int32_t PAL_SO_LINGER = 0x0080, PAL_SO_OOBINLINE = 0x0100, // PAL_SO_DONTLINGER = ~PAL_SO_LINGER, - // PAL_SO_EXCLUSIVEADDRUSE = ~PAL_SO_REUSEADDR, + PAL_SO_EXCLUSIVEADDRUSE = ~PAL_SO_REUSEADDR, PAL_SO_SNDBUF = 0x1001, PAL_SO_RCVBUF = 0x1002, PAL_SO_SNDLOWAT = 0x1003, diff --git a/src/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs b/src/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs index 4d4f503f05fd..b38894067766 100644 --- a/src/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs +++ b/src/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs @@ -219,5 +219,65 @@ private static Socket CreateBoundUdpSocket(out int localPort) localPort = (receiveSocket.LocalEndPoint as IPEndPoint).Port; return receiveSocket; } + + [Theory] + [InlineData(null, null, null, true)] + [InlineData(null, null, false, true)] + [InlineData(null, false, false, true)] + [InlineData(null, true, false, true)] + [InlineData(null, true, true, false)] + [InlineData(true, null, null, true)] + [InlineData(true, null, false, true)] + [InlineData(true, null, true, true)] + [InlineData(true, false, null, true)] + [InlineData(true, false, false, true)] + [InlineData(true, false, true, true)] + public void ReuseAddress(bool? exclusiveAddressUse, bool? firstSocketReuseAddress, bool? secondSocketReuseAddress, bool expectFailure) + { + using (Socket a = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + { + if (exclusiveAddressUse.HasValue) + { + a.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, exclusiveAddressUse.Value); + } + if (firstSocketReuseAddress.HasValue) + { + a.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, firstSocketReuseAddress.Value); + } + + a.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + + using (Socket b = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + { + if (secondSocketReuseAddress.HasValue) + { + b.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, secondSocketReuseAddress.Value); + } + + if (expectFailure) + { + Assert.ThrowsAny(() => b.Bind(a.LocalEndPoint)); + } + else + { + b.Bind(a.LocalEndPoint); + } + } + } + } + + [Theory] + [PlatformSpecific(PlatformID.Windows)] + [InlineData(false, null, null, true)] + [InlineData(false, null, false, true)] + [InlineData(false, false, null, true)] + [InlineData(false, false, false, true)] + [InlineData(false, true, null, true)] + [InlineData(false, true, false, true)] + [InlineData(false, true, true, false)] + public void ReuseAddress_Windows(bool? exclusiveAddressUse, bool? firstSocketReuseAddress, bool? secondSocketReuseAddress, bool expectFailure) + { + ReuseAddress(exclusiveAddressUse, firstSocketReuseAddress, secondSocketReuseAddress, expectFailure); + } } } diff --git a/src/System.Net.Sockets/tests/FunctionalTests/TcpClientTest.cs b/src/System.Net.Sockets/tests/FunctionalTests/TcpClientTest.cs index 8dd73940727d..4caa410dd051 100644 --- a/src/System.Net.Sockets/tests/FunctionalTests/TcpClientTest.cs +++ b/src/System.Net.Sockets/tests/FunctionalTests/TcpClientTest.cs @@ -84,7 +84,7 @@ public void ConnectedAvailable_NullClient() [Fact] [PlatformSpecific(PlatformID.Windows)] - public void ExclusiveAddressUse_NullClient() + public void ExclusiveAddressUse_NullClient_Windows() { using (TcpClient client = new TcpClient()) { @@ -95,13 +95,33 @@ public void ExclusiveAddressUse_NullClient() } [Fact] - [PlatformSpecific(PlatformID.Windows)] - public void Roundtrip_ExclusiveAddressUse_GetEqualsSet() + [PlatformSpecific(~PlatformID.Windows)] + public void ExclusiveAddressUse_NullClient_NonWindows() + { + using (TcpClient client = new TcpClient()) + { + client.Client = null; + + Assert.True(client.ExclusiveAddressUse); + } + } + + [Fact] + public void Roundtrip_ExclusiveAddressUse_GetEqualsSet_True() { using (TcpClient client = new TcpClient()) { client.ExclusiveAddressUse = true; Assert.True(client.ExclusiveAddressUse); + } + } + + [Fact] + [PlatformSpecific(PlatformID.Windows)] + public void Roundtrip_ExclusiveAddressUse_GetEqualsSet_False() + { + using (TcpClient client = new TcpClient()) + { client.ExclusiveAddressUse = false; Assert.False(client.ExclusiveAddressUse); } @@ -109,14 +129,13 @@ public void Roundtrip_ExclusiveAddressUse_GetEqualsSet() [Fact] [PlatformSpecific(PlatformID.AnyUnix)] - public void ExclusiveAddressUse_NotSupported() + public void ExclusiveAddressUse_Set_False_NotSupported() { using (TcpClient client = new TcpClient()) { - Assert.Throws(() => client.ExclusiveAddressUse); Assert.Throws(() => { - client.ExclusiveAddressUse = true; + client.ExclusiveAddressUse = false; }); } }