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