< Summary

Information
Class: Renci.SshNet.ForwardedPortDynamic
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\ForwardedPortDynamic.cs
Line coverage
75%
Covered lines: 293
Uncovered lines: 96
Coverable lines: 389
Total lines: 753
Line coverage: 75.3%
Branch coverage
64%
Covered branches: 73
Total branches: 113
Branch coverage: 64.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
get_BoundHost()100%1100%
get_BoundPort()100%1100%
get_IsStarted()100%1100%
.ctor(...)100%1100%
.ctor(...)100%1100%
StartPort()50%250%
StopPort(...)100%2100%
CheckDisposed()100%1100%
Dispose(...)100%2100%
InternalStart()100%2100%
StartAccept(...)50%1064%
AcceptCompleted(...)87.5%869.23%
ProcessAccept(...)100%490.62%
InitializePendingChannelCountdown()100%2100%
HandleSocks(...)75%880%
closeClientSocket()100%1100%
CloseClientSocket(...)100%275%
StopListener()75%4100%
InternalStop(...)50%257.14%
InternalDispose(...)100%6100%
Session_Disconnected(...)0%20%
Session_ErrorOccured(...)100%2100%
Channel_Exception(...)100%10%
HandleSocks4(...)50%1068.75%
HandleSocks5(...)50%2256.6%
GetSocks5Host(...)38.46%1351.72%
CreateSocks5Reply(...)50%290%
ReadString(...)75%487.5%
Finalize()100%1100%

File(s)

\home\appveyor\projects\ssh-net\src\Renci.SshNet\ForwardedPortDynamic.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.Linq;
 4using System.Net;
 5using System.Net.Sockets;
 6using System.Text;
 7using System.Threading;
 8
 9using Renci.SshNet.Abstractions;
 10using Renci.SshNet.Channels;
 11using Renci.SshNet.Common;
 12
 13namespace Renci.SshNet
 14{
 15    /// <summary>
 16    /// Provides functionality for forwarding connections from the client to destination servers via the SSH server,
 17    /// also known as dynamic port forwarding.
 18    /// </summary>
 19    public class ForwardedPortDynamic : ForwardedPort
 20    {
 21        private ForwardedPortStatus _status;
 22
 23        /// <summary>
 24        /// Holds a value indicating whether the current instance is disposed.
 25        /// </summary>
 26        /// <value>
 27        /// <see langword="true"/> if the current instance is disposed; otherwise, <see langword="false"/>.
 28        /// </value>
 29        private bool _isDisposed;
 30
 31        /// <summary>
 32        /// Gets the bound host.
 33        /// </summary>
 41034        public string BoundHost { get; }
 35
 36        /// <summary>
 37        /// Gets the bound port.
 38        /// </summary>
 22839        public uint BoundPort { get; }
 40
 41        private Socket _listener;
 42        private CountdownEvent _pendingChannelCountdown;
 43
 44        /// <summary>
 45        /// Gets a value indicating whether port forwarding is started.
 46        /// </summary>
 47        /// <value>
 48        /// <see langword="true"/> if port forwarding is started; otherwise, <see langword="false"/>.
 49        /// </value>
 50        public override bool IsStarted
 51        {
 307552            get { return _status == ForwardedPortStatus.Started; }
 53        }
 54
 55        /// <summary>
 56        /// Initializes a new instance of the <see cref="ForwardedPortDynamic"/> class.
 57        /// </summary>
 58        /// <param name="port">The port.</param>
 59        public ForwardedPortDynamic(uint port)
 4260            : this(string.Empty, port)
 4261        {
 4262        }
 63
 64        /// <summary>
 65        /// Initializes a new instance of the <see cref="ForwardedPortDynamic"/> class.
 66        /// </summary>
 67        /// <param name="host">The host.</param>
 68        /// <param name="port">The port.</param>
 29669        public ForwardedPortDynamic(string host, uint port)
 29670        {
 29671            BoundHost = host;
 29672            BoundPort = port;
 29673            _status = ForwardedPortStatus.Stopped;
 29674        }
 75
 76        /// <summary>
 77        /// Starts local port forwarding.
 78        /// </summary>
 79        protected override void StartPort()
 22280        {
 22281            if (!ForwardedPortStatus.ToStarting(ref _status))
 082            {
 083                return;
 84            }
 85
 86            try
 22287            {
 22288                InternalStart();
 22289            }
 090            catch (Exception)
 091            {
 092                _status = ForwardedPortStatus.Stopped;
 093                throw;
 94            }
 22295        }
 96
 97        /// <summary>
 98        /// Stops local port forwarding, and waits for the specified timeout until all pending
 99        /// requests are processed.
 100        /// </summary>
 101        /// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
 102        protected override void StopPort(TimeSpan timeout)
 337103        {
 337104            if (!ForwardedPortStatus.ToStopping(ref _status))
 115105            {
 115106                return;
 107            }
 108
 109            // signal existing channels that the port is closing
 222110            base.StopPort(timeout);
 111
 112            // prevent new requests from getting processed
 222113            StopListener();
 114
 115            // wait for open channels to close
 222116            InternalStop(timeout);
 117
 118            // mark port stopped
 222119            _status = ForwardedPortStatus.Stopped;
 337120        }
 121
 122        /// <summary>
 123        /// Ensures the current instance is not disposed.
 124        /// </summary>
 125        /// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
 126        protected override void CheckDisposed()
 276127        {
 128#if NET7_0_OR_GREATER
 186129            ObjectDisposedException.ThrowIf(_isDisposed, this);
 130#else
 90131            if (_isDisposed)
 3132            {
 3133                throw new ObjectDisposedException(GetType().FullName);
 134            }
 135#endif // NET7_0_OR_GREATER
 267136        }
 137
 138        /// <summary>
 139        /// Releases unmanaged and - optionally - managed resources.
 140        /// </summary>
 141        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 142        protected override void Dispose(bool disposing)
 385143        {
 385144            if (_isDisposed)
 101145            {
 101146                return;
 147            }
 148
 284149            base.Dispose(disposing);
 150
 284151            InternalDispose(disposing);
 284152            _isDisposed = true;
 385153        }
 154
 155        private void InternalStart()
 222156        {
 222157            InitializePendingChannelCountdown();
 158
 222159            var ip = IPAddress.Any;
 222160            if (!string.IsNullOrEmpty(BoundHost))
 182161            {
 182162                ip = DnsAbstraction.GetHostAddresses(BoundHost)[0];
 182163            }
 164
 222165            var ep = new IPEndPoint(ip, (int) BoundPort);
 166
 222167            _listener = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
 222168            _listener.Bind(ep);
 222169            _listener.Listen(5);
 170
 222171            Session.ErrorOccured += Session_ErrorOccured;
 222172            Session.Disconnected += Session_Disconnected;
 173
 174            // consider port started when we're listening for inbound connections
 222175            _status = ForwardedPortStatus.Started;
 176
 222177            StartAccept(e: null);
 222178        }
 179
 180        private void StartAccept(SocketAsyncEventArgs e)
 375181        {
 375182            if (e is null)
 222183            {
 184#pragma warning disable CA2000 // Dispose objects before losing scope
 222185                e = new SocketAsyncEventArgs();
 186#pragma warning restore CA2000 // Dispose objects before losing scope
 222187                e.Completed += AcceptCompleted;
 222188            }
 189            else
 153190            {
 191                // clear the socket as we're reusing the context object
 153192                e.AcceptSocket = null;
 153193            }
 194
 195            // only accept new connections while we are started
 375196            if (IsStarted)
 374197            {
 198                try
 374199                {
 374200                    if (!_listener.AcceptAsync(e))
 0201                    {
 0202                        AcceptCompleted(sender: null, e);
 0203                    }
 374204                }
 0205                catch (ObjectDisposedException)
 0206                {
 0207                    if (_status == ForwardedPortStatus.Stopping || _status == ForwardedPortStatus.Stopped)
 0208                    {
 209                        // ignore ObjectDisposedException while stopping or stopped
 0210                        return;
 211                    }
 212
 0213                    throw;
 214                }
 374215            }
 375216        }
 217
 218        private void AcceptCompleted(object sender, SocketAsyncEventArgs e)
 374219        {
 374220            if (e.SocketError is SocketError.OperationAborted or SocketError.NotSocket)
 221221            {
 222                // server was stopped
 221223                return;
 224            }
 225
 226            // capture client socket
 153227            var clientSocket = e.AcceptSocket;
 228
 153229            if (e.SocketError != SocketError.Success)
 0230            {
 231                // accept new connection
 0232                StartAccept(e);
 233
 234                // dispose broken client socket
 0235                CloseClientSocket(clientSocket);
 0236                return;
 237            }
 238
 239            // accept new connection
 153240            StartAccept(e);
 241
 242            // process connection
 153243            ProcessAccept(clientSocket);
 374244        }
 245
 246        private void ProcessAccept(Socket clientSocket)
 153247        {
 248            // close the client socket if we're no longer accepting new connections
 153249            if (!IsStarted)
 2250            {
 2251                CloseClientSocket(clientSocket);
 2252                return;
 253            }
 254
 255            // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
 256            // that same instance; the instance field for the countdown event is re-initialized when the port is restart
 257            // and at that time there may still be pending requests
 151258            var pendingChannelCountdown = _pendingChannelCountdown;
 259
 151260            pendingChannelCountdown.AddCount();
 261
 262            try
 151263            {
 151264                using (var channel = Session.CreateChannelDirectTcpip())
 151265                {
 151266                    channel.Exception += Channel_Exception;
 267
 151268                    if (!HandleSocks(channel, clientSocket, Session.ConnectionInfo.Timeout))
 54269                    {
 54270                        CloseClientSocket(clientSocket);
 54271                        return;
 272                    }
 273
 274                    // start receiving from client socket (and sending to server)
 79275                    channel.Bind();
 79276                }
 79277            }
 22278            catch (Exception exp)
 22279            {
 22280                RaiseExceptionEvent(exp);
 22281                CloseClientSocket(clientSocket);
 22282            }
 283            finally
 151284            {
 285                // take into account that CountdownEvent has since been disposed; when stopping the port we
 286                // wait for a given time for the channels to close, but once that timeout period has elapsed
 287                // the CountdownEvent will be disposed
 288                try
 151289                {
 151290                    _ = pendingChannelCountdown.Signal();
 151291                }
 0292                catch (ObjectDisposedException)
 0293                {
 294                    // Ignore any ObjectDisposedException
 0295                }
 151296            }
 153297        }
 298
 299        /// <summary>
 300        /// Initializes the <see cref="CountdownEvent"/>.
 301        /// </summary>
 302        /// <remarks>
 303        /// <para>
 304        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
 305        /// of <c>1</c>.
 306        /// </para>
 307        /// <para>
 308        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
 309        /// initial count of <c>1</c>.
 310        /// </para>
 311        /// </remarks>
 312        private void InitializePendingChannelCountdown()
 222313        {
 222314            var original = Interlocked.Exchange(ref _pendingChannelCountdown, new CountdownEvent(1));
 222315            original?.Dispose();
 222316        }
 317
 318        private bool HandleSocks(IChannelDirectTcpip channel, Socket clientSocket, TimeSpan timeout)
 151319        {
 151320            Closing += closeClientSocket;
 321
 322            try
 151323            {
 151324                var version = SocketAbstraction.ReadByte(clientSocket, timeout);
 119325                switch (version)
 326                {
 327                    case -1:
 328                        // SOCKS client closed connection
 22329                        return false;
 330                    case 4:
 75331                        return HandleSocks4(clientSocket, channel, timeout);
 332                    case 5:
 4333                        return HandleSocks5(clientSocket, channel, timeout);
 334                    default:
 18335                        throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "SOCKS version {0} i
 336                }
 337            }
 32338            catch (SocketException ex)
 32339            {
 340                // ignore exception thrown by interrupting the blocking receive as part of closing
 341                // the forwarded port
 342#if NETFRAMEWORK
 10343                if (ex.SocketErrorCode != SocketError.Interrupted)
 0344                {
 0345                    RaiseExceptionEvent(ex);
 0346                }
 347#else
 348                // Since .NET 5 the exception has been changed.
 349                // more info https://github.com/dotnet/runtime/issues/41585
 22350                if (ex.SocketErrorCode != SocketError.ConnectionAborted)
 0351                {
 0352                    RaiseExceptionEvent(ex);
 0353                }
 354#endif
 32355                return false;
 356            }
 357            finally
 151358            {
 359                // interrupt of blocking receive is now handled by channel (SOCKS4 and SOCKS5)
 360                // or no longer necessary
 151361                Closing -= closeClientSocket;
 151362            }
 363
 364#pragma warning disable SA1300 // Element should begin with upper-case letter
 365            void closeClientSocket(object sender, EventArgs args)
 32366            {
 32367                CloseClientSocket(clientSocket);
 32368            }
 369#pragma warning restore SA1300 // Element should begin with upper-case letter
 133370        }
 371
 372        private static void CloseClientSocket(Socket clientSocket)
 110373        {
 110374            if (clientSocket.Connected)
 74375            {
 376                try
 74377                {
 74378                    clientSocket.Shutdown(SocketShutdown.Send);
 74379                }
 0380                catch (Exception)
 0381                {
 382                    // ignore exception when client socket was already closed
 0383                }
 74384            }
 385
 110386            clientSocket.Dispose();
 110387        }
 388
 389        /// <summary>
 390        /// Interrupts the listener, and unsubscribes from <see cref="Session"/> events.
 391        /// </summary>
 392        private void StopListener()
 222393        {
 394            // close listener socket
 222395            _listener?.Dispose();
 396
 397            // unsubscribe from session events
 222398            var session = Session;
 222399            if (session is not null)
 222400            {
 222401                session.ErrorOccured -= Session_ErrorOccured;
 222402                session.Disconnected -= Session_Disconnected;
 222403            }
 222404        }
 405
 406        /// <summary>
 407        /// Waits for pending channels to close.
 408        /// </summary>
 409        /// <param name="timeout">The maximum time to wait for the pending channels to close.</param>
 410        private void InternalStop(TimeSpan timeout)
 222411        {
 222412            _ = _pendingChannelCountdown.Signal();
 413
 222414            if (!_pendingChannelCountdown.Wait(timeout))
 0415            {
 416                // TODO: log as warning
 0417                DiagnosticAbstraction.Log("Timeout waiting for pending channels in dynamic forwarded port to close.");
 0418            }
 222419        }
 420
 421        private void InternalDispose(bool disposing)
 284422        {
 284423            if (disposing)
 253424            {
 253425                var listener = _listener;
 253426                if (listener is not null)
 172427                {
 172428                    _listener = null;
 172429                    listener.Dispose();
 172430                }
 431
 253432                var pendingRequestsCountdown = _pendingChannelCountdown;
 253433                if (pendingRequestsCountdown is not null)
 172434                {
 172435                    _pendingChannelCountdown = null;
 172436                    pendingRequestsCountdown.Dispose();
 172437                }
 253438            }
 284439        }
 440
 441        private void Session_Disconnected(object sender, EventArgs e)
 0442        {
 0443            var session = Session;
 0444            if (session is not null)
 0445            {
 0446                StopPort(session.ConnectionInfo.Timeout);
 0447            }
 0448        }
 449
 450        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
 27451        {
 27452            var session = Session;
 27453            if (session is not null)
 27454            {
 27455                StopPort(session.ConnectionInfo.Timeout);
 27456            }
 27457        }
 458
 459        private void Channel_Exception(object sender, ExceptionEventArgs e)
 0460        {
 0461            RaiseExceptionEvent(e.Exception);
 0462        }
 463
 464        private bool HandleSocks4(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
 75465        {
 75466            var commandCode = SocketAbstraction.ReadByte(socket, timeout);
 75467            if (commandCode == -1)
 0468            {
 469                // SOCKS client closed connection
 0470                return false;
 471            }
 472
 75473            var portBuffer = new byte[2];
 75474            if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
 0475            {
 476                // SOCKS client closed connection
 0477                return false;
 478            }
 479
 75480            var port = Pack.BigEndianToUInt16(portBuffer);
 481
 75482            var ipBuffer = new byte[4];
 75483            if (SocketAbstraction.Read(socket, ipBuffer, 0, ipBuffer.Length, timeout) == 0)
 0484            {
 485                // SOCKS client closed connection
 0486                return false;
 487            }
 488
 75489            var ipAddress = new IPAddress(ipBuffer);
 490
 75491            var username = ReadString(socket, timeout);
 75492            if (username is null)
 0493            {
 494                // SOCKS client closed connection
 0495                return false;
 496            }
 497
 75498            var host = ipAddress.ToString();
 499
 75500            RaiseRequestReceived(host, port);
 501
 75502            channel.Open(host, port, this, socket);
 503
 75504            SocketAbstraction.SendByte(socket, 0x00);
 505
 75506            if (channel.IsOpen)
 75507            {
 75508                SocketAbstraction.SendByte(socket, 0x5a);
 75509                SocketAbstraction.Send(socket, portBuffer, 0, portBuffer.Length);
 75510                SocketAbstraction.Send(socket, ipBuffer, 0, ipBuffer.Length);
 75511                return true;
 512            }
 513
 514            // signal that request was rejected or failed
 0515            SocketAbstraction.SendByte(socket, 0x5b);
 0516            return false;
 75517        }
 518
 519        private bool HandleSocks5(Socket socket, IChannelDirectTcpip channel, TimeSpan timeout)
 4520        {
 4521            var authenticationMethodsCount = SocketAbstraction.ReadByte(socket, timeout);
 4522            if (authenticationMethodsCount == -1)
 0523            {
 524                // SOCKS client closed connection
 0525                return false;
 526            }
 527
 4528            var authenticationMethods = new byte[authenticationMethodsCount];
 4529            if (SocketAbstraction.Read(socket, authenticationMethods, 0, authenticationMethods.Length, timeout) == 0)
 0530            {
 531                // SOCKS client closed connection
 0532                return false;
 533            }
 534
 4535            if (authenticationMethods.Min() == 0)
 4536            {
 537                // no user authentication is one of the authentication methods supported
 538                // by the SOCKS client
 4539                SocketAbstraction.Send(socket, new byte[] { 0x05, 0x00 }, 0, 2);
 4540            }
 541            else
 0542            {
 543                // the SOCKS client requires authentication, which we currently do not support
 0544                SocketAbstraction.Send(socket, new byte[] { 0x05, 0xFF }, 0, 2);
 545
 546                // we continue business as usual but expect the client to close the connection
 547                // so one of the subsequent reads should return -1 signaling that the client
 548                // has effectively closed the connection
 0549            }
 550
 4551            var version = SocketAbstraction.ReadByte(socket, timeout);
 4552            if (version == -1)
 0553            {
 554                // SOCKS client closed connection
 0555                return false;
 556            }
 557
 4558            if (version != 5)
 0559            {
 0560                throw new ProxyException("SOCKS5: Version 5 is expected.");
 561            }
 562
 4563            var commandCode = SocketAbstraction.ReadByte(socket, timeout);
 4564            if (commandCode == -1)
 0565            {
 566                // SOCKS client closed connection
 0567                return false;
 568            }
 569
 4570            var reserved = SocketAbstraction.ReadByte(socket, timeout);
 4571            if (reserved == -1)
 0572            {
 573                // SOCKS client closed connection
 0574                return false;
 575            }
 576
 4577            if (reserved != 0)
 0578            {
 0579                throw new ProxyException("SOCKS5: 0 is expected for reserved byte.");
 580            }
 581
 4582            var addressType = SocketAbstraction.ReadByte(socket, timeout);
 4583            if (addressType == -1)
 0584            {
 585                // SOCKS client closed connection
 0586                return false;
 587            }
 588
 4589            var host = GetSocks5Host(addressType, socket, timeout);
 4590            if (host is null)
 0591            {
 592                // SOCKS client closed connection
 0593                return false;
 594            }
 595
 4596            var portBuffer = new byte[2];
 4597            if (SocketAbstraction.Read(socket, portBuffer, 0, portBuffer.Length, timeout) == 0)
 0598            {
 599                // SOCKS client closed connection
 0600                return false;
 601            }
 602
 4603            var port = Pack.BigEndianToUInt16(portBuffer);
 604
 4605            RaiseRequestReceived(host, port);
 606
 4607            channel.Open(host, port, this, socket);
 608
 4609            var socksReply = CreateSocks5Reply(channel.IsOpen);
 610
 4611            SocketAbstraction.Send(socket, socksReply, 0, socksReply.Length);
 612
 4613            return true;
 4614        }
 615
 616        private static string GetSocks5Host(int addressType, Socket socket, TimeSpan timeout)
 4617        {
 4618            switch (addressType)
 619            {
 620                case 0x01: // IPv4
 1621                    {
 1622                        var addressBuffer = new byte[4];
 1623                        if (SocketAbstraction.Read(socket, addressBuffer, 0, 4, timeout) == 0)
 0624                        {
 625                            // SOCKS client closed connection
 0626                            return null;
 627                        }
 628
 1629                        var ipv4 = new IPAddress(addressBuffer);
 1630                        return ipv4.ToString();
 631                    }
 632
 633                case 0x03: // Domain name
 3634                    {
 3635                        var length = SocketAbstraction.ReadByte(socket, timeout);
 3636                        if (length == -1)
 0637                        {
 638                            // SOCKS client closed connection
 0639                            return null;
 640                        }
 641
 3642                        var addressBuffer = new byte[length];
 3643                        if (SocketAbstraction.Read(socket, addressBuffer, 0, addressBuffer.Length, timeout) == 0)
 0644                        {
 645                            // SOCKS client closed connection
 0646                            return null;
 647                        }
 648
 3649                        var hostName = SshData.Ascii.GetString(addressBuffer, 0, addressBuffer.Length);
 3650                        return hostName;
 651                    }
 652
 653                case 0x04: // IPv6
 0654                    {
 0655                        var addressBuffer = new byte[16];
 0656                        if (SocketAbstraction.Read(socket, addressBuffer, 0, 16, timeout) == 0)
 0657                        {
 658                            // SOCKS client closed connection
 0659                            return null;
 660                        }
 661
 0662                        var ipv6 = new IPAddress(addressBuffer);
 0663                        return ipv6.ToString();
 664                    }
 665
 666                default:
 0667                    throw new ProxyException(string.Format(CultureInfo.InvariantCulture, "SOCKS5: Address type '{0}' is 
 668            }
 4669        }
 670
 671        private static byte[] CreateSocks5Reply(bool channelOpen)
 4672        {
 4673            var socksReply = new byte[// SOCKS version
 4674                                      1 +
 4675
 4676                                      // Reply field
 4677                                      1 +
 4678
 4679                                      // Reserved; fixed: 0x00
 4680                                      1 +
 4681
 4682                                      // Address type; fixed: 0x01
 4683                                      1 +
 4684
 4685                                      // IPv4 server bound address; fixed: {0x00, 0x00, 0x00, 0x00}
 4686                                      4 +
 4687
 4688                                      // server bound port; fixed: {0x00, 0x00}
 4689                                      2];
 690
 4691            socksReply[0] = 0x05;
 692
 4693            if (channelOpen)
 4694            {
 4695                socksReply[1] = 0x00; // succeeded
 4696            }
 697            else
 0698            {
 0699                socksReply[1] = 0x01; // general SOCKS server failure
 0700            }
 701
 702            // reserved
 4703            socksReply[2] = 0x00;
 704
 705            // IPv4 address type
 4706            socksReply[3] = 0x01;
 707
 4708            return socksReply;
 4709        }
 710
 711        /// <summary>
 712        /// Reads a null terminated string from a socket.
 713        /// </summary>
 714        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
 715        /// <param name="timeout">The timeout to apply to individual reads.</param>
 716        /// <returns>
 717        /// The <see cref="string"/> read, or <see langword="null"/> when the socket was closed.
 718        /// </returns>
 719        private static string ReadString(Socket socket, TimeSpan timeout)
 75720        {
 75721            var text = new StringBuilder();
 75722            var buffer = new byte[1];
 723
 781724            while (true)
 781725            {
 781726                if (SocketAbstraction.Read(socket, buffer, 0, 1, timeout) == 0)
 0727                {
 728                    // SOCKS client closed connection
 0729                    return null;
 730                }
 731
 781732                var byteRead = buffer[0];
 781733                if (byteRead == 0)
 75734                {
 735                    // end of the string
 75736                    break;
 737                }
 738
 706739                _ = text.Append((char) byteRead);
 706740            }
 741
 75742            return text.ToString();
 75743        }
 744
 745        /// <summary>
 746        /// Finalizes an instance of the <see cref="ForwardedPortDynamic"/> class.
 747        /// </summary>
 748        ~ForwardedPortDynamic()
 62749        {
 31750            Dispose(disposing: false);
 62751        }
 752    }
 753}