// ---------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ---------------------------------------------------------------------------- // #define DEBUG_TCP using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; #if !SINGULARITY using System.Net; #endif using Microsoft.Contracts; using NetStack.Common; using TcpError = NetStack.Contracts.TcpError; namespace NetStack.Runtime { using Protocols; public enum TcpStateEnum { Undefined = 0, Closed = 1, Listen = 2, SynRecvd = 3, SynSent = 4, Established = 5, CloseWait = 6, LastAck = 7, FinWait1 = 8, FinWait2 = 9, Closing = 10, } internal class TcpState { /// /// Names corresponding to Enums, above (reflection missing). /// internal static readonly string[] TcpStateNames = new string[] { "Undefined", "Closed ", "Listen ", "SynRecvd ", "SynSent ", "Establish", "CloseWait", "LastAck ", "FinWait1 ", "FinWait2 ", "Closing ", }; private static TcpState! instance = new TcpState(); internal static TcpState! InstanceOfUndefined() { return TcpState.instance; } // ctor internal TcpState() { } // make a state transition protected void ChangeState(TcpSession! tcpSession, TcpState newState) { tcpSession.ChangeState((!)newState); } // enter the new state virtual internal void OnStateEnter(TcpSession! tcpSession) { } // a new packet(=event) received, should handle it // according to the current state. virtual internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { return NetStatus.Code.PROTOCOL_OK; } // leave the current state to the new one virtual internal void OnStateLeave(TcpSession! tcpSession, TcpState newState) { } // Handle timeout events. // args is a session argument which is session specific virtual internal NetStatus OnTimeout(TcpSession! tcpSession, Dispatcher.CallbackArgs args) { TcpSessionEventsSource.EventLog.LogTimeout( tcpSession.Uid, tcpSession.currentState.StateEnum, TcpSession.TcpTimeoutType.Unknown); return NetStatus.Code.PROTOCOL_OK; } // Get the state name for debugging internal string GetStateName() { return TcpState.TcpStateNames[(int)this.StateEnum]; } // get state enum for eventing (tracing) virtual internal TcpStateEnum GetStateEnum() { return TcpStateEnum.Undefined; } // State's Enumeration as a Property internal TcpStateEnum StateEnum { get { return this.GetStateEnum(); } } // State's Name as a Property internal string StateName { get { return TcpState.TcpStateNames[(int)this.StateEnum]; } } [ Conditional("DEBUG_TCP") ] internal static void DebugPrint(string format, params object [] args) { Core.Log(format, args); } [ Conditional("DEBUG_TCP") ] internal static void DebugPrintLine(string format, params object [] args) { TcpState.DebugPrint(format, args); } } /// // This the TCP Finite State Machine Implementation // NOTICE: This is just a quick implementation... // should handle FIN, RST, window management, slow start, double acks... // internal class TCPFSM { // Definition of the TCP states internal static TcpState! CLOSED { get {return TCPStateClosed.Instance();}} internal static TcpState! LISTEN { get {return TCPStateListen.Instance();}} internal static TcpState! SYN_RECVD { get {return TCPStateSynRcv.Instance();}} internal static TcpState! SYN_SENT { get {return TCPStateSynSent.Instance();}} internal static TcpState! ESTABLISHED { get {return TCPStateEstab.Instance();}} internal static TcpState! CLOSE_WAIT { get {return TCPCloseWait.Instance();}} internal static TcpState! LAST_ACK { get {return TCPLastAck.Instance();}} internal static TcpState! FIN_WAIT1 { get {return TCPFINWait1.Instance();}} internal static TcpState! FIN_WAIT2 { get {return TCPFINWait2.Instance();}} internal static TcpState! CLOSING { get {return TCPClosing.Instance();}} //---------------------------------------------------------------------------- // Compare two TCP sequence numbers: // - means A < B, 0 means A == B and + means A > B [Pure] [Inline] private static int TcpSequenceNumberCompare(uint seqA, uint seqB) { // Exploit integer underflow to compare correctly even in the case // of sequence number wraparound. This assumes the two numbers are // always in the same half of the numberspace. return unchecked((int)(unchecked(seqA - seqB))); } [Pure] internal static bool TCPSeqEQ(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) == 0; } [Pure] internal static bool TCPSeqNEQ(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) != 0; } [Pure] internal static bool TCPSeqLEQ(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) <= 0; } [Pure] internal static bool TCPSeqLess(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) < 0; } [Pure] internal static bool TCPSeqGEQ(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) >= 0; } [Pure] internal static bool TCPSeqGreater(uint seqA, uint seqB) { return TCPFSM.TcpSequenceNumberCompare(seqA, seqB) > 0; } //---------------------------------------------------------------------------- internal sealed class TCPStateClosed : TcpState { static TCPStateClosed! instance = new TCPStateClosed(); static TCPStateClosed() {} internal static TCPStateClosed! Instance() { return instance; } // get state enum for eventing (tracing) and debugging. override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.Closed; } // enter the Closed state override internal void OnStateEnter(TcpSession! tcpSession) { tcpSession.DestroyRetransmitTimer(); } // leave the state override internal void OnStateLeave(TcpSession! tcpSession, TcpState newState) { assert newState != null; base.OnStateLeave(tcpSession, newState); switch (newState.StateEnum) { case TcpStateEnum.Listen: break; case TcpStateEnum.SynSent: if (!TCPFSM.SendSYN(tcpSession, TcpFormat.TCP_MSS)) { // This should never happen; our outbound // packet queue should never be empty // while we're setting up a session Debug.Assert(false); } break; case TcpStateEnum.SynRecvd: break; default: Core.Panic("TCP: Ses{0,3} ({1}) state machine error!", tcpSession.Uid, this.StateName); break; } } override internal NetStatus OnTimeout(TcpSession! tcpSession, Dispatcher.CallbackArgs args) { base.OnTimeout(tcpSession, args); return NetStatus.Code.PROTOCOL_OK; } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { // A Closed session cannot handle any packets. // BUGBUG: The parent class should make this the default behavior. // TODO return NetStatus.Code.PROTOCOL_DROP_ERROR; } } //---------------------------------------------------------------------------- // a passive connection listen state internal sealed class TCPStateListen : TcpState { static TCPStateListen! instance = new TCPStateListen(); static TCPStateListen() {} internal static TCPStateListen! Instance() { return instance; } // get state enum for eventing (tracing) and debugging. override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.Listen; } override internal void OnStateEnter(TcpSession! tcpSession) { DebugPrintLine("TCP: Ses{0,3} ({1}) Waiting for connections.", tcpSession.Uid, this.StateName); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; if (TcpFormat.IsReset(ref tcpHeader)) { return NetStatus.Code.PROTOCOL_DROP_ERROR; // we ignore it } if (TcpFormat.IsAck(ref tcpHeader)) { SendReset(tcpSession, false); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (TcpFormat.IsSync(ref tcpHeader)) { // we received a syn! // create a new TCP session and initialize it (only if // we have room) TBC: should use listen param for bound) // new session's new state (OnEnter) will send syn-ack // (this is the result of the ChangeState call). TcpSession client = (TcpSession!)tcpSession.Protocol.CreateSession(); client.passiveSession = tcpSession; // this is our "owner" client.SetLocalEndPoint(tcpSession.LocalAddress, tcpSession.LocalPort); client.InitializeServerSession(tcpHeader.seq, tcpHeader.window); // we have access to the IP header from the packet overlapped // context (TCPModule put it there) IPFormat.IPHeader ipHeader = packet.OverlapContext as IPFormat.IPHeader; assert ipHeader != null; client.SetRemoteEndPoint(ipHeader.Source, tcpHeader.sourcePort); // Save ourselves work if we should reject the session right away if (tcpSession.AcceptQueueIsFull()) { TcpSegment reset = CreateResetSegment(client, true); tcpSession.Protocol.OnProtocolSend(reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // New session starts at syn-rcv. It immediately (from // OnEntry) sends a SYN-Ack and starts the connect timer. client.ChangeState(SYN_RECVD); // WE are left at the SAME state! return NetStatus.Code.PROTOCOL_OK; } else { return NetStatus.Code.PROTOCOL_DROP_ERROR; } } } //---------------------------------------------------------------------------- internal sealed class TCPStateSynRcv : TcpState { static TCPStateSynRcv! instance = new TCPStateSynRcv(); static TCPStateSynRcv() {} internal static TCPStateSynRcv! Instance() { return instance; } // get state enum for eventing (tracing) override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.SynRecvd; } override internal void OnStateEnter(TcpSession! tcpSession) { // send syn-ack if (!SendSYNAck(tcpSession)) { // This should never happen; our outbound packet queues // should never be full while we're setting up a session. Debug.Assert(false); } // Start timer to limit the time to establishing the new session tcpSession.StartConnectTimer(); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; // all TCP states get the tcpHeader context TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; bool packetIsAcceptable = IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize); if (!packetIsAcceptable) { if (!TcpFormat.IsReset(ref tcpHeader)) { // send an ACK; don't care about whether this works // or not since we're discarding the packet anyhow. TCPFSM.SendAck(tcpSession); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } // we accept the segment Debug.Assert(tcpHeader.seq == tcpSession.sessionTCB.RCV.NXT); // if we get a reset if (TcpFormat.IsReset(ref tcpHeader)) { NetStatus.Code result; switch (tcpSession.oldStateEnum) { case TcpStateEnum.Listen: // if we came from the listen state (passive open) // the reset will return us to the Listen state tcpSession.FlushRetransmissions(); ChangeState(tcpSession, LISTEN); result = NetStatus.Code.PROTOCOL_DROP_ERROR; break; case TcpStateEnum.SynSent: // we came from SYN_SENT (active open) // the connection was refused, close it! HandleTerminateSession(tcpSession, null, TcpError.Refused); result = NetStatus.Code.PROTOCOL_DROP_ERROR; break; default: result = NetStatus.Code.PROTOCOL_DROP_ERROR; break; } return result; } if (TcpFormat.IsSync(ref tcpHeader)) { // reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (TcpFormat.IsAck(ref tcpHeader)) { if (TCPSeqLEQ(tcpSession.sessionTCB.SND.UNA, tcpHeader.ackSeq) && TCPSeqGEQ(tcpSession.sessionTCB.SND.NXT, tcpHeader.ackSeq)) { // enter established state // we get an Ack for our syn-ack // remove the SYN-ACK from the retransmit Q TCPFSM.HandleTCPAck(tcpSession, ref tcpHeader); AttemptToEnterESTABLISHED(tcpSession, ref tcpHeader); return NetStatus.Code.PROTOCOL_OK; } else { // reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.Reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } } if (TcpFormat.IsFIN(ref tcpHeader)) { DebugPrintLine("TCP: Ses{0,3} ({1)} TCPStateSynRcv: Received FIN.", tcpSession.Uid, this.StateName); // RCV.NXT should reflect the data that we processed out of // this packet, but not the FIN. Advance over the FIN. tcpSession.sessionTCB.RCV.NXT += 1; // ack the FIN. Don't worry if this doesn't actually work; // the remote side will think the ACK got lost and retransmit, // which we will hopefully be able to successfully ACK later. SendAck(tcpSession); ChangeState(tcpSession, CLOSE_WAIT); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } } //---------------------------------------------------------------------------- internal sealed class TCPStateSynSent : TcpState { static TCPStateSynSent! instance = new TCPStateSynSent(); static TCPStateSynSent() {} internal static TCPStateSynSent! Instance() { return instance; } // get state enum for eventing (tracing) and debugging. override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.SynSent; } override internal void OnStateEnter(TcpSession! tcpSession) { DebugPrintLine("TCP: Ses{0,3} ({1}) Sending SYN.", tcpSession.Uid, this.StateName); } // we sent a syn we wait for syn-ack override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; // all TCP states get the tcpHeader context TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; bool ackAcceptable=false; if (TcpFormat.IsAck(ref tcpHeader)) { if (TCPSeqLEQ(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.ISS) || TCPSeqGreater(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.NXT)) { if (TcpFormat.IsReset(ref tcpHeader)) { return NetStatus.Code.PROTOCOL_DROP_ERROR; } SendReset(tcpSession, false); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (TCPSeqLEQ(tcpSession.sessionTCB.SND.UNA, tcpHeader.ackSeq) && TCPSeqLEQ(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.NXT)) { ackAcceptable=true; } } if (TcpFormat.IsReset(ref tcpHeader)) { if (ackAcceptable) { // connection was reset, drop the segment // and close the connection. HandleTerminateSession(tcpSession, null, TcpError.Reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } else { return NetStatus.Code.PROTOCOL_DROP_ERROR; } } // check syn // ack is is ok or there is no ack and no RST if (TcpFormat.IsSync(ref tcpHeader)) { if (ackAcceptable) { DebugPrintLine("TCP: Ses{0,3} ({1}) SYNSENT, Received SYN-ACK", tcpSession.Uid, this.StateName); // use the ack parameters to complete the session's data tcpSession.sessionTCB.RCV.NXT = tcpHeader.seq+1; tcpSession.sessionTCB.RCV.IRS = tcpHeader.seq; tcpSession.sessionTCB.SND.UNA = tcpHeader.ackSeq; tcpSession.sessionTCB.SND.WND = tcpHeader.window; tcpSession.sessionTCB.RCV.WND = TcpFormat.TCP_MSS; // now remove the SYN from the retransmit Q // (so we don't retransmit it again) TCPFSM.HandleTCPAck(tcpSession, ref tcpHeader); if (tcpSession.sessionTCB.SND.UNA > tcpSession.sessionTCB.SND.ISS) { // our syn has been acked. TCPFSM.SendAck(tcpSession); // change the state to established! AttemptToEnterESTABLISHED(tcpSession, ref tcpHeader); return NetStatus.Code.PROTOCOL_OK; } } else { // received a new SYN (overlap connection) // (see SYN_RECVD for more details) // Create a new object to track the new session TcpSession client = (TcpSession!) (tcpSession.Protocol.CreateSession()); client.passiveSession = tcpSession; // this is our "owner" client.SetLocalEndPoint(tcpSession.LocalAddress, tcpSession.LocalPort); client.InitializeServerSession(tcpHeader.seq, tcpHeader.window); // we have access to the IP header from the packet overlapped // context (TCPModule put it there) IPFormat.IPHeader ipHeader = packet.OverlapContext as IPFormat.IPHeader; assert ipHeader!=null; client.SetRemoteEndPoint(ipHeader.Source, tcpHeader.sourcePort); // Save ourselves work if we should reject the session right away if (tcpSession.AcceptQueueIsFull()) { TcpSegment reset = CreateResetSegment(client, true); tcpSession.Protocol.OnProtocolSend(reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // New session starts at syn-rcv. It immediately (from // OnEntry) sends a SYN-Ack and starts the connect timer. client.ChangeState(SYN_RECVD); return NetStatus.Code.PROTOCOL_OK; } } return NetStatus.Code.PROTOCOL_DROP_ERROR; } } // TCPStateSynSent //---------------------------------------------------------------------------- // This is the working mode state internal sealed class TCPStateEstab : TcpState { static TCPStateEstab! instance = new TCPStateEstab(); static TCPStateEstab() {} internal static TCPStateEstab! Instance() { return instance; } // get state enum for eventing (tracing) and debugging. override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.Established; } override internal void OnStateEnter(TcpSession! tcpSession) { // When becoming Established, stop the connect timer // if there is one ticking. tcpSession.DestroyConnectTimer(); // Wake up anyone waiting for the connection to complete tcpSession.setupCompleteEvent.Set(); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us NetStatus res = NetStatus.Code.PROTOCOL_DROP_ERROR; bool accept=IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize); if (!accept) { if (!TcpFormat.IsReset(ref tcpHeader)) { // send an ACK and return TCPFSM.SendAck(tcpSession); } return res; } if (TcpFormat.IsReset(ref tcpHeader)) { // for simplicity, just close the connection. HandleTerminateSession(tcpSession, null, TcpError.Reset); return res; } // check syn if (TcpFormat.IsSync(ref tcpHeader)) { // send a reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.Reset); return res; } res = HandleTCPData(ref tcpHeader, packet, tcpSession); // our peer wants to end the relationship... if (TcpFormat.IsFIN(ref tcpHeader)) { // RCV.NXT should reflect any data in this packet, but not the FIN. // Advance over the FIN. tcpSession.sessionTCB.RCV.NXT += 1; // ack the FIN SendAck(tcpSession); ChangeState(tcpSession, CLOSE_WAIT); } return res; } } //---------------------------------------------------------------------------- // CLOSE_WAIT is entered when we receive (and ACK) a FIN from the // remote side. There is no further data to receive, but we can // continue sending if we like. internal sealed class TCPCloseWait : TcpState { static TCPCloseWait! instance = new TCPCloseWait(); static TCPCloseWait() {} internal static TCPCloseWait! Instance() { return instance; } // get state enum (debug) override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.CloseWait; } override internal void OnStateEnter(TcpSession! tcpSession) { base.OnStateEnter(tcpSession); // Signal that there is nothing more to read on this // connection tcpSession.ValidForRead = false; } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; if (!IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize)) { if (!TcpFormat.IsReset(ref tcpHeader)) { // send an ACK and return TCPFSM.SendAck(tcpSession); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (TcpFormat.IsReset(ref tcpHeader) || TcpFormat.IsSync(ref tcpHeader) || (!TcpFormat.IsAck(ref tcpHeader))) { // Abort HandleTerminateSession(tcpSession, null, TcpError.Reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } TCPFSM.HandleTCPAck(tcpSession, ref tcpHeader); return NetStatus.Code.PROTOCOL_OK; } } //---------------------------------------------------------------------------- // LAST_ACK is entered when we close our side of the duplex // connection and the remote site had sent us its FIN // previously. We send out our own FIN and just hang around // to make sure it was received properly before shutting down. // // CALLER SHOULD ARRANGE TO TRANSMIT THE FIN PACKET BEFORE // ENTERING THIS STATE! internal sealed class TCPLastAck : TcpState { static TCPLastAck! instance = new TCPLastAck(); static TCPLastAck() {} internal static TCPLastAck! Instance() { return instance; } // get state enum for eventing (tracing) and debugging. override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.LastAck; } override internal void OnStateEnter(TcpSession! tcpSession) { base.OnStateEnter(tcpSession); // Flag that writing is now disallowed on this session tcpSession.ValidForWrite = false; } override internal NetStatus OnPacketReceive(TcpSession! tcbSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us // RST is illegal at this point if (TcpFormat.IsReset(ref tcpHeader)) { // for simplicity, just close the connection. HandleTerminateSession(tcbSession, null, TcpError.Reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // SYNC causes us to issue a RST and abort if (TcpFormat.IsSync(ref tcpHeader)) { // send a reset HandleTerminateSession(tcbSession, CreateResetSegment(tcbSession, false), TcpError.Reset); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (!IsSegmentAcceptable(tcbSession, ref tcpHeader, segmentSize)) { // Just drop it return NetStatus.Code.PROTOCOL_DROP_ERROR; } // Do ACK housekeeping if (TCPSeqLess(tcbSession.sessionTCB.SND.UNA, tcpHeader.ackSeq) && TCPSeqLEQ(tcpHeader.ackSeq, tcbSession.sessionTCB.SND.NXT)) { // This appears to be ACKing something we sent. tcbSession.sessionTCB.SND.UNA = tcpHeader.ackSeq; // remove the packet(s) from the retransmit queue TCPFSM.HandleTCPAck(tcbSession, ref tcpHeader); } // Note the weirdness here; because we prepacketize // data, we don't want to compare to SND.NXT if (TCPSeqGEQ(tcbSession.sessionTCB.SND.UNA, tcbSession.sessionTCB.SND.NextSeq)) { // The remote site has acknowledged everything we sent. // We're done. HandleTerminateSession(tcbSession, null, TcpError.Reset); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } } //---------------------------------------------------------------------------- // FIN_WAIT1 is entered when we close our side of the duplex // connection. We don't send any more data, but we continue to // receive data from the remote side. // // CALLER SHOULD ARRANGE TO SEND THE FIN PACKET BEFORE ENTERING // THIS STATE! internal sealed class TCPFINWait1 : TcpState { static TCPFINWait1! instance = new TCPFINWait1(); static TCPFINWait1() {} internal static TCPFINWait1! Instance() { return instance; } // get state enum (debug) override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.FinWait1; } override internal void OnStateEnter(TcpSession! tcpSession) { base.OnStateEnter(tcpSession); // Flag that writing is now disallowed on this session tcpSession.ValidForWrite = false; } // leave the state override internal void OnStateLeave(TcpSession! tcpSession, TcpState newState) { assert newState != null; base.OnStateLeave(tcpSession, newState); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us // RST is illegal at this point if (TcpFormat.IsReset(ref tcpHeader)) { // for simplicity, just close the connection. HandleTerminateSession(tcpSession, null, TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // SYNC causes us to issue a RST and abort if (TcpFormat.IsSync(ref tcpHeader)) { // send a reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (!IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize)) { if (!TcpFormat.IsReset(ref tcpHeader)) { // send an ACK and return TCPFSM.SendAck(tcpSession); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } // Chew on the payload... NetStatus retval = HandleTCPData(ref tcpHeader, packet, tcpSession); if (TCPSeqLEQ(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT) && (TcpFormat.IsFIN(ref tcpHeader))) { // This is the remote side's FIN, and we have // received all data preceding it, too! // advance RCV.NXT over the FIN sequence tcpSession.sessionTCB.RCV.NXT += 1; // Note the weirdness here; because we // prepacketize data, we don't want to test // against SND.NXT if (TCPSeqLess(tcpSession.sessionTCB.SND.UNA, tcpSession.sessionTCB.SND.NextSeq)) { // ...but the remote side hasn't received // everything we have said, yet. // ack the FIN SendAck(tcpSession); ChangeState(tcpSession, CLOSING); } else { // The remote side has heard everything we // have to say. We're done. // ACK the FIN and shut down TcpSegment ackSeg = CreateAckSegment(tcpSession); HandleTerminateSession(tcpSession, ackSeg, TcpError.Closed); } } else { // Note the weirdness here; because we // prepacketize data, we do not want to test // against SND.NXT, but rather SND.NextSeq. if (TCPSeqGEQ(tcpSession.sessionTCB.SND.UNA, tcpSession.sessionTCB.SND.NextSeq)) { // The remote side has ACKed everything we have // said, but they haven't FINed themselves. ChangeState(tcpSession, FIN_WAIT2); } } return retval; } } //---------------------------------------------------------------------------- // FIN_WAIT2 is entered when the other side has received our FIN, but has // more data to send. We wait for them to signal FIN as well. internal sealed class TCPFINWait2 : TcpState { static TCPFINWait2! instance = new TCPFINWait2(); static TCPFINWait2() {} internal static TCPFINWait2! Instance() { return instance; } // get state enum (debug) override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.FinWait2; } // leave the state override internal void OnStateLeave(TcpSession! tcpSession, TcpState newState) { base.OnStateLeave(tcpSession, newState); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us // RST is illegal at this point if (TcpFormat.IsReset(ref tcpHeader)) { // for simplicity, just close the connection. HandleTerminateSession(tcpSession, null, TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // SYNC causes us to issue a RST and abort if (TcpFormat.IsSync(ref tcpHeader)) { // send a reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (!IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize)) { if (!TcpFormat.IsReset(ref tcpHeader)) { // send an ACK and return TCPFSM.SendAck(tcpSession); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } // Chew on the payload... NetStatus retval = HandleTCPData(ref tcpHeader, packet, tcpSession); // Only check for FIN if we have previously received // all data. if (TCPSeqLEQ(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT) && (TcpFormat.IsFIN(ref tcpHeader))) { // RCV.NXT should reflect the data in this packet, but not // the FIN. Advance over the FIN... tcpSession.sessionTCB.RCV.NXT += 1; // ack the FIN and shut down TcpSegment ackSeg = CreateAckSegment(tcpSession); HandleTerminateSession(tcpSession, ackSeg, TcpError.Closed); } return retval; } } //---------------------------------------------------------------------------- // CLOSING is entered when both sides have sent a FIN, but we're not sure // the other side has received all our data yet. We hang around processing // ACKs until we're sure the other side has heard everything we have to say. internal sealed class TCPClosing : TcpState { static TCPClosing! instance = new TCPClosing(); static TCPClosing() {} internal static TCPClosing! Instance() { return instance; } // get state enum (debug) override internal TcpStateEnum GetStateEnum() { return TcpStateEnum.Closing; } override internal void OnStateEnter(TcpSession! tcpSession) { base.OnStateEnter(tcpSession); // Signal that there's nothing more to read on this session tcpSession.ValidForRead = false; } // leave the state override internal void OnStateLeave(TcpSession! tcpSession, TcpState newState) { assert newState != null; base.OnStateLeave(tcpSession, newState); } override internal NetStatus OnPacketReceive(TcpSession! tcpSession, NetPacket! packet, object context) { assert context != null; TcpFormat.TcpHeader tcpHeader = (TcpFormat.TcpHeader)context; uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us // RST is illegal at this point if (TcpFormat.IsReset(ref tcpHeader)) { // for simplicity, just close the connection. HandleTerminateSession(tcpSession, null, TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // SYNC causes us to issue a RST and abort if (TcpFormat.IsSync(ref tcpHeader)) { // send a reset HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.ProtocolViolation); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (!IsSegmentAcceptable(tcpSession, ref tcpHeader, segmentSize)) { // Just drop it return NetStatus.Code.PROTOCOL_DROP_ERROR; } // Do ACK housekeeping if (TCPSeqLess(tcpSession.sessionTCB.SND.UNA, tcpHeader.ackSeq) && TCPSeqLEQ(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.NXT)) { // This appears to be ACKing something we sent. tcpSession.sessionTCB.SND.UNA = tcpHeader.ackSeq; // remove the packet(s) from the retransmit queue TCPFSM.HandleTCPAck(tcpSession, ref tcpHeader); } // Note the weirdness here; because we // prepacketize data, we don't want to compare against // SND.NXT if (TCPSeqGEQ(tcpSession.sessionTCB.SND.UNA, tcpSession.sessionTCB.SND.NextSeq)) { // The remote side has now heard everything we said. HandleTerminateSession(tcpSession, null, TcpError.Closed); } return NetStatus.Code.PROTOCOL_DROP_ERROR; } } //---------------------------------------------------------------------------- // Create an ACK for data we have received to date internal static TcpSegment! CreateAckSegment(TcpSession! tcpSession) { byte[] ackBuffer = new byte[EthernetFormat.Size+IPFormat.Size+TcpFormat.Size]; TcpFormat.WriteTcpSegment( ackBuffer, tcpSession.LocalPort, tcpSession.RemotePort, tcpSession.sessionTCB.RCV.NXT, tcpSession.sessionTCB.SND.NextSeq, TcpFormat.TCP_MSS, tcpSession.LocalAddress, tcpSession.RemoteAddress, 0, true, false, false, false, false); return new TcpSegment(ackBuffer, tcpSession, tcpSession.sessionTCB.SND.NextSeq, true); } // sends an ack internal static bool SendAck(TcpSession! tcpSession) { TcpSegment ackSeg = CreateAckSegment(tcpSession); // put it on this session outgoing queue return tcpSession.PutPacket(tcpSession.outQueue, ackSeg, false); } // Sends a FIN internal static bool SendFin(TcpSession! tcpSession, bool canBlock) { byte[] finBuffer = new byte[EthernetFormat.Size+IPFormat.Size+TcpFormat.Size]; TcpFormat.WriteTcpSegment( finBuffer, tcpSession.LocalPort, tcpSession.RemotePort, tcpSession.sessionTCB.RCV.NXT, tcpSession.sessionTCB.SND.NextSeq, TcpFormat.TCP_MSS, tcpSession.LocalAddress, tcpSession.RemoteAddress, 0, true, false, false, true, false); // ok, we have a ready segment. TcpSegment syn = new TcpSegment(finBuffer, tcpSession, tcpSession.sessionTCB.SND.NextSeq, true); bool retVal = tcpSession.PutPacket(tcpSession.outQueue, syn, canBlock); // Only advance the segment counter if we successfully queued the // outbound packet if (retVal) { tcpSession.sessionTCB.SND.NextSeq++; } return retVal; } // sends a syn ack packet internal static bool SendSYNAck(TcpSession! tcpSession) { byte[] synackBuffer = new byte[EthernetFormat.Size+IPFormat.Size+TcpFormat.Size]; TcpFormat.WriteTcpSegment(synackBuffer, tcpSession.LocalPort, tcpSession.RemotePort, tcpSession.sessionTCB.RCV.NXT, tcpSession.sessionTCB.SND.ISS, (ushort)tcpSession.sessionTCB.SND.WND, tcpSession.LocalAddress, tcpSession.RemoteAddress, 0, true, true, false, false, false); // ok, we have a ready segment. // (SYN is regular segment) TcpSegment syn = new TcpSegment(synackBuffer, tcpSession, tcpSession.sessionTCB.SND.ISS, true); // put it on this session's outgoing queue return tcpSession.PutPacket(tcpSession.outQueue, syn, false); } // sends a syn packet, with MSS options internal static bool SendSYN(TcpSession! tcpSession, ushort MSS) { byte[] synBuffer = new byte[EthernetFormat.Size+IPFormat.Size+TcpFormat.Size+4]; // setup the session tcpSession.sessionTCB.SND.ISS=(uint)DateTime.UtcNow.Ticks; tcpSession.sessionTCB.SND.UNA=tcpSession.sessionTCB.SND.ISS; tcpSession.sessionTCB.SND.NXT=tcpSession.sessionTCB.SND.ISS+1; // next packet sequence tcpSession.sessionTCB.SND.NextSeq = tcpSession.sessionTCB.SND.NXT; tcpSession.sessionTCB.SND.WND=TcpFormat.TCP_MSS; // we must first write the data... byte[] options=new byte[] {2, 4, ((byte)(MSS>>8)), (byte)MSS}; Array.Copy(options, 0, synBuffer, EthernetFormat.Size+IPFormat.Size+TcpFormat.Size, 4); // now create the segment+checksum TcpFormat.WriteTcpSegment( synBuffer, tcpSession.LocalPort, tcpSession.RemotePort, 0, tcpSession.sessionTCB.SND.ISS, MSS, tcpSession.LocalAddress, tcpSession.RemoteAddress, 4, false, true, false, false, true); // ok, we have a ready segment. // (SYN is regular segment) TcpSegment syn = new TcpSegment(synBuffer, tcpSession, tcpSession.sessionTCB.SND.ISS, false); // put it on this session outgoing queue return tcpSession.PutPacket(tcpSession.outQueue, syn, false); } // send a TCP reset internal static bool SendReset(TcpSession! tcpSession, bool isAck) { // we won't retransmit it (it is like an Ack) TcpSegment syn = CreateResetSegment(tcpSession, true); // put it on this session outgoing queue return tcpSession.PutPacket(tcpSession.outQueue, syn, false); } // send a TCP reset internal static TcpSegment! CreateResetSegment(TcpSession! tcpSession, bool isAck) { byte[] rstBuffer = new byte[EthernetFormat.Size+IPFormat.Size+TcpFormat.Size]; // NOTE: use SND.NXT as the sequence number here instead of nextSeq, // otherwise the sequence number may be far ahead (if there's lots of queued // data) and the receiving host may ignore it as outside its window. TcpFormat.WriteTcpSegment( rstBuffer, tcpSession.LocalPort, tcpSession.RemotePort, tcpSession.sessionTCB.RCV.NXT, tcpSession.sessionTCB.SND.NXT, TcpFormat.TCP_MSS, tcpSession.LocalAddress, tcpSession.RemoteAddress, 0, isAck, false, true, false, false); // we won't retransmit it TcpSegment syn = new TcpSegment(rstBuffer, tcpSession, tcpSession.sessionTCB.SND.NXT, true); return syn; } // Handle received TCP data // Returns the appropriate NetStatus.Code internal static NetStatus HandleTCPData(ref TcpFormat.TcpHeader tcpHeader, NetPacket! packet, TcpSession! tcpSession) { uint segmentSize = (uint)packet.Available; // TCPModule already shrinked it for us // TBC: We assume the segment start with // the RCV.NXT !!! (otherwise we can buffer them) if (TCPSeqGreater(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT)) { // we missed one or few, send ack again TCPFSM.SendAck(tcpSession); return NetStatus.Code.PROTOCOL_DROP_ERROR; } if (!TcpFormat.IsAck(ref tcpHeader)) { return NetStatus.Code.PROTOCOL_DROP_ERROR; } // First, deal with the window advertised in this packet. tcpSession.sessionTCB.SND.WND = tcpHeader.window; tcpSession.HandleWindowUpdate(); // ack is set and it is relevant (ack something we sent) if (TCPSeqLess(tcpSession.sessionTCB.SND.UNA, tcpHeader.ackSeq) && TCPSeqLEQ(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.NXT)) { tcpSession.sessionTCB.SND.UNA = tcpHeader.ackSeq; // remove the packet(s) from the retransmit queue TCPFSM.HandleTCPAck(tcpSession, ref tcpHeader); } else if (tcpSession.sessionTCB.SND.UNA > tcpHeader.ackSeq) { // a duplicate ack return NetStatus.Code.PROTOCOL_DROP_ERROR; } else if (TCPSeqGreater(tcpHeader.ackSeq, tcpSession.sessionTCB.SND.NXT)) { // ack for future data.. TCPFSM.SendAck(tcpSession); return NetStatus.Code.PROTOCOL_DROP_ERROR; } // check URG bit if (TcpFormat.IsUrg(ref tcpHeader)) { // TBC } // check PUSH bit if (TcpFormat.IsPush(ref tcpHeader)) { // TBC } // at last, process the segment data!!! // at the end send an ACK (TBC: change to accumulate acks) if (segmentSize > 0) { // put the data in the session's inQ if (tcpSession.PutPacket(tcpSession.inQueue, packet, false)) { tcpSession.sessionTCB.RCV.NXT+=segmentSize; // we expect the next segment seq tcpSession.sessionTCB.RCV.WND=TcpFormat.TCP_MSS; // TBC: change according to buffer // send our ack if we ack data SendAck(tcpSession); // don't release the packet return NetStatus.Code.PROTOCOL_PROCESSING; } else { // packet was dropped... // send our ack if we ack data SendAck(tcpSession); return NetStatus.Code.PROTOCOL_DROP_ERROR; } } else { // Payload was empty... return NetStatus.Code.PROTOCOL_DROP_ERROR; } } // handle a TCP received ack // the ack is already checked for validity (by the actual state) internal static void HandleTCPAck(TcpSession! tcpSession, ref TcpFormat.TcpHeader tcpHdr) { tcpSession.ACKThrough(tcpHdr.ackSeq); } // check if we can accept the segment internal static bool IsSegmentAcceptable(TcpSession! tcpSession, ref TcpFormat.TcpHeader tcpHeader, uint segmentSize) { bool accept=false; // first check sequence number if ((segmentSize == 0) && (tcpSession.sessionTCB.RCV.WND == 0)) { accept = (tcpHeader.seq == tcpSession.sessionTCB.RCV.NXT); } else if ((segmentSize == 0) && (tcpSession.sessionTCB.RCV.WND > 0)) { accept=(TCPSeqGEQ(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT) && TCPSeqLess(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT+tcpSession.sessionTCB.RCV.WND)); } else if ((segmentSize > 0) && (tcpSession.sessionTCB.RCV.WND > 0)) { accept=(TCPSeqGEQ(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT) && TCPSeqLess(tcpHeader.seq, tcpSession.sessionTCB.RCV.NXT+tcpSession.sessionTCB.RCV.WND)); accept = accept || (TCPSeqLEQ(tcpSession.sessionTCB.RCV.NXT, tcpHeader.seq+segmentSize-1) && TCPSeqLess(tcpHeader.seq +segmentSize - 1, tcpSession.sessionTCB.RCV.NXT + tcpSession.sessionTCB.RCV.WND)); } return accept; } private static void AttemptToEnterESTABLISHED(TcpSession! tcpSession, ref TcpFormat.TcpHeader tcpHeader) { TcpSession passiveOwner = tcpSession.passiveSession; if (passiveOwner != null) { bool success = passiveOwner.AddAcceptedSession(tcpSession); if (!success) { // Oops; while this connection was being established // we ran out of room in the accept queue. Abort // the connection! HandleTerminateSession(tcpSession, CreateResetSegment(tcpSession, false), TcpError.ResourcesExhausted); return; } } tcpSession.ChangeState(ESTABLISHED); } // this method terminated the given session // if nextSegment is not null than it is sent before final // removal. internal static void HandleTerminateSession(TcpSession! tcpSession, TcpSegment nextSegment, TcpError connectError) { tcpSession.StopPersistTimer(); // 1. first clear the retransmit queue // 2. make the session not valid for users! // 3. release any user waiting for read/write on the session // 4. clear the out/in queues // 5. remove the session from TCP if it is no longer needed // (when it considered to be closed) tcpSession.FlushRetransmissions(); // some user may wait on the q to write/read data // we will release them by trigger the monitors. // they will fail since Valid is false and they are users. // only TCP can still use this session. lock (tcpSession.outQueue.SyncRoot) { // from now on, every user thread will // fail to read/write data tcpSession.ValidForRead = false; tcpSession.ValidForWrite = false; tcpSession.DrainQueue(tcpSession.outQueue); if (nextSegment != null) tcpSession.outQueue.Add(nextSegment); // Don't forget these! Monitor.PulseAll(tcpSession.outQueue.SyncRoot); Core.Instance().SignalOutboundPackets(); } lock (tcpSession.inQueue.SyncRoot) { // Pulse anyone waiting to read data so they can notice // they are unlikely to succeed, but don't clear the // queue; if there is data on it, we may want to drain // it, still. Monitor.PulseAll(tcpSession.inQueue.SyncRoot); } if (nextSegment == null) { IProtocol tcpProtocol = tcpSession.Protocol; Core.Instance().DeregisterSession(tcpProtocol, tcpSession); } // change the state to close tcpSession.ChangeState(TCPFSM.CLOSED); // Signal anyone who was waiting for this session to begin or end tcpSession.connectError = connectError; tcpSession.setupCompleteEvent.Set(); // NOTE: If we are transmitting a last-gasp packet, // don't signal the session as closed just yet; wait for the packet // to actually get transmitted. if (nextSegment == null) { tcpSession.closedEvent.Set(); } } } }