/// <summary> /// Parses a simple chunk and does not attempt to process any chunk value. /// This method is suitable when: /// - the chunk type consists only of the 4 byte header and has /// no fixed or variable parameters set. /// </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 ParseBaseChunk(byte[] buffer, int posn) { var chunk = new SctpChunk(); ushort chunkLength = chunk.ParseFirstWord(buffer, posn); if (chunkLength > SCTP_CHUNK_HEADER_LENGTH) { chunk.ChunkValue = new byte[chunkLength - SCTP_CHUNK_HEADER_LENGTH]; Buffer.BlockCopy(buffer, posn + SCTP_CHUNK_HEADER_LENGTH, chunk.ChunkValue, 0, chunk.ChunkValue.Length); } return(chunk); }
/// <summary> /// Parses the chunks from a serialised SCTP packet. /// </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> /// <returns>The lsit of parsed chunks and a list of unrecognised chunks that were not de-serialised.</returns> private static (List <SctpChunk> chunks, List <byte[]> unrecognisedChunks) ParseChunks(byte[] buffer, int offset, int length) { List <SctpChunk> chunks = new List <SctpChunk>(); List <byte[]> unrecognisedChunks = new List <byte[]>(); int posn = offset + SctpHeader.SCTP_HEADER_LENGTH; bool stop = false; while (posn < length) { byte chunkType = buffer[posn]; if (Enum.IsDefined(typeof(SctpChunkType), chunkType)) { var chunk = SctpChunk.Parse(buffer, posn); chunks.Add(chunk); } else { switch (SctpChunk.GetUnrecognisedChunkAction(chunkType)) { case SctpUnrecognisedChunkActions.Stop: stop = true; break; case SctpUnrecognisedChunkActions.StopAndReport: stop = true; unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(buffer, posn)); break; case SctpUnrecognisedChunkActions.Skip: break; case SctpUnrecognisedChunkActions.SkipAndReport: unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(buffer, posn)); break; } } if (stop) { logger.LogWarning($"SCTP unrecognised chunk type {chunkType} indicated no further chunks should be processed."); break; } posn += (int)SctpChunk.GetChunkLengthFromHeader(buffer, posn, true); } return(chunks, unrecognisedChunks); }
/// <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); } }
/// <summary> /// Initialises the association state based on the echoed cookie (the cookie that we sent /// to the remote party and was then echoed back to us). An association can only be initialised /// from a cookie prior to it being used and prior to it ever having entered the established state. /// </summary> /// <param name="cookie">The echoed cookie that was returned from the remote party.</param> public void GotCookie(SctpTransportCookie cookie) { // The CookieEchoed state is allowed, even though a cookie should be creating a brand // new association rather than one that has already sent an INIT, in order to deal with // a race condition where both SCTP end points attempt to establish the association at // the same time using the same ports. if (_wasAborted || _wasShutdown) { logger.LogWarning($"SCTP association cannot initialise with a cookie after an abort or shutdown."); } else if (!(State == SctpAssociationState.Closed || State == SctpAssociationState.CookieEchoed)) { throw new ApplicationException($"SCTP association cannot initialise with a cookie in state {State}."); } else { _sctpSourcePort = cookie.SourcePort; _sctpDestinationPort = cookie.DestinationPort; VerificationTag = cookie.Tag; ARwnd = cookie.ARwnd; Destination = !string.IsNullOrEmpty(cookie.RemoteEndPoint) ? IPSocket.Parse(cookie.RemoteEndPoint) : null; if (_dataReceiver == null) { _dataReceiver = new SctpDataReceiver(ARwnd, _defaultMTU, cookie.RemoteTSN); } if (_dataSender == null) { _dataSender = new SctpDataSender(ID, this.SendChunk, _defaultMTU, cookie.TSN, cookie.RemoteARwnd); } InitRemoteProperties(cookie.RemoteTag, cookie.RemoteTSN, cookie.RemoteARwnd); var cookieAckChunk = new SctpChunk(SctpChunkType.COOKIE_ACK); SendChunk(cookieAckChunk); SetState(SctpAssociationState.Established); _dataSender.StartSending(); CancelTimers(); } }
/// <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; } } } }
/// <summary> /// Adds a new chunk to send with an outgoing packet. /// </summary> /// <param name="chunk">The chunk to add.</param> public void AddChunk(SctpChunk chunk) { Chunks.Add(chunk); }
/// <summary> /// Copies an unrecognised chunk to a byte buffer and returns it. This method is /// used to assist in reporting unrecognised chunk types. /// </summary> /// <param name="buffer">The buffer containing the chunk.</param> /// <param name="posn">The position in the buffer that the unrecognised chunk starts.</param> /// <returns>A new buffer containing a copy of the chunk.</returns> public static byte[] CopyUnrecognisedChunk(byte[] buffer, int posn) { byte[] unrecognised = new byte[SctpChunk.GetChunkLengthFromHeader(buffer, posn, true)]; Buffer.BlockCopy(buffer, posn, unrecognised, 0, unrecognised.Length); return(unrecognised); }