File: System\Net\Sockets\SafeSocketHandle.Unix.cs
Web Access
Project: src\src\libraries\System.Net.Sockets\src\System.Net.Sockets.csproj (System.Net.Sockets)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;
 
namespace System.Net.Sockets
{
    public partial class SafeSocketHandle
    {
        private int _receiveTimeout = -1;
        private int _sendTimeout = -1;
        private bool _nonBlocking;
        private SocketAsyncContext? _asyncContext;
 
        private TrackedSocketOptions _trackedOptions;
        internal bool LastConnectFailed { get; set; }
        internal bool DualMode { get; set; }
        internal bool ExposedHandleOrUntrackedConfiguration { get; private set; }
        internal bool PreferInlineCompletions { get; set; } = SocketAsyncEngine.InlineSocketCompletionsEnabled;
        internal bool IsSocket { get; set; } = true; // (ab)use Socket class for performing async I/O on non-socket fds.
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
        internal bool TfoEnabled { get; set; }
#endif
        internal void RegisterConnectResult(SocketError error)
        {
            switch (error)
            {
                case SocketError.Success:
                case SocketError.WouldBlock:
                    break;
                default:
                    LastConnectFailed = true;
                    break;
            }
        }
 
        internal void TransferTrackedState(SafeSocketHandle target)
        {
            target._trackedOptions = _trackedOptions;
            target.LastConnectFailed = LastConnectFailed;
            target.DualMode = DualMode;
            target.ExposedHandleOrUntrackedConfiguration = ExposedHandleOrUntrackedConfiguration;
            target.IsSocket = IsSocket;
#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM
            target.TfoEnabled = TfoEnabled;
#endif
        }
 
        internal void SetExposed() => ExposedHandleOrUntrackedConfiguration = true;
 
        internal bool IsTrackedOption(TrackedSocketOptions option) => (_trackedOptions & option) != 0;
 
        internal void TrackOption(SocketOptionLevel level, SocketOptionName name)
        {
            // As long as only these options are set, we can support Connect{Async}(IPAddress[], ...).
            switch (level)
            {
                case SocketOptionLevel.Tcp:
                    switch (name)
                    {
                        case SocketOptionName.NoDelay: _trackedOptions |= TrackedSocketOptions.NoDelay; return;
                    }
                    break;
 
                case SocketOptionLevel.IP:
                    switch (name)
                    {
                        case SocketOptionName.DontFragment: _trackedOptions |= TrackedSocketOptions.DontFragment; return;
                        case SocketOptionName.IpTimeToLive: _trackedOptions |= TrackedSocketOptions.Ttl; return;
                    }
                    break;
 
                case SocketOptionLevel.IPv6:
                    switch (name)
                    {
                        case SocketOptionName.IPv6Only: _trackedOptions |= TrackedSocketOptions.DualMode; return;
                        case SocketOptionName.IpTimeToLive: _trackedOptions |= TrackedSocketOptions.Ttl; return;
                    }
                    break;
 
                case SocketOptionLevel.Socket:
                    switch (name)
                    {
                        case SocketOptionName.Broadcast: _trackedOptions |= TrackedSocketOptions.EnableBroadcast; return;
                        case SocketOptionName.Linger: _trackedOptions |= TrackedSocketOptions.LingerState; return;
                        case SocketOptionName.ReceiveBuffer: _trackedOptions |= TrackedSocketOptions.ReceiveBufferSize; return;
                        case SocketOptionName.ReceiveTimeout: _trackedOptions |= TrackedSocketOptions.ReceiveTimeout; return;
                        case SocketOptionName.SendBuffer: _trackedOptions |= TrackedSocketOptions.SendBufferSize; return;
                        case SocketOptionName.SendTimeout: _trackedOptions |= TrackedSocketOptions.SendTimeout; return;
                    }
                    break;
            }
 
            // For any other settings, we need to track that they were used so that we can error out
            // if a Connect{Async}(IPAddress[],...) attempt is made.
            ExposedHandleOrUntrackedConfiguration = true;
        }
 
        internal SocketAsyncContext AsyncContext =>
            _asyncContext ??
            Interlocked.CompareExchange(ref _asyncContext, new SocketAsyncContext(this), null) ??
            _asyncContext!;
 
        /// <summary>
        /// This represents whether the Socket instance is blocking or non-blocking *from the user's point of view*,
        /// i.e. it corresponds to the Socket.Blocking property (except in reverse).
        /// Even if this is false, the underlying native socket may still be non-blocking if anything ever caused it to become non-blocking,
        /// either by issuing an async operation or explicitly setting this property to true.
        /// </summary>
        internal bool IsNonBlocking
        {
            get
            {
                return _nonBlocking;
            }
            set
            {
                _nonBlocking = value;
 
                // If transitioning from blocking to non-blocking, we need to set the native socket to non-blocking mode.
                // If transitioning from non-blocking to blocking, we keep the native socket in non-blocking mode, and emulate
                // blocking operations within SocketAsyncContext on top of epoll/kqueue.
                // This avoids problems with switching to native blocking while there are pending operations.
                if (value)
                {
                    AsyncContext.SetHandleNonBlocking();
                }
            }
        }
 
        internal bool IsUnderlyingHandleBlocking => !AsyncContext.IsHandleNonBlocking;
 
        internal int ReceiveTimeout
        {
            get
            {
                return _receiveTimeout;
            }
            set
            {
                Debug.Assert(value == -1 || value > 0, $"Unexpected value: {value}");
                _receiveTimeout = value;
            }
        }
 
        internal int SendTimeout
        {
            get
            {
                return _sendTimeout;
            }
            set
            {
                Debug.Assert(value == -1 || value > 0, $"Unexpected value: {value}");
                _sendTimeout = value;
            }
        }
 
        internal bool IsDisconnected { get; private set; }
 
        internal void SetToDisconnected()
        {
            IsDisconnected = true;
        }
 
        /// <returns>Returns whether operations were canceled.</returns>
        private bool OnHandleClose()
        {
            // If we've aborted async operations, return true to cause an abortive close.
            return _asyncContext?.StopAndAbort() ?? false;
        }
 
        /// <returns>Returns whether operations were canceled.</returns>
        private unsafe bool TryUnblockSocket(bool abortive)
        {
            // Calling 'close' on a socket that has pending blocking calls (e.g. recv, send, accept, ...)
            // may block indefinitely. This is a best-effort attempt to not get blocked and make those operations return.
            // We need to ensure we keep the expected TCP behavior that is observed by the socket peer (FIN vs RST close).
            // What we do here isn't specified by POSIX and doesn't work on all OSes.
            // On Linux this works well.
            // On OSX, TCP connections will be closed with a FIN close instead of an abortive RST close.
            // And, pending TCP connect operations and UDP receive are not abortable.
 
            // Don't disconnect sockets we don't own.
            if (!OwnsHandle)
            {
                return false;
            }
 
            // Unless we're doing an abortive close, don't touch sockets which don't have the CLOEXEC flag set.
            // These may be shared with other processes and we want to avoid disconnecting them.
            if (!abortive)
            {
                int fdFlags = Interop.Sys.Fcntl.GetFD(handle);
                if (fdFlags == 0)
                {
                    return false;
                }
            }
 
            int type = 0;
            int optLen = sizeof(int);
            Interop.Error err = Interop.Sys.GetSockOpt(handle, SocketOptionLevel.Socket, SocketOptionName.Type, (byte*)&type, &optLen);
            if (err == Interop.Error.SUCCESS)
            {
                // For TCP (SocketType.Stream), perform an abortive close.
                // Unless the user requested a normal close using Socket.Shutdown.
                if (type == (int)SocketType.Stream && !_hasShutdownSend)
                {
                    Interop.Sys.Disconnect(handle);
                }
                else
                {
                    Interop.Sys.Shutdown(handle, SocketShutdown.Both);
                }
            }
 
            return true;
        }
 
        private unsafe SocketError DoCloseHandle(bool abortive)
        {
            Interop.Error errorCode = Interop.Error.SUCCESS;
 
            if (!IsSocket)
            {
                return SocketPal.GetSocketErrorForErrorCode(CloseHandle(handle));
            }
            if (OperatingSystem.IsWasi())
            {
                // WASI never blocks and doesn't support linger options
                return SocketPal.GetSocketErrorForErrorCode(CloseHandle(handle));
            }
 
            // If abortive is not set, we're not running on the finalizer thread, so it's safe to block here.
            // We can honor the linger options set on the socket.  It also means closesocket() might return
            // EWOULDBLOCK, in which case we need to do some recovery.
            if (!abortive)
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"handle:{handle} Following 'non-abortive' branch.");
 
                // Close, and if its errno is other than EWOULDBLOCK, there's nothing more to do - we either succeeded or failed.
                errorCode = CloseHandle(handle);
                if (errorCode != Interop.Error.EWOULDBLOCK)
                {
                    return SocketPal.GetSocketErrorForErrorCode(errorCode);
                }
 
                // The socket must be non-blocking with a linger timeout set.
                // We have to set the socket to blocking.
                if (Interop.Sys.Fcntl.DangerousSetIsNonBlocking(handle, 0) == 0)
                {
                    // The socket successfully made blocking; retry the close().
                    return SocketPal.GetSocketErrorForErrorCode(CloseHandle(handle));
                }
 
                // The socket could not be made blocking; fall through to the regular abortive close.
            }
 
            // By default or if the non-abortive path failed, set linger timeout to zero to get an abortive close (RST).
            var linger = new Interop.Sys.LingerOption
            {
                OnOff = 1,
                Seconds = 0
            };
 
            errorCode = Interop.Sys.SetLingerOption(handle, &linger);
#if DEBUG
            _closeSocketLinger = SocketPal.GetSocketErrorForErrorCode(errorCode);
#endif
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"handle:{handle}, setsockopt():{errorCode}");
 
            switch (errorCode)
            {
                case Interop.Error.SUCCESS:
                case Interop.Error.EINVAL:
                case Interop.Error.ENOPROTOOPT:
                case Interop.Error.ENOTSOCK:
                    errorCode = CloseHandle(handle);
                    break;
 
                // For other errors, it's too dangerous to try closesocket() - it might block!
            }
 
            return SocketPal.GetSocketErrorForErrorCode(errorCode);
        }
 
        private Interop.Error CloseHandle(IntPtr handle)
        {
            Interop.Error errorCode = Interop.Error.SUCCESS;
            bool remappedError = false;
 
            if (Interop.Sys.Close(handle) != 0)
            {
                errorCode = Interop.Sys.GetLastError();
                if (errorCode == Interop.Error.ECONNRESET)
                {
                    // Some Unix platforms (e.g. FreeBSD) non-compliantly return ECONNRESET from close().
                    // For our purposes, we want to ignore such a "failure" and treat it as success.
                    // In such a case, the file descriptor was still closed and there's no corrective
                    // action to take.
                    errorCode = Interop.Error.SUCCESS;
                    remappedError = true;
                }
            }
 
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, remappedError ?
                    $"handle:{handle}, close():ECONNRESET, but treating it as SUCCESS" :
                    $"handle:{handle}, close():{errorCode}");
            }
 
#if DEBUG
            _closeSocketResult = SocketPal.GetSocketErrorForErrorCode(errorCode);
#endif
 
            return errorCode;
        }
    }
 
    /// <summary>Flags that correspond to exposed options on Socket.</summary>
    [Flags]
    internal enum TrackedSocketOptions : short
    {
        DontFragment = 0x1,
        DualMode = 0x2,
        EnableBroadcast = 0x4,
        LingerState = 0x8,
        NoDelay = 0x10,
        ReceiveBufferSize = 0x20,
        ReceiveTimeout = 0x40,
        SendBufferSize = 0x80,
        SendTimeout = 0x100,
        Ttl = 0x200,
    }
}