743 lines
16 KiB
C
743 lines
16 KiB
C
/*++
|
|
|
|
Copyright (c) 2000 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
tcpsend.c
|
|
|
|
Abstract:
|
|
|
|
TCP data transmission related functions
|
|
|
|
Revision History:
|
|
|
|
06/06/2000 davidx
|
|
Created it.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
|
|
//
|
|
// Maximum number of transmission retries
|
|
//
|
|
UINT cfgMaxXmitRetries = 5;
|
|
|
|
//
|
|
// Maximum and minimum retransmission timeout (in ticks)
|
|
// NOTE: Non-standard behavior!!!
|
|
// Our maximum timeout between retries is 8sec, much shorter
|
|
// than the usual value of 60sec. This is ok for our first
|
|
// release since we only support LAN play.
|
|
//
|
|
UINT cfgMinRexmitTimeout = SLOW_TCP_TIMER_FREQ;
|
|
UINT cfgMaxRexmitTimeout = 8 * SLOW_TCP_TIMER_FREQ;
|
|
|
|
//
|
|
// Minimum zero-window-probe timeout (in ticks)
|
|
//
|
|
UINT cfgMinPersistTimeout = 5*SLOW_TCP_TIMER_FREQ;
|
|
|
|
//
|
|
// TCP statistics
|
|
//
|
|
TCPSTATS TcpStats;
|
|
|
|
|
|
VOID
|
|
TcbSendSegment(
|
|
TCB* tcb,
|
|
TCPSENDBUF* sendbuf
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Send a TCP segment out
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
sendbuf - Points to the segment to be sent
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
{
|
|
Packet* pkt = GetSendbufPkt(sendbuf);
|
|
|
|
if (!IsSendbufActive(sendbuf)) {
|
|
tcb->delayedAcks = 0;
|
|
SendbufIncRef(sendbuf);
|
|
|
|
ASSERT(SEQ_LE(sendbuf->seq, TcbSndNxt(tcb)) || (sendbuf->tcpflags & TCP_FIN));
|
|
|
|
FILL_TCPHEADER(
|
|
pkt,
|
|
&tcb->addrpair,
|
|
sendbuf->seq,
|
|
tcb->rcv_nxt,
|
|
sendbuf->tcphdrlen,
|
|
sendbuf->tcpflags,
|
|
(WORD) tcb->rcv_wnd);
|
|
|
|
if (sendbuf->retries)
|
|
pkt->pktflags |= PKTFLAG_REXMIT;
|
|
|
|
PcbSetupIpHeader((PCB*) tcb, pkt, &tcb->addrpair);
|
|
IpSendPacket(pkt, tcb->dstaddr, NULL, &tcb->rte);
|
|
}
|
|
|
|
if (sendbuf == TcbFirstSendbuf(tcb)) {
|
|
// Set up the retransmisssion or persist timer
|
|
|
|
UINT minTimeout = tcb->persistFlag ?
|
|
cfgMinPersistTimeout :
|
|
cfgMinRexmitTimeout;
|
|
|
|
tcb->xmitTimer = (tcb->RTO << sendbuf->retries);
|
|
if (tcb->xmitTimer < minTimeout)
|
|
tcb->xmitTimer = minTimeout;
|
|
else if (tcb->xmitTimer > cfgMaxRexmitTimeout)
|
|
tcb->xmitTimer = cfgMaxRexmitTimeout;
|
|
}
|
|
|
|
sendbuf->retries++;
|
|
}
|
|
|
|
|
|
BOOL
|
|
TcbStartOutput(
|
|
TCB* tcb
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Called when we have buffered data to send on a TCP socket.
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
sendbuf - Specifies the segment to be sent
|
|
|
|
Return Value:
|
|
|
|
TRUE if a segment was sent; FALSE otherwise.
|
|
|
|
--*/
|
|
|
|
{
|
|
INT sndwndAvail;
|
|
TCPSENDBUF* sendbuf;
|
|
TCPSEQ cwndmax, swndmax;
|
|
|
|
RUNS_AT_DISPATCH_LEVEL
|
|
|
|
sendbuf = tcb->sendbufNext;
|
|
ASSERT(tcb->snd_nxt == sendbuf->seq);
|
|
ASSERT(sendbuf->retries == 0);
|
|
|
|
// We always send SYN or FIN segments without delay
|
|
if (sendbuf->datalen == 0) goto sendnow;
|
|
|
|
// NOTE: We're using signed integer here in case
|
|
// the send window was shrunk by the receiver.
|
|
swndmax = tcb->snd_wl2 + tcb->snd_wnd;
|
|
cwndmax = tcb->snd_una + tcb->snd_cwnd;
|
|
sndwndAvail = (SEQ_LE(swndmax, cwndmax) ? swndmax : cwndmax) - tcb->snd_nxt;
|
|
|
|
if ((INT) sendbuf->datalen <= sndwndAvail) {
|
|
// Send window is large enough. Perform send-side
|
|
// SWS avoidance. Note that if the PSH flag is not set,
|
|
// then we must be sending a full-sized data segment.
|
|
// Check the logic in TcbSend().
|
|
|
|
// BUGBUG: Since we don't yet combine data from
|
|
// multiple send calls into a single segment, it's
|
|
// meaningless to delay the send.
|
|
goto sendnow;
|
|
}
|
|
|
|
// Cannot send due to window constraints
|
|
// set up persist timers
|
|
if (tcb->snd_nxt == tcb->snd_una) {
|
|
TRACE_("Setting persist timer...");
|
|
TcpStats.persists++;
|
|
tcb->persistFlag = 1;
|
|
tcb->xmitTimer = max(tcb->RTO, cfgMinPersistTimeout);
|
|
}
|
|
return FALSE;
|
|
|
|
sendnow:
|
|
|
|
// Use this segment to measure round-trip time if possible
|
|
tcb->persistFlag = 0;
|
|
if (tcb->rtt_tick == 0) {
|
|
tcb->rtt_tick = TcpTickCount;
|
|
tcb->rtt_seq = sendbuf->seq;
|
|
}
|
|
|
|
sendbuf->firstSendTime = TcpTickCount;
|
|
TcbSendSegment(tcb, sendbuf);
|
|
tcb->snd_nxt = sendbuf->seqnext;
|
|
tcb->sendbufNext = TcbSendbufFlink(sendbuf);
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
TCB*
|
|
TcbXmitTimeout(
|
|
TCB* tcb
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This function is called when the TCP retransmission timer expires.
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
|
|
Return Value:
|
|
|
|
NULL if the specified TCB was deleted inside this call.
|
|
Otherwise, just return the input tcb parameter.
|
|
|
|
--*/
|
|
|
|
{
|
|
TCPSENDBUF* sendbuf;
|
|
|
|
if (IsPcbSendBufEmpty(tcb)) return tcb;
|
|
sendbuf = TcbFirstSendbuf(tcb);
|
|
|
|
if (sendbuf->retries >= cfgMaxXmitRetries) {
|
|
TcbResetPeer(tcb);
|
|
return TcbReset(tcb, NETERR_TIMEOUT);
|
|
}
|
|
|
|
// Stop round-trip time measurement if there is retransmission
|
|
tcb->rtt_tick = 0;
|
|
|
|
// Retransmit the first segment
|
|
if (tcb->persistFlag) {
|
|
if (sendbuf->retries == 0) {
|
|
// Send out the window probe segment for the first time.
|
|
// BUGBUG: we always send out the whole segment instead
|
|
// of just a single byte of data.
|
|
|
|
ASSERT(sendbuf == tcb->sendbufNext);
|
|
TcbSendSegment(tcb, sendbuf);
|
|
tcb->snd_nxt = sendbuf->seqnext;
|
|
tcb->sendbufNext = TcbSendbufFlink(sendbuf);
|
|
return tcb;
|
|
}
|
|
} else {
|
|
TRACE_("Retransmission timeout: %u, %d", sendbuf->seq, sendbuf->retries);
|
|
ASSERT(sendbuf->retries != 0);
|
|
TcpStats.rexmitTimeouts++;
|
|
|
|
// Congestion control:
|
|
// set the slow-start threshold to be half the flight size
|
|
// and set the congestion window to be 1 SMSS
|
|
TcbResetSsthresh(tcb);
|
|
tcb->snd_cwnd = tcb->snd_mss;
|
|
TcbStopFastRexmitMode(tcb);
|
|
}
|
|
|
|
TcbSendSegment(tcb, sendbuf);
|
|
return tcb;
|
|
}
|
|
|
|
|
|
VOID
|
|
TcpSourceQuench(
|
|
const IpHeader* iphdr
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Process ICMP source quench error message
|
|
for a TCP connection
|
|
|
|
Arguments:
|
|
|
|
iphdr - Points to the original IP header + at least 8 bytes of data
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
{
|
|
const TcpHeader* tcphdr;
|
|
TCB* tcb;
|
|
|
|
tcphdr = (const TcpHeader*) ((const BYTE*) iphdr + GETIPHDRLEN(iphdr));
|
|
tcb = (TCB*) PcbFindMatch(
|
|
iphdr->srcaddr,
|
|
tcphdr->srcport,
|
|
iphdr->dstaddr,
|
|
tcphdr->dstport,
|
|
SOCK_STREAM,
|
|
IPPROTOCOL_TCP);
|
|
|
|
if (tcb && IsTcpSyncState(tcb)) {
|
|
// Treat source quench the same way as a retransmission timeout:
|
|
// reset the slow-start threshold
|
|
// and set the cwnd to 1 SMSS
|
|
TcbResetSsthresh(tcb);
|
|
tcb->snd_cwnd = tcb->snd_mss;
|
|
TcbStopFastRexmitMode(tcb);
|
|
}
|
|
}
|
|
|
|
|
|
VOID
|
|
TcbDoFastRexmit(
|
|
TCB* tcb
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Initiate the TCP fast retransmit / recovery procedure
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
{
|
|
TCPSENDBUF* sendbuf;
|
|
|
|
TRACE_("Fast rexmit mode: %u", tcb->snd_una);
|
|
TcpStats.fastRexmits++;
|
|
|
|
// Reset the slow-start threshold
|
|
// and set the congestion window to be that plus 3*SMSS
|
|
TcbResetSsthresh(tcb);
|
|
tcb->snd_cwnd = tcb->snd_ssthresh + 3*tcb->snd_mss;
|
|
tcb->fastRexmitFlag = 1;
|
|
tcb->dupacks = 0;
|
|
|
|
// Retransmit the "lost" segment immediately
|
|
sendbuf = TcbFirstSendbuf(tcb);
|
|
ASSERT(sendbuf != TcbSendbufNil(tcb) && sendbuf->retries);
|
|
TcbSendSegment(tcb, sendbuf);
|
|
}
|
|
|
|
|
|
VOID
|
|
TcbQueueSegment(
|
|
TCB* tcb,
|
|
Packet* pkt,
|
|
BYTE tcpflags
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Initialize the header information for an outgoing TCP packet
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
pkt - Points to the outgoing TCP packet
|
|
tcpflags - Specifies the TCP segment flags
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
// TCP option length in our SYN segment
|
|
#define SYNTCPOPTLEN 4
|
|
|
|
{
|
|
TCPSENDBUF* sendbuf;
|
|
|
|
RUNS_AT_DISPATCH_LEVEL
|
|
|
|
sendbuf = GETPKTBUF(pkt, TCPSENDBUF);
|
|
sendbuf->refcount = 1;
|
|
sendbuf->retries = 0;
|
|
sendbuf->tcpflags = tcpflags;
|
|
sendbuf->tcphdrlen = (BYTE) ((tcpflags & TCP_SYN) ?
|
|
TCPHDRLEN+SYNTCPOPTLEN :
|
|
TCPHDRLEN);
|
|
sendbuf->seq = tcb->snd_end;
|
|
|
|
if (tcpflags & (TCP_SYN|TCP_FIN)) {
|
|
sendbuf->datalen = 0;
|
|
sendbuf->seqnext = sendbuf->seq + 1;
|
|
} else {
|
|
sendbuf->datalen = pkt->datalen - TCPHDRLEN;
|
|
sendbuf->seqnext = sendbuf->seq + sendbuf->datalen;
|
|
}
|
|
tcb->snd_end = sendbuf->seqnext;
|
|
|
|
tcb->sendbufSize += sendbuf->datalen;
|
|
InsertTailList(&tcb->sendbuf, &sendbuf->links);
|
|
|
|
if (!TcbHasPendingSend(tcb)) {
|
|
tcb->sendbufNext = sendbuf;
|
|
TcbStartOutput(tcb);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// TCP segment packet completion routine
|
|
//
|
|
PRIVATE VOID TcbSendCompletionProc(Packet* pkt, NTSTATUS status) {
|
|
TCPSENDBUF* sendbuf = GETPKTBUF(pkt, TCPSENDBUF);
|
|
pkt->data = sendbuf->pktdata;
|
|
pkt->datalen = sendbuf->pktdatalen;
|
|
SendbufRelease(sendbuf);
|
|
}
|
|
|
|
//
|
|
// Allocate packet buffer for sending a TCP segment
|
|
// (that could be retransmitted)
|
|
//
|
|
PRIVATE Packet*
|
|
TcbAllocSendbuf(
|
|
UINT datalen,
|
|
UINT pktflags
|
|
)
|
|
{
|
|
UINT hdrlen;
|
|
Packet* pkt;
|
|
TCPSENDBUF* sendbuf;
|
|
|
|
hdrlen = sizeof(TCPSENDBUF) + MAXLINKHDRLEN + MAXIPHDRLEN;
|
|
datalen += TCPHDRLEN;
|
|
pkt = XnetAllocPacket(hdrlen + datalen, pktflags);
|
|
if (!pkt) return NULL;
|
|
|
|
pkt->data += hdrlen;
|
|
pkt->datalen = datalen;
|
|
sendbuf = GETPKTBUF(pkt, TCPSENDBUF);
|
|
sendbuf->pktdata = pkt->data;
|
|
sendbuf->pktdatalen = pkt->datalen;
|
|
|
|
XnetSetPacketCompletion(pkt, TcbSendCompletionProc);
|
|
return pkt;
|
|
}
|
|
|
|
|
|
NTSTATUS
|
|
TcbSend(
|
|
TCB* tcb,
|
|
SENDREQ* sendreq
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Handle a user request to send data from a TCP socket
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
sendreq - Points to the send request information
|
|
|
|
Return Value:
|
|
|
|
Status code
|
|
|
|
Note:
|
|
|
|
This function always return immediately. For blocking operation,
|
|
we assume the caller has taken care of any necessary wait.
|
|
|
|
--*/
|
|
|
|
{
|
|
NTSTATUS status = NETERR_OK;
|
|
Packet* pkt = NULL;
|
|
WSABUF* bufs = sendreq->bufs;
|
|
UINT bytesToSend = sendreq->sendtotal;
|
|
UINT mss = tcb->snd_mss - tcb->ipoptlen;
|
|
UINT n, datalen;
|
|
BYTE* data;
|
|
KIRQL irql;
|
|
|
|
// Make sure we don't overflow the send buffer too much.
|
|
// In the following case:
|
|
// old sendbuf size + the datagram size > max sendbuf size
|
|
// we'll temporarily exceed the send buffer size limit a little bit.
|
|
ASSERT(!IsPcbSendBufFull(tcb));
|
|
n = tcb->maxSendBufsize - tcb->sendbufSize;
|
|
if (bytesToSend > max(n, mss))
|
|
sendreq->sendtotal = bytesToSend = mss;
|
|
|
|
data = bufs->buf;
|
|
datalen = bufs->len;
|
|
|
|
while (bytesToSend > 0) {
|
|
UINT segsize = min(mss, bytesToSend);
|
|
BYTE* p;
|
|
|
|
pkt = TcbAllocSendbuf(segsize, 0);
|
|
if (!pkt) {
|
|
if ((sendreq->sendtotal -= bytesToSend) == 0) {
|
|
// If we've already send out some data,
|
|
// then return success with partial byte count.
|
|
// Otherwise, return error code.
|
|
status = NETERR_MEMORY;
|
|
}
|
|
break;
|
|
}
|
|
|
|
bytesToSend -= segsize;
|
|
p = pkt->data + TCPHDRLEN;
|
|
if (segsize <= datalen) {
|
|
// Fast case: all the data for this segment
|
|
// comes from the same user buffer.
|
|
CopyMem(p, data, segsize);
|
|
data += segsize;
|
|
datalen -= segsize;
|
|
} else {
|
|
// Slow case: need to gather data for this segment
|
|
// from multiple user buffers.
|
|
while (segsize) {
|
|
while (datalen == 0) {
|
|
bufs++;
|
|
data = bufs->buf;
|
|
datalen = bufs->len;
|
|
}
|
|
n = min(segsize, datalen);
|
|
CopyMem(p, data, n);
|
|
p += n;
|
|
data += n;
|
|
datalen -= n;
|
|
segsize -= n;
|
|
}
|
|
}
|
|
|
|
irql = RaiseToDpc();
|
|
status = PcbGetErrStatus(tcb);
|
|
if (NT_SUCCESS(status)) {
|
|
TcbQueueSegment(tcb, pkt, (BYTE) (bytesToSend ? TCP_ACK : (TCP_ACK|TCP_PSH)));
|
|
LowerFromDpc(irql);
|
|
} else {
|
|
XnetFreePacket(pkt);
|
|
LowerFromDpc(irql);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the app calls connect() on a non-blocking socket, and then calls send()
|
|
// without calling select(), then we need to reset the connectSelected flag
|
|
// so future select() calls will ignore PCBEVENT_CONNECT.
|
|
tcb->connectSelected = 1;
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
VOID
|
|
TcbEmitRST(
|
|
IfInfo* ifp,
|
|
IpAddrPair* addrpair,
|
|
TCPSEQ seq,
|
|
TCPSEQ ack,
|
|
BYTE flags
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Send out a TCP RST segment in response to a received packet
|
|
|
|
Arguments:
|
|
|
|
ifp - Points the interface involved
|
|
addrpair - Specifies the source and destination
|
|
seq, ack - Specifies the sequence and acknowledgement number
|
|
flag - Specifies the TCP segment flags
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
{
|
|
Packet* pkt;
|
|
|
|
pkt = XnetAllocIpPacket(0, TCPHDRLEN);
|
|
if (!pkt) return;
|
|
|
|
FILL_TCPHEADER(
|
|
pkt,
|
|
addrpair,
|
|
seq,
|
|
ack,
|
|
TCPHDRLEN,
|
|
(BYTE) (TCP_RST|flags),
|
|
0);
|
|
|
|
IpSendPacketInternal(pkt, addrpair->srcaddr, addrpair->dstaddr, IPPROTOCOL_TCP, ifp);
|
|
}
|
|
|
|
|
|
NTSTATUS
|
|
TcbEmitSYN(
|
|
TCB* tcb,
|
|
BOOL synAck
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Emit a TCP SYN segment
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
synAck - Whether to send ACK as well
|
|
|
|
Return Value:
|
|
|
|
Status code
|
|
|
|
--*/
|
|
|
|
{
|
|
Packet* pkt;
|
|
BYTE* opt;
|
|
TCPSENDBUF* sendbuf;
|
|
|
|
// Check if we're already trying to send out a SYN segment
|
|
if (!IsPcbSendBufEmpty(tcb)) {
|
|
sendbuf = (TCPSENDBUF*) RemoveHeadList(&tcb->sendbuf);
|
|
SendbufRelease(sendbuf);
|
|
ASSERT(IsPcbSendBufEmpty(tcb));
|
|
ASSERT(!TcbHasPendingSend(tcb));
|
|
}
|
|
|
|
tcb->snd_una = tcb->snd_nxt = tcb->snd_end = tcb->snd_isn;
|
|
|
|
// Allocate a TCP send buffer for possible retransmission
|
|
pkt = TcbAllocSendbuf(SYNTCPOPTLEN, synAck ? PKTFLAG_NETPOOL : 0);
|
|
if (!pkt) {
|
|
// This is very bad - we failed to allocate memory
|
|
// for the SYN segment. We'll mark the TCB as dead
|
|
// and return an error code.
|
|
TcbReset(tcb, NETERR_MEMORY);
|
|
return NETERR_MEMORY;
|
|
}
|
|
|
|
TcbSetSynTimer(tcb, cfgConnectTimeout);
|
|
|
|
opt = pkt->data + TCPHDRLEN;
|
|
opt[0] = TCPOPT_MAX_SEGSIZE;
|
|
opt[1] = SYNTCPOPTLEN;
|
|
opt[2] = (BYTE) (tcb->rcv_mss >> 8);
|
|
opt[3] = (BYTE) tcb->rcv_mss;
|
|
|
|
TcbQueueSegment(tcb, pkt, (BYTE) (synAck ? TCP_SYN|TCP_ACK : TCP_SYN));
|
|
return NETERR_OK;
|
|
}
|
|
|
|
|
|
VOID
|
|
TcbEmitACK(
|
|
TCB* tcb
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Send out an ACK segment on a TCP connection
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
|
|
Return Value:
|
|
|
|
NONE
|
|
|
|
--*/
|
|
|
|
{
|
|
TCPSEQ seq;
|
|
Packet* pkt = XnetAllocPacket(MAXLINKHDRLEN + MAXIPHDRLEN + TCPHDRLEN, PKTFLAG_NETPOOL);
|
|
if (!pkt) return;
|
|
|
|
pkt->data += MAXLINKHDRLEN + MAXIPHDRLEN;
|
|
pkt->datalen = TCPHDRLEN;
|
|
|
|
seq = TcbSndNxt(tcb);
|
|
FILL_TCPHEADER(
|
|
pkt,
|
|
&tcb->addrpair,
|
|
seq,
|
|
tcb->rcv_nxt,
|
|
TCPHDRLEN,
|
|
TCP_ACK,
|
|
(WORD) tcb->rcv_wnd);
|
|
|
|
tcb->delayedAcks = 0;
|
|
PcbSetupIpHeader((PCB*) tcb, pkt, &tcb->addrpair);
|
|
IpSendPacket(pkt, tcb->dstaddr, NULL, &tcb->rte);
|
|
}
|
|
|
|
|
|
NTSTATUS
|
|
TcbEmitFIN(
|
|
TCB* tcb
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Send out a FIN segment on a TCP connection
|
|
|
|
Arguments:
|
|
|
|
tcb - Points to the TCP control block
|
|
|
|
Return Value:
|
|
|
|
Status code
|
|
|
|
--*/
|
|
|
|
{
|
|
Packet* pkt = TcbAllocSendbuf(0, 0);
|
|
if (!pkt) return NETERR_MEMORY;
|
|
TcbQueueSegment(tcb, pkt, TCP_ACK|TCP_FIN);
|
|
return NETERR_OK;
|
|
}
|
|
|