/// <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; } } } }