diff --git a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs index 41cdd601dc5611..a326a9f5cc36e5 100644 --- a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs @@ -12,9 +12,7 @@ internal static partial class SocketProtocolSupportPal private static unsafe bool IsSupported(AddressFamily af) { - // Check for AF_UNIX on iOS/tvOS. The OS claims to support this, but returns EPERM on bind. - // We should explicitly set the return here to false, to avoid giving a false impression. - if (af == AddressFamily.Unix && (OperatingSystem.IsTvOS() || OperatingSystem.IsWasi() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()))) + if (af == AddressFamily.Unix && OperatingSystem.IsWasi()) { return false; } diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Sockets.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Sockets.cs new file mode 100644 index 00000000000000..c3ea492de8c4e0 --- /dev/null +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Sockets.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Net.Sockets; +using System.Text; + +namespace System +{ + public static partial class PlatformDetection + { + private static readonly Lazy s_unixDomainSocketDirectory = new Lazy(GetUnixDomainSocketDirectory); + private static readonly Lazy s_supportsUnixDomainSocketBinding = new Lazy(GetSupportsUnixDomainSocketBinding); + + public static string UnixDomainSocketDirectory => s_unixDomainSocketDirectory.Value; + public static bool SupportsUnixDomainSocketBinding => s_supportsUnixDomainSocketBinding.Value; + + private static string GetUnixDomainSocketDirectory() + { + string directory = Path.GetTempPath(); +#if !NETFRAMEWORK + if (IsiOS || IstvOS) + { + // Simulator app container paths can exceed the native socket path limit. + string relativeDirectory = Path.GetRelativePath(Environment.CurrentDirectory, directory); + if (Encoding.UTF8.GetByteCount(relativeDirectory) < Encoding.UTF8.GetByteCount(directory)) + { + directory = relativeDirectory; + } + } +#endif + return directory; + } + + private static bool GetSupportsUnixDomainSocketBinding() + { +#if NETFRAMEWORK + return false; +#else + if (!Socket.OSSupportsUnixDomainSockets) + { + return false; + } + + if (IsiOS || IstvOS) + { + string path = Path.Combine(UnixDomainSocketDirectory, Guid.NewGuid().ToString("N").Substring(0, 8)); + using Socket socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + try + { + socket.Bind(new UnixDomainSocketEndPoint(path)); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AccessDenied) + { + // Filesystem socket binding can be denied by the test application's sandbox. + return false; + } + } + + return true; +#endif + } + } +} diff --git a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj index e29f6f24e3633a..ba8963659c58a8 100644 --- a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj +++ b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index fb66bb6c6f6955..cf713a4d3e96fb 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -4772,7 +4772,7 @@ public async Task ConnectCallback_UseMemoryBuffer_Success(bool useSsl) await new[] { serverTask, clientTask }.WhenAllOrAnyFailed(60_000); } - [ConditionalTheory(typeof(SocketsHttpHandlerTest_ConnectCallback), nameof(PlatformSupportsUnixDomainSockets))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [InlineData(true)] [InlineData(false)] public async Task ConnectCallback_UseUnixDomainSocket_Success(bool useSsl) @@ -4780,7 +4780,13 @@ public async Task ConnectCallback_UseUnixDomainSocket_Success(bool useSsl) GenericLoopbackOptions options = new GenericLoopbackOptions() { UseSsl = useSsl }; string guid = $"{Guid.NewGuid():N}"; - string socketPath = Path.Combine(Path.GetTempPath(), guid); + string socketDirectory = PlatformDetection.UnixDomainSocketDirectory; + if (PlatformDetection.IsiOS || PlatformDetection.IstvOS) + { + // Keep the name short for app container paths, and avoid an all-numeric URI host. + guid = "s" + guid.Substring(0, 7); + } + string socketPath = Path.Combine(socketDirectory, guid); UnixDomainSocketEndPoint serverEP = new UnixDomainSocketEndPoint(socketPath); using Socket listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); listenSocket.Bind(serverEP); @@ -4793,7 +4799,7 @@ public async Task ConnectCallback_UseUnixDomainSocket_Success(bool useSsl) socketsHandler.ConnectCallback = async (context, token) => { string hostname = context.DnsEndPoint.Host; - UnixDomainSocketEndPoint clientEP = new UnixDomainSocketEndPoint(Path.Combine(Path.GetTempPath(), hostname)); + UnixDomainSocketEndPoint clientEP = new UnixDomainSocketEndPoint(Path.Combine(socketDirectory, hostname)); Socket clientSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); await clientSocket.ConnectAsync(clientEP); @@ -5227,8 +5233,6 @@ public async Task ConnectCallback_UseNamedPipe_Success(bool useSsl) await TestHelper.WhenAllCompletedOrAnyFailedWithTimeout(GenericLoopbackServer.LoopbackServerTimeoutMilliseconds, clientTask, serverTask); } - private static bool PlatformSupportsUnixDomainSockets => Socket.OSSupportsUnixDomainSockets; - private sealed class ReadAheadStream : DelegatingStream { private readonly ITestOutputHelper _output; diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs index f3faf63b036df1..79f3c06a6d4c5c 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs @@ -151,6 +151,11 @@ private unsafe Socket(SafeSocketHandle handle, bool loadPropertiesFromHandle) Debug.Assert(bufferLength <= buffer.Length); + if (_addressFamily == AddressFamily.Unknown) + { + _addressFamily = SocketAddressPal.GetAddressFamily(buffer.Slice(0, bufferLength)); + } + // Try to get the local end point. That will in turn enable the remote // end point to be retrieved on-demand when the property is accessed. switch (_addressFamily) diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs index 31b4314a804982..9f155b7a2d477c 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs @@ -25,7 +25,23 @@ public UnixDomainSocketTest(ITestOutputHelper output) _log = output; } - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [Fact] + [PlatformSpecific(TestPlatforms.iOS | TestPlatforms.tvOS)] + public void Socket_OSSupportsUnixDomainSockets_OnAppleMobile() + { + Assert.True(Socket.OSSupportsUnixDomainSockets); + + using Socket socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + UnixDomainSocketEndPoint endPoint = new("socket"); + Assert.Equal(AddressFamily.Unix, socket.AddressFamily); + Assert.Equal(AddressFamily.Unix, endPoint.AddressFamily); + Assert.Equal("socket", endPoint.ToString()); + + using Socket clone = new Socket(socket.SafeHandle); + Assert.Equal(AddressFamily.Unix, clone.AddressFamily); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] public async Task Socket_ConnectAsyncUnixDomainSocketEndPoint_Success() { @@ -74,7 +90,7 @@ public async Task Socket_ConnectAsyncUnixDomainSocketEndPoint_Success() } finally { - server.Dispose(); + server?.Dispose(); Assert.False(File.Exists(path)); } @@ -118,7 +134,7 @@ public async Task Socket_ConnectAsyncUnixDomainSocketEndPoint_NotServer() } } - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] public void Socket_SendReceive_Success() { @@ -150,7 +166,7 @@ public void Socket_SendReceive_Success() Assert.False(File.Exists(path)); } - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] public void Socket_SendReceive_Clone_Success() { @@ -171,8 +187,14 @@ public void Socket_SendReceive_Clone_Success() _log.WriteLine($"accepted: LocalEndPoint={accepted.LocalEndPoint} RemoteEndPoint={accepted.RemoteEndPoint}"); _log.WriteLine($"acceptedClone: LocalEndPoint={acceptedClone.LocalEndPoint} RemoteEndPoint={acceptedClone.RemoteEndPoint}"); + Assert.Equal(AddressFamily.Unix, clientClone.AddressFamily); + Assert.Equal(AddressFamily.Unix, acceptedClone.AddressFamily); Assert.True(clientClone.Connected); Assert.True(acceptedClone.Connected); + Assert.NotNull(clientClone.LocalEndPoint); + Assert.NotNull(clientClone.RemoteEndPoint); + Assert.NotNull(acceptedClone.LocalEndPoint); + Assert.NotNull(acceptedClone.RemoteEndPoint); Assert.Equal(client.LocalEndPoint.ToString(), clientClone.LocalEndPoint.ToString()); Assert.Equal(client.RemoteEndPoint.ToString(), clientClone.RemoteEndPoint.ToString()); Assert.Equal(accepted.LocalEndPoint.ToString(), acceptedClone.LocalEndPoint.ToString()); @@ -195,7 +217,7 @@ public void Socket_SendReceive_Clone_Success() Assert.False(File.Exists(path)); } - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] public async Task Socket_SendReceiveAsync_Success() { @@ -228,7 +250,7 @@ public async Task Socket_SendReceiveAsync_Success() } [ActiveIssue("https://github.com/dotnet/runtime/issues/26189", TestPlatforms.Windows)] - [ConditionalTheory(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [InlineData(5000, 1, 1)] [InlineData(500, 18, 21)] [InlineData(500, 21, 18)] @@ -285,7 +307,7 @@ public async Task Socket_SendReceiveAsync_PropagateToStream_Success(int iteratio Assert.False(File.Exists(path)); } - [ConditionalTheory(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [ActiveIssue("https://github.com/dotnet/runtime/issues/26189", TestPlatforms.Windows)] [InlineData(false)] [InlineData(true)] @@ -331,7 +353,7 @@ public async Task ConcurrentSendReceive(bool forceNonBlocking) } } - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] public async Task ConcurrentSendReceiveAsync() { @@ -384,22 +406,24 @@ public void UnixDomainSocketEndPoint_InvalidPaths_Throws() Assert.Throws(() => new UnixDomainSocketEndPoint(invalidLengthString)); } - [ConditionalTheory(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] - [InlineData(false)] - [InlineData(true)] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] - public void UnixDomainSocketEndPoint_RemoteEndPointEqualsBindAddress(bool abstractAddress) + public void UnixDomainSocketEndPoint_RemoteEndPointEqualsBindAddress() => + RemoteEndPointEqualsBindAddress(abstractAddress: false); + + [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [PlatformSpecific(TestPlatforms.Linux)] + [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] + public void UnixDomainSocketEndPoint_AbstractRemoteEndPointEqualsBindAddress() => + RemoteEndPointEqualsBindAddress(abstractAddress: true); + + private static void RemoteEndPointEqualsBindAddress(bool abstractAddress) { string serverAddress; string clientAddress; string expectedClientAddress; if (abstractAddress) { - // abstract socket addresses are a Linux feature. - if (!OperatingSystem.IsLinux()) - { - return; - } // An abstract socket address starts with a zero byte. serverAddress = '\0' + Guid.NewGuid().ToString(); clientAddress = '\0' + Guid.NewGuid().ToString() + "ABC"; @@ -538,7 +562,7 @@ public void FilePathEquality() Assert.NotEqual(endPoint2, endPoint3); } - [ConditionalTheory(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsUnixDomainSocketBinding))] [ActiveIssue("https://github.com/dotnet/runtime/issues/26189", TestPlatforms.Windows)] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] [InlineData(true)] @@ -639,11 +663,25 @@ public async Task UnixDomainSocket_Receive_GetsCanceledByDispose() internal static string GetRandomNonExistingFilePath() { + string directory = PlatformDetection.UnixDomainSocketDirectory; + bool isAppleMobile = PlatformDetection.IsiOS || PlatformDetection.IstvOS; + string result; do { - // get random name and append random number of characters to get variable name length. - result = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + new string('A', Random.Shared.Next(1, 32))); + string fileName = Path.GetRandomFileName(); + if (isAppleMobile) + { + // App container paths are long; leave room for the socket name and test-specific suffixes. + fileName = fileName.Substring(0, 8); + } + else + { + // Append a random number of characters to get variable name length. + fileName += new string('A', Random.Shared.Next(1, 32)); + } + + result = Path.Combine(directory, fileName); } while (File.Exists(result));