Example #1
0
        protected void GotInit(SctpPacket initPacket, IPEndPoint remoteEndPoint)
        {
            // INIT packets have specific processing rules in order to prevent resource exhaustion.
            // See Section 5 of RFC 4960 https://tools.ietf.org/html/rfc4960#section-5 "Association Initialization".

            SctpInitChunk initChunk = initPacket.Chunks.Single(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk;

            if (initChunk.InitiateTag == 0 ||
                initChunk.NumberInboundStreams == 0 ||
                initChunk.NumberOutboundStreams == 0)
            {
                // If the value of the Initiate Tag in a received INIT chunk is found
                // to be 0, the receiver MUST treat it as an error and close the
                // association by transmitting an ABORT. (RFC4960 pg. 25)

                // Note: A receiver of an INIT with the OS value set to 0 SHOULD
                // abort the association. (RFC4960 pg. 25)

                // Note: A receiver of an INIT with the MIS value of 0 SHOULD abort
                // the association. (RFC4960 pg. 26)

                SendError(
                    true,
                    initPacket.Header.DestinationPort,
                    initPacket.Header.SourcePort,
                    initChunk.InitiateTag,
                    new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter));
            }
            else
            {
                var initAckPacket = GetInitAck(initPacket, remoteEndPoint);
                var buffer        = initAckPacket.GetBytes();
                Send(null, buffer, 0, buffer.Length);
            }
        }
        /// <summary>
        /// Event handler for a packet receive on the UDP encapsulation socket.
        /// </summary>
        /// <param name="receiver">The UDP receiver that received the packet.</param>
        /// <param name="localPort">The local port the packet was received on.</param>
        /// <param name="remoteEndPoint">The remote end point the packet was received from.</param>
        /// <param name="packet">A buffer containing the packet.</param>
        private void OnEncapsulationSocketPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet)
        {
            try
            {
                if (!SctpPacket.VerifyChecksum(packet, 0, packet.Length))
                {
                    logger.LogWarning($"SCTP packet from UDP {remoteEndPoint} dropped due to invalid checksum.");
                }
                else
                {
                    var sctpPacket = SctpPacket.Parse(packet, 0, packet.Length);

                    // Process packet.
                    if (sctpPacket.Header.VerificationTag == 0)
                    {
                        GotInit(sctpPacket, remoteEndPoint);
                    }
                    else if (sctpPacket.Chunks.Any(x => x.KnownType == SctpChunkType.COOKIE_ECHO))
                    {
                        // The COOKIE ECHO chunk is the 3rd step in the SCTP handshake when the remote party has
                        // requested a new association be created.
                        var cookie = base.GetCookie(sctpPacket);

                        if (cookie.IsEmpty())
                        {
                            logger.LogWarning($"SCTP error acquiring handshake cookie from COOKIE ECHO chunk.");
                        }
                        else
                        {
                            logger.LogDebug($"SCTP creating new association for {remoteEndPoint}.");

                            var association = new SctpAssociation(this, cookie, localPort);

                            if (_associations.TryAdd(association.ID, association))
                            {
                                if (sctpPacket.Chunks.Count > 1)
                                {
                                    // There could be DATA chunks after the COOKIE ECHO chunk.
                                    association.OnPacketReceived(sctpPacket);
                                }
                            }
                            else
                            {
                                logger.LogError($"SCTP failed to add new association to dictionary.");
                            }
                        }
                    }
                    else
                    {
                        // TODO: Lookup the existing association for the packet.
                        _associations.Values.First().OnPacketReceived(sctpPacket);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.LogError($"Exception SctpTransport.OnEncapsulationSocketPacketReceived. {excp}");
            }
        }
Example #3
0
 /// <summary>
 /// Sends an SCTP packet to the remote peer.
 /// </summary>
 /// <param name="pkt">The packet to send.</param>
 private void SendPacket(SctpPacket pkt)
 {
     if (!_wasAborted)
     {
         byte[] buffer = pkt.GetBytes();
         _sctpTransport.Send(ID, buffer, 0, buffer.Length);
     }
 }
Example #4
0
        /// <summary>
        /// Parses an SCTP packet from a serialised buffer.
        /// </summary>
        /// <param name="buffer">The buffer holding the serialised packet.</param>
        /// <param name="offset">The position in the buffer of the packet.</param>
        /// <param name="length">The length of the serialised packet in the buffer.</param>
        public static SctpPacket Parse(byte[] buffer, int offset, int length)
        {
            var pkt = new SctpPacket();

            pkt.Header = SctpHeader.Parse(buffer, offset);
            (pkt.Chunks, pkt.UnrecognisedChunks) = ParseChunks(buffer, offset, length);

            return(pkt);
        }
Example #5
0
        /// <summary>
        /// Gets an SCTP packet for a control (non-data) chunk.
        /// </summary>
        /// <param name="chunk">The control chunk to get a packet for.</param>
        /// <returns>A single control chunk SCTP packet.</returns>
        public SctpPacket GetControlPacket(SctpChunk chunk)
        {
            SctpPacket pkt = new SctpPacket(
                _sctpSourcePort,
                _sctpDestinationPort,
                _remoteVerificationTag);

            pkt.AddChunk(chunk);

            return(pkt);
        }
Example #6
0
        /// <summary>
        /// Sends a SCTP chunk to the remote party.
        /// </summary>
        /// <param name="chunk">The chunk to send.</param>
        internal void SendChunk(SctpChunk chunk)
        {
            if (!_wasAborted)
            {
                SctpPacket pkt = new SctpPacket(
                    _sctpSourcePort,
                    _sctpDestinationPort,
                    _remoteVerificationTag);

                pkt.AddChunk(chunk);

                byte[] buffer = pkt.GetBytes();

                _sctpTransport.Send(ID, buffer, 0, buffer.Length);
            }
        }
Example #7
0
        /// <summary>
        /// Creates the INIT ACK chunk and packet to send as a response to an SCTP
        /// packet containing an INIT chunk.
        /// </summary>
        /// <param name="initPacket">The received packet containing the INIT chunk.</param>
        /// <param name="remoteEP">Optional. The remote IP end point the INIT packet was
        /// received on. For transports that don't use an IP transport directly this parameter
        /// can be set to null and it will not form part of the COOKIE ECHO checks.</param>
        /// <returns>An SCTP packet with a single INIT ACK chunk.</returns>
        protected SctpPacket GetInitAck(SctpPacket initPacket, IPEndPoint remoteEP)
        {
            SctpInitChunk initChunk = initPacket.Chunks.Single(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk;

            SctpPacket initAckPacket = new SctpPacket(
                initPacket.Header.DestinationPort,
                initPacket.Header.SourcePort,
                initChunk.InitiateTag);

            var cookie = GetInitAckCookie(
                initPacket.Header.DestinationPort,
                initPacket.Header.SourcePort,
                initChunk.InitiateTag,
                initChunk.InitialTSN,
                initChunk.ARwnd,
                remoteEP != null ? remoteEP.ToString() : string.Empty,
                (int)(initChunk.CookiePreservative / 1000));

            var json       = cookie.ToJson();
            var jsonBuffer = Encoding.UTF8.GetBytes(json);

            using (HMACSHA256 hmac = new HMACSHA256(_hmacKey))
            {
                var result = hmac.ComputeHash(jsonBuffer);
                cookie.HMAC = result.HexStr();
            }

            var jsonWithHMAC       = cookie.ToJson();
            var jsonBufferWithHMAC = Encoding.UTF8.GetBytes(jsonWithHMAC);

            SctpInitChunk initAckChunk = new SctpInitChunk(
                SctpChunkType.INIT_ACK,
                cookie.Tag,
                cookie.TSN,
                cookie.ARwnd,
                SctpAssociation.DEFAULT_NUMBER_OUTBOUND_STREAMS,
                SctpAssociation.DEFAULT_NUMBER_INBOUND_STREAMS);

            initAckChunk.StateCookie = jsonBufferWithHMAC;
            initAckChunk.UnrecognizedPeerParameters = initChunk.UnrecognizedPeerParameters;

            initAckPacket.AddChunk(initAckChunk);

            return(initAckPacket);
        }
Example #8
0
        /// <summary>
        /// Send an SCTP packet with one of the error type chunks (ABORT or ERROR) to the remote peer.
        /// </summary>
        /// <param name=isAbort">Set to true to use an ABORT chunk otherwise an ERROR chunk will be used.</param>
        /// <param name="desintationPort">The SCTP destination port.</param>
        /// <param name="sourcePort">The SCTP source port.</param>
        /// <param name="initiateTag">If available the initial tag for the remote peer.</param>
        /// <param name="error">The error to send.</param>
        private void SendError(
            bool isAbort,
            ushort destinationPort,
            ushort sourcePort,
            uint initiateTag,
            ISctpErrorCause error)
        {
            SctpPacket errorPacket = new SctpPacket(
                destinationPort,
                sourcePort,
                initiateTag);

            SctpErrorChunk errorChunk = isAbort ? new SctpAbortChunk(true) : new SctpErrorChunk();

            errorChunk.AddErrorCause(error);
            errorPacket.AddChunk(errorChunk);

            var buffer = errorPacket.GetBytes();

            Send(null, buffer, 0, buffer.Length);
        }
Example #9
0
        /// <summary>
        /// Attempts to retrieve the cookie that should have been set by this peer from a COOKIE ECHO
        /// chunk. This is the step in the handshake that a new SCTP association will be created
        /// for a remote party. Providing the state cookie is valid create a new association.
        /// </summary>
        /// <param name="sctpPacket">The packet containing the COOKIE ECHO chunk received from the remote party.</param>
        /// <returns>If the state cookie in the chunk is valid a new SCTP association will be returned. IF
        /// it's not valid an empty cookie will be returned and an error response gets sent to the peer.</returns>
        protected SctpTransportCookie GetCookie(SctpPacket sctpPacket)
        {
            var cookieEcho   = sctpPacket.Chunks.Single(x => x.KnownType == SctpChunkType.COOKIE_ECHO);
            var cookieBuffer = cookieEcho.ChunkValue;
            var cookie       = JSONParser.FromJson <SctpTransportCookie>(Encoding.UTF8.GetString(cookieBuffer));

            logger.LogDebug($"Cookie: {cookie.ToJson()}");

            string calculatedHMAC = GetCookieHMAC(cookieBuffer);

            if (calculatedHMAC != cookie.HMAC)
            {
                logger.LogWarning($"SCTP COOKIE ECHO chunk had an invalid HMAC, calculated {calculatedHMAC}, cookie {cookie.HMAC}.");
                SendError(
                    true,
                    sctpPacket.Header.DestinationPort,
                    sctpPacket.Header.SourcePort,
                    0,
                    new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter));
                return(SctpTransportCookie.Empty);
            }
            else if (DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt)).TotalSeconds > cookie.Lifetime)
            {
                logger.LogWarning($"SCTP COOKIE ECHO chunk was stale, created at {cookie.CreatedAt}, now {DateTime.Now.ToString("o")}, lifetime {cookie.Lifetime}s.");
                var diff = DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt).AddSeconds(cookie.Lifetime));
                SendError(
                    true,
                    sctpPacket.Header.DestinationPort,
                    sctpPacket.Header.SourcePort,
                    0,
                    new SctpErrorStaleCookieError {
                    MeasureOfStaleness = (uint)(diff.TotalMilliseconds * 1000)
                });
                return(SctpTransportCookie.Empty);
            }
            else
            {
                return(cookie);
            }
        }
Example #10
0
        /// <summary>
        /// Attempts to create an association with a remote party by sending an initialisation
        /// control chunk.
        /// </summary>
        private void SendInit()
        {
            if (!_wasAborted)
            {
                // A packet containing an INIT chunk MUST have a zero Verification Tag (RFC4960 Pg 15).
                SctpPacket init = new SctpPacket(_sctpSourcePort, _sctpDestinationPort, 0);

                SctpInitChunk initChunk = new SctpInitChunk(
                    SctpChunkType.INIT,
                    VerificationTag,
                    TSN,
                    ARwnd,
                    _numberOutboundStreams,
                    _numberInboundStreams);
                init.AddChunk(initChunk);

                SetState(SctpAssociationState.CookieWait);

                byte[] buffer = init.GetBytes();
                _sctpTransport.Send(ID, buffer, 0, buffer.Length);

                _t1Init = new Timer(T1InitTimerExpired, init, T1_INIT_TIMER_MILLISECONDS, T1_INIT_TIMER_MILLISECONDS);
            }
        }
Example #11
0
        /// <summary>
        /// Implements the SCTP association state machine.
        /// </summary>
        /// <param name="packet">An SCTP packet received from the remote party.</param>
        /// <remarks>
        /// SCTP Association State Diagram:
        /// https://tools.ietf.org/html/rfc4960#section-4
        /// </remarks>
        internal void OnPacketReceived(SctpPacket packet)
        {
            if (_wasAborted)
            {
                logger.LogWarning($"SCTP packet received but association has been aborted, ignoring.");
            }
            else if (packet.Header.VerificationTag != VerificationTag)
            {
                logger.LogWarning($"SCTP packet dropped due to wrong verification tag, expected " +
                                  $"{VerificationTag} got {packet.Header.VerificationTag}.");
            }
            else if (!_sctpTransport.IsPortAgnostic && packet.Header.DestinationPort != _sctpSourcePort)
            {
                logger.LogWarning($"SCTP packet dropped due to wrong SCTP destination port, expected " +
                                  $"{_sctpSourcePort} got {packet.Header.DestinationPort}.");
            }
            else if (!_sctpTransport.IsPortAgnostic && packet.Header.SourcePort != _sctpDestinationPort)
            {
                logger.LogWarning($"SCTP packet dropped due to wrong SCTP source port, expected " +
                                  $"{_sctpDestinationPort} got {packet.Header.SourcePort}.");
            }
            else
            {
                foreach (var chunk in packet.Chunks)
                {
                    var chunkType = (SctpChunkType)chunk.ChunkType;

                    switch (chunkType)
                    {
                    case SctpChunkType.ABORT:
                        string abortReason = (chunk as SctpAbortChunk).GetAbortReason();
                        logger.LogWarning($"SCTP packet ABORT chunk received from remote party, reason {abortReason}.");
                        _wasAborted = true;
                        OnAbortReceived?.Invoke(abortReason);
                        break;

                    case var ct when ct == SctpChunkType.COOKIE_ACK && State != SctpAssociationState.CookieEchoed:
                        // https://tools.ietf.org/html/rfc4960#section-5.2.5
                        // At any state other than COOKIE-ECHOED, an endpoint should silently
                        // discard a received COOKIE ACK chunk.
                        break;

                    case var ct when ct == SctpChunkType.COOKIE_ACK && State == SctpAssociationState.CookieEchoed:
                        SetState(SctpAssociationState.Established);
                        CancelTimers();
                        _dataSender.StartSending();
                        break;

                    case SctpChunkType.COOKIE_ECHO:
                        // In standard operation an SCTP association gets created when the parent transport
                        // receives a COOKIE ECHO chunk. The association gets initialised from the chunk and
                        // does not need to process it.
                        // The scenarios in https://tools.ietf.org/html/rfc4960#section-5.2 describe where
                        // an association could receive a COOKIE ECHO.
                        break;

                    case SctpChunkType.DATA:
                        var dataChunk = chunk as SctpDataChunk;

                        if (dataChunk.UserData == null || dataChunk.UserData.Length == 0)
                        {
                            // Fatal condition:
                            // - If an endpoint receives a DATA chunk with no user data (i.e., the
                            //   Length field is set to 16), it MUST send an ABORT with error cause
                            //   set to "No User Data". (RFC4960 pg. 80)
                            Abort(new SctpErrorNoUserData {
                                TSN = (chunk as SctpDataChunk).TSN
                            });
                        }
                        else
                        {
                            logger.LogTrace($"SCTP data chunk received on ID {ID} with TSN {dataChunk.TSN}, payload length {dataChunk.UserData.Length}, flags {dataChunk.ChunkFlags:X2}.");

                            // A received data chunk can result in multiple data frames becoming available.
                            // For example if a stream has out of order frames already received and the next
                            // in order frame arrives then all the in order ones will be supplied.
                            var sortedFrames = _dataReceiver.OnDataChunk(dataChunk);

                            var sack = _dataReceiver.GetSackChunk();
                            if (sack != null)
                            {
                                SendChunk(sack);
                            }

                            foreach (var frame in sortedFrames)
                            {
                                OnData?.Invoke(frame);
                            }
                        }

                        break;

                    case SctpChunkType.ERROR:
                        var errorChunk = chunk as SctpErrorChunk;
                        foreach (var err in errorChunk.ErrorCauses)
                        {
                            logger.LogWarning($"SCTP error {err.CauseCode}.");
                        }
                        break;

                    case SctpChunkType.HEARTBEAT:
                        // The HEARTBEAT ACK sends back the same chunk but with the type changed.
                        chunk.ChunkType = (byte)SctpChunkType.HEARTBEAT_ACK;
                        SendChunk(chunk);
                        break;

                    case var ct when ct == SctpChunkType.INIT_ACK && State != SctpAssociationState.CookieWait:
                        // https://tools.ietf.org/html/rfc4960#section-5.2.3
                        // If an INIT ACK is received by an endpoint in any state other than the
                        // COOKIE - WAIT state, the endpoint should discard the INIT ACK chunk.
                        break;

                    case var ct when ct == SctpChunkType.INIT_ACK && State == SctpAssociationState.CookieWait:

                        if (_t1Init != null)
                        {
                            _t1Init.Dispose();
                            _t1Init = null;
                        }

                        var initAckChunk = chunk as SctpInitChunk;

                        if (initAckChunk.InitiateTag == 0 ||
                            initAckChunk.NumberInboundStreams == 0 ||
                            initAckChunk.NumberOutboundStreams == 0)
                        {
                            // Fatal conditions:
                            //  - The Initiate Tag MUST NOT take the value 0. (RFC4960 pg 30).
                            //  - Note: A receiver of an INIT ACK with the OS value set to 0 SHOULD
                            //    destroy the association discarding its TCB. (RFC4960 pg 31).
                            //  - Note: A receiver of an INIT ACK with the MIS value set to 0 SHOULD
                            //    destroy the association discarding its TCB. (RFC4960 pg 31).
                            Abort(new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter));
                        }
                        else
                        {
                            InitRemoteProperties(initAckChunk.InitiateTag, initAckChunk.InitialTSN, initAckChunk.ARwnd);

                            var cookie = initAckChunk.StateCookie;

                            // The cookie chunk parameter can be changed to a COOKE ECHO CHUNK by changing the first two bytes.
                            // But it's more convenient to create a new chunk.
                            var cookieEchoChunk = new SctpChunk(SctpChunkType.COOKIE_ECHO)
                            {
                                ChunkValue = cookie
                            };
                            var cookieEchoPkt = GetControlPacket(cookieEchoChunk);

                            if (initAckChunk.UnrecognizedPeerParameters.Count > 0)
                            {
                                var errChunk = new SctpErrorChunk();

                                foreach (var unrecognised in initAckChunk.UnrecognizedPeerParameters)
                                {
                                    var unrecognisedParams = new SctpErrorUnrecognizedParameters {
                                        UnrecognizedParameters = unrecognised.GetBytes()
                                    };
                                    errChunk.AddErrorCause(unrecognisedParams);
                                }

                                cookieEchoPkt.AddChunk(errChunk);
                            }

                            SendPacket(cookieEchoPkt);
                            SetState(SctpAssociationState.CookieEchoed);

                            _t1Cookie = new Timer(T1CookieTimerExpired, cookieEchoPkt, T1_COOKIE_TIMER_MILLISECONDS, T1_COOKIE_TIMER_MILLISECONDS);
                        }
                        break;

                    case var ct when ct == SctpChunkType.INIT_ACK && State != SctpAssociationState.CookieWait:
                        logger.LogWarning($"SCTP association received INIT_ACK chunk in wrong state of {State}, ignoring.");
                        break;

                    case SctpChunkType.SACK:
                        _dataSender.GotSack(chunk as SctpSackChunk);
                        break;

                    case var ct when ct == SctpChunkType.SHUTDOWN && State == SctpAssociationState.Established:
                        // TODO: Check outstanding data chunks.
                        var shutdownAck = new SctpChunk(SctpChunkType.SHUTDOWN_ACK);
                        SendChunk(shutdownAck);
                        SetState(SctpAssociationState.ShutdownAckSent);
                        break;

                    case var ct when ct == SctpChunkType.SHUTDOWN_ACK && State == SctpAssociationState.ShutdownSent:
                        SetState(SctpAssociationState.Closed);
                        var shutCompleteChunk = new SctpChunk(SctpChunkType.SHUTDOWN_COMPLETE,
                                                              (byte)(_remoteVerificationTag != 0 ? SHUTDOWN_CHUNK_TBIT_FLAG : 0x00));
                        var shutCompletePkt = GetControlPacket(shutCompleteChunk);
                        shutCompletePkt.Header.VerificationTag = packet.Header.VerificationTag;
                        SendPacket(shutCompletePkt);
                        break;

                    case var ct when ct == SctpChunkType.SHUTDOWN_COMPLETE &&
                        (State == SctpAssociationState.ShutdownAckSent || State == SctpAssociationState.ShutdownSent):
                        _wasShutdown = true;
                        SetState(SctpAssociationState.Closed);
                        break;

                    default:
                        logger.LogWarning($"SCTP association no rule for {chunkType} in state of {State}.");
                        break;
                    }
                }
            }
        }
Example #12
0
        /// <summary>
        /// This method runs on a dedicated thread to listen for incoming SCTP
        /// packets on the DTLS transport.
        /// </summary>
        private void DoReceive(object state)
        {
            byte[] recvBuffer = new byte[SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW];

            while (!_isClosed)
            {
                try
                {
                    int bytesRead = transport.Receive(recvBuffer, 0, recvBuffer.Length, RECEIVE_TIMEOUT_MILLISECONDS);

                    if (bytesRead == DtlsSrtpTransport.DTLS_RETRANSMISSION_CODE)
                    {
                        // Timed out waiting for a packet, this is by design and the receive attempt should
                        // be retired.
                        continue;
                    }
                    else if (bytesRead > 0)
                    {
                        if (!SctpPacket.VerifyChecksum(recvBuffer, 0, bytesRead))
                        {
                            logger.LogWarning($"SCTP packet received on DTLS transport dropped due to invalid checksum.");
                        }
                        else
                        {
                            var pkt = SctpPacket.Parse(recvBuffer, 0, bytesRead);

                            if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.INIT))
                            {
                                var initChunk = pkt.Chunks.First(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk;
                                logger.LogDebug($"SCTP INIT packet received, initial tag {initChunk.InitiateTag}, initial TSN {initChunk.InitialTSN}.");

                                GotInit(pkt, null);
                            }
                            else if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.COOKIE_ECHO))
                            {
                                // The COOKIE ECHO chunk is the 3rd step in the SCTP handshake when the remote party has
                                // requested a new association be created.
                                var cookie = base.GetCookie(pkt);

                                if (cookie.IsEmpty())
                                {
                                    logger.LogWarning($"SCTP error acquiring handshake cookie from COOKIE ECHO chunk.");
                                }
                                else
                                {
                                    RTCSctpAssociation.GotCookie(cookie);

                                    if (pkt.Chunks.Count() > 1)
                                    {
                                        // There could be DATA chunks after the COOKIE ECHO chunk.
                                        RTCSctpAssociation.OnPacketReceived(pkt);
                                    }
                                }
                            }
                            else
                            {
                                RTCSctpAssociation.OnPacketReceived(pkt);
                            }
                        }
                    }
                    else if (_isClosed)
                    {
                        // The DTLS transport has been closed or is no longer available.
                        logger.LogWarning($"SCTP the RTCSctpTransport DTLS transport returned an error.");
                        break;
                    }
                }
                catch (ApplicationException appExcp)
                {
                    // Treat application exceptions as recoverable, things like SCTP packet parse failures.
                    logger.LogWarning($"SCTP error processing RTCSctpTransport receive. {appExcp.Message}");
                }
                catch (TlsFatalAlert alert) when(alert.InnerException is SocketException)
                {
                    var sockExcp = alert.InnerException as SocketException;

                    logger.LogWarning($"SCTP RTCSctpTransport receive socket failure {sockExcp.SocketErrorCode}.");
                    break;
                }
                catch (Exception excp)
                {
                    logger.LogError($"SCTP fatal error processing RTCSctpTransport receive. {excp}");
                    break;
                }
            }

            if (!_isClosed)
            {
                logger.LogWarning($"SCTP association {RTCSctpAssociation.ID} receive thread stopped.");
            }

            SetState(RTCSctpTransportState.Closed);
        }