/// <summary> /// Parses an SCTP chunk from a buffer. /// </summary> /// <param name="buffer">The buffer holding the serialised chunk.</param> /// <param name="posn">The position to start parsing at.</param> /// <returns>An SCTP chunk instance.</returns> public static SctpChunk Parse(byte[] buffer, int posn) { if (buffer.Length < posn + SCTP_CHUNK_HEADER_LENGTH) { throw new ApplicationException("Buffer did not contain the minimum of bytes for an SCTP chunk."); } byte chunkType = buffer[posn]; if (Enum.IsDefined(typeof(SctpChunkType), chunkType)) { switch ((SctpChunkType)chunkType) { case SctpChunkType.ABORT: return(SctpAbortChunk.ParseChunk(buffer, posn, true)); case SctpChunkType.DATA: return(SctpDataChunk.ParseChunk(buffer, posn)); case SctpChunkType.ERROR: return(SctpErrorChunk.ParseChunk(buffer, posn, false)); case SctpChunkType.SACK: return(SctpSackChunk.ParseChunk(buffer, posn)); case SctpChunkType.COOKIE_ACK: case SctpChunkType.COOKIE_ECHO: case SctpChunkType.HEARTBEAT: case SctpChunkType.HEARTBEAT_ACK: case SctpChunkType.SHUTDOWN_ACK: case SctpChunkType.SHUTDOWN_COMPLETE: return(ParseBaseChunk(buffer, posn)); case SctpChunkType.INIT: case SctpChunkType.INIT_ACK: return(SctpInitChunk.ParseChunk(buffer, posn)); case SctpChunkType.SHUTDOWN: return(SctpShutdownChunk.ParseChunk(buffer, posn)); default: logger.LogDebug($"TODO: Implement parsing logic for well known chunk type {(SctpChunkType)chunkType}."); return(ParseBaseChunk(buffer, posn)); } } // Shouldn't reach this point. The SCTP packet parsing logic checks if the chunk is // recognised before attempting to parse it. throw new ApplicationException($"SCTP chunk type of {chunkType} was not recognised."); }
/// <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); }
/// <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; } } } }