From ad30bad1a66ce56430a9e51a7ddf231df178e75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 20 Apr 2025 16:16:06 -0300 Subject: [PATCH 01/40] feat: initial TcpState implementation --- src/types/network-modules/tcpModule.ts | 223 ++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 817b13d..42cab15 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -148,7 +148,7 @@ export class TcpModule { } } -const MAX_BUFFER_SIZE = 65535; +const MAX_BUFFER_SIZE = 0xffff; export class TcpSocket { private srcHost: ViewHost; @@ -302,6 +302,227 @@ export class TcpListener { } } +// TODO: add overflow checks +class TcpState { + private srcPort: Port; + private dstPort: Port; + + // Buffer of data received + private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private readClosed = false; + + // Buffer of data to be sent + private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private writeClosed = false; + + // See https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.1 + // SND.UNA + private sendUnacknowledged: number; + // SND.NXT + private sendNext: number; + // SND.WND + private sendWindow: number; + // SND.UP + // private sendUrgentPointer; + // SND.WL1 + private seqNumForLastWindowUpdate: number; + // SND.WL2 + private ackNumForLastWindowUpdate: number; + // ISS + private initialSendSeqNum; + + // RCV.NXT + private recvNext: number; + // RCV.WND + private recvWindow: number; + + // IRS + private initialRecvSeqNum: number; + + constructor(srcPort: Port, dstPort: Port) { + this.srcPort = srcPort; + this.dstPort = dstPort; + this.initialSendSeqNum = getInitialSeqNumber(); + } + + processSynAck(segment: TcpSegment) { + if (!segment.flags.syn || !segment.flags.ack) { + return false; + } + if (segment.acknowledgementNumber !== this.initialSendSeqNum + 1) { + return false; + } + } + + read(output: Uint8Array): number { + const readLength = this.readBuffer.read(output); + if (readLength === 0 && this.readClosed) { + return -1; + } + return readLength; + } + + write(input: Uint8Array): number { + if (this.writeClosed) { + throw new Error("write closed"); + } + return this.writeBuffer.write(input); + } + + recvSegment(receivedSegment: TcpSegment) { + // Sanity check: ports match with expected + if ( + receivedSegment.destinationPort !== this.dstPort || + receivedSegment.sourcePort !== this.srcPort + ) { + throw new Error("segment not for this socket"); + } + // Check the sequence number is valid + const segSeq = receivedSegment.sequenceNumber; + const segLen = receivedSegment.data.length; + if (!this.isSeqNumValid(segSeq, segLen)) { + return false; + } + + // TODO: check RST flag + + // For now this only accepts ACK segments. + // 3WHS are handled outside of this function + if (!receivedSegment.flags.ack) { + return false; + } + if (!this.recvAck(receivedSegment)) { + return false; + } + + // Process the segment data + // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ + if (receivedSegment.sequenceNumber === this.recvNext) { + let receivedData = receivedSegment.data; + // NOTE: for simplicity, we ignore cases where the data is only partially + // inside the window + if (receivedData.length > this.recvWindow) { + throw new Error("buffer overflow"); + } + this.readBuffer.write(receivedData); + this.recvNext = receivedSegment.sequenceNumber + receivedData.length; + this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); + } + + // If FIN, mark read end as closed + if (receivedSegment.flags.fin) { + this.readClosed = true; + } + return true; + } + + nextSegment(): TcpSegment | null { + const segment = new TcpSegment( + 0, + 0, + this.seqNum, + this.ackNum, + new Flags(), + new Uint8Array(), + ); + return segment; + } + + // utils + + private isSeqNumValid(segSeq: number, segLen: number) { + const lengthIsZero = segLen === 0; + const windowIsZero = this.recvWindow === 0; + + if (lengthIsZero && windowIsZero) { + return segSeq === this.recvNext; + } else if (lengthIsZero && !windowIsZero) { + return this.isInReceiveWindow(segSeq); + } else if (!lengthIsZero && windowIsZero) { + return false; + } else { + return ( + this.isInReceiveWindow(segSeq) || + this.isInReceiveWindow(segSeq + segLen - 1) + ); + } + } + + private isInReceiveWindow(n: number) { + return this.recvNext <= n && n < this.recvNext + this.recvWindow; + } + + private recvAck(segment: TcpSegment) { + // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 + // If the ACK is for a packet not yet sent, drop it + if (this.sendNext < segment.acknowledgementNumber) { + return false; + } + // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK + if (this.sendUnacknowledged <= segment.acknowledgementNumber) { + this.sendUnacknowledged = segment.acknowledgementNumber; + if (this.isSegmentNewer(segment)) { + // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. + this.sendWindow = segment.window; + this.seqNumForLastWindowUpdate = segment.sequenceNumber; + this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; + } + } + } + + private isSegmentNewer(segment: TcpSegment): boolean { + // Since both SEQ and ACK numbers are monotonic, we can use + // them to determine if the segment is newer than the last + // one that was used for updating the window + // + // SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ and SND.WL2 =< SEG.ACK) + return ( + this.seqNumForLastWindowUpdate < segment.sequenceNumber || + (this.seqNumForLastWindowUpdate === segment.sequenceNumber && + this.ackNumForLastWindowUpdate <= segment.acknowledgementNumber) + ); + } +} + +class BytesBuffer { + private buffer: Uint8Array; + private length: number; + + constructor(size: number) { + this.buffer = new Uint8Array(size); + this.length = 0; + } + + read(output: Uint8Array): number { + const readLength = Math.min(this.length, output.length); + if (readLength == 0) { + return 0; + } + output.set(this.buffer.subarray(0, readLength)); + this.buffer.copyWithin(0, readLength, this.length); + this.length -= readLength; + return readLength; + } + + write(data: Uint8Array): number { + const newLength = this.length + data.length; + if (newLength > this.buffer.length) { + return 0; + } + this.buffer.set(data, this.length); + this.length = newLength; + return data.length; + } + + bytesAvailable() { + return this.length; + } + + isEmpty() { + return this.bytesAvailable() === 0; + } +} + function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { const viewgraph = src.viewgraph; From 20d1af9d04e4cea8f41ac19671bafa12c0a5e494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:20:03 -0300 Subject: [PATCH 02/40] feat: implement connection and segmentation --- src/packets/tcp.ts | 18 +- src/types/network-modules/tcp/tcpState.ts | 306 ++++++++++++++++++++++ src/types/network-modules/tcpModule.ts | 230 +--------------- 3 files changed, 321 insertions(+), 233 deletions(-) create mode 100644 src/types/network-modules/tcp/tcpState.ts diff --git a/src/packets/tcp.ts b/src/packets/tcp.ts index e376dff..c970670 100644 --- a/src/packets/tcp.ts +++ b/src/packets/tcp.ts @@ -147,10 +147,10 @@ export class TcpSegment implements IpPayload { constructor( srcPort: number, dstPort: number, - seqNum: number, - ackNum: number, - flags: Flags, - data: Uint8Array, + seqNum: number = 0, + ackNum: number = 0, + flags: Flags = new Flags(), + data: Uint8Array = new Uint8Array(0), ) { checkUint(srcPort, 16); checkUint(dstPort, 16); @@ -165,6 +165,16 @@ export class TcpSegment implements IpPayload { this.data = data; } + withFlags(flags: Flags): TcpSegment { + this.flags = flags; + return this; + } + + withData(data: Uint8Array): TcpSegment { + this.data = data; + return this; + } + computeChecksum(): number { const segmentBytes = this.toBytes({ withChecksum: false }); diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts new file mode 100644 index 0000000..7acb52b --- /dev/null +++ b/src/types/network-modules/tcp/tcpState.ts @@ -0,0 +1,306 @@ +import { Flags, TcpSegment } from "../../../packets/tcp"; + +// TODO: import +type Port = number; + +const MAX_BUFFER_SIZE = 0xffff; + +function getInitialSeqNumber() { + return Math.floor(Math.random() * 0xffffffff); +} + +// TODO: add overflow checks +class TcpState { + private srcPort: Port; + private dstPort: Port; + + // Buffer of data received + private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private readClosed = false; + + // Buffer of data to be sent + private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private writeClosed = false; + + // See https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.1 + // SND.UNA + private sendUnacknowledged: number; + // SND.NXT + private sendNext: number; + // SND.WND + private sendWindow: number; + // SND.UP + // private sendUrgentPointer: number; + // SND.WL1 + private seqNumForLastWindowUpdate: number; + // SND.WL2 + private ackNumForLastWindowUpdate: number; + // ISS + private initialSendSeqNum; + + // RCV.NXT + private recvNext: number; + // RCV.WND + private recvWindow = MAX_BUFFER_SIZE; + + // IRS + // private initialRecvSeqNum: number; + + private sendSegment: (tcpSegment: TcpSegment) => void; + + constructor( + srcPort: Port, + dstPort: Port, + sendSegment: (tcpSegment: TcpSegment) => void, + ) { + this.srcPort = srcPort; + this.dstPort = dstPort; + this.initialSendSeqNum = getInitialSeqNumber(); + this.sendSegment = sendSegment; + } + + startConnection() { + const flags = new Flags().withSyn(); + const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); + this.sendSegment(segment); + } + + recvSynAck(segment: TcpSegment) { + if (!segment.flags.syn || !segment.flags.ack) { + return false; + } + if (segment.acknowledgementNumber !== this.initialSendSeqNum + 1) { + return false; + } + this.recvNext = segment.sequenceNumber + 1; + // this.initialRecvSeqNum = segment.sequenceNumber; + this.sendNext = segment.acknowledgementNumber; + this.sendWindow = segment.window; + + const ackSegment = this.newSegment(this.sendNext, this.recvNext); + ackSegment.withFlags(new Flags().withAck()); + this.sendSegment(ackSegment); + } + + recvSegment(receivedSegment: TcpSegment) { + // Sanity check: ports match with expected + if ( + receivedSegment.destinationPort !== this.dstPort || + receivedSegment.sourcePort !== this.srcPort + ) { + throw new Error("segment not for this socket"); + } + // Check the sequence number is valid + const segSeq = receivedSegment.sequenceNumber; + const segLen = receivedSegment.data.length; + if (!this.isSeqNumValid(segSeq, segLen)) { + return false; + } + + // TODO: check RST flag + + // For now this only accepts ACK segments. + // 3WHS are handled outside of this function + if (!receivedSegment.flags.ack) { + return false; + } + if (!this.recvAck(receivedSegment)) { + return false; + } + + // Process the segment data + // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ + if (receivedSegment.sequenceNumber === this.recvNext) { + let receivedData = receivedSegment.data; + // NOTE: for simplicity, we ignore cases where the data is only partially + // inside the window + if (receivedData.length > this.recvWindow) { + throw new Error("buffer overflow"); + } + this.readBuffer.write(receivedData); + this.recvNext = receivedSegment.sequenceNumber + receivedData.length; + this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); + } + + // If FIN, mark read end as closed + if (receivedSegment.flags.fin) { + this.readClosed = true; + } + return true; + } + + read(output: Uint8Array): number { + const readLength = this.readBuffer.read(output); + if (readLength === 0 && this.readClosed) { + return -1; + } + return readLength; + } + + write(input: Uint8Array): number { + if (this.writeClosed) { + throw new Error("write closed"); + } + const writeLength = this.writeBuffer.write(input); + if (this.sendWindow > 0 && writeLength > 0) { + this.sendSegment(this.produceSegment()); + } + return writeLength; + } + + closeWrite() { + this.writeClosed = true; + const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withFin().withAck(), + ); + this.sendNext++; + this.sendSegment(segment); + } + + // utils + + private newSegment(seqNum: number, ackNum: number) { + return new TcpSegment(this.srcPort, this.dstPort, seqNum, ackNum); + } + + private produceSegment() { + const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withAck(), + ); + + if (this.writeBuffer.bytesAvailable() > 0 && this.sendWindow > 0) { + const data = new Uint8Array(this.sendWindow); + const writeLength = this.writeBuffer.read(data); + segment.withData(data.subarray(0, writeLength)); + this.sendNext += writeLength; + this.sendWindow -= writeLength; + } + return segment; + } + + private isSeqNumValid(segSeq: number, segLen: number) { + const lengthIsZero = segLen === 0; + const windowIsZero = this.recvWindow === 0; + + if (lengthIsZero && windowIsZero) { + return segSeq === this.recvNext; + } else if (lengthIsZero && !windowIsZero) { + return this.isInReceiveWindow(segSeq); + } else if (!lengthIsZero && windowIsZero) { + return false; + } else { + return ( + this.isInReceiveWindow(segSeq) || + this.isInReceiveWindow(segSeq + segLen - 1) + ); + } + } + + private isInReceiveWindow(n: number) { + return this.recvNext <= n && n < this.recvNext + this.recvWindow; + } + + private recvAck(segment: TcpSegment) { + // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 + // If the ACK is for a packet not yet sent, drop it + if (this.sendNext < segment.acknowledgementNumber) { + return false; + } + // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK + if (this.sendUnacknowledged <= segment.acknowledgementNumber) { + this.sendUnacknowledged = segment.acknowledgementNumber; + if (this.isSegmentNewer(segment)) { + // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. + this.sendWindow = segment.window; + this.seqNumForLastWindowUpdate = segment.sequenceNumber; + this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; + } + } + } + + private isSegmentNewer(segment: TcpSegment): boolean { + // Since both SEQ and ACK numbers are monotonic, we can use + // them to determine if the segment is newer than the last + // one that was used for updating the window + // + // SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ and SND.WL2 =< SEG.ACK) + return ( + this.seqNumForLastWindowUpdate < segment.sequenceNumber || + (this.seqNumForLastWindowUpdate === segment.sequenceNumber && + this.ackNumForLastWindowUpdate <= segment.acknowledgementNumber) + ); + } +} + +interface RetransmissionQueueItem { + segment: TcpSegment; + timeoutPromise: Promise; +} + +function sleep(ms?: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const RETRANSMIT_TIMEOUT = 60 * 1000; + +class RetransmissionQueue { + private queue: RetransmissionQueueItem[] = []; + + push(segment: TcpSegment) { + const timeoutPromise = sleep(RETRANSMIT_TIMEOUT); + // Inserts the segment at the beginning of the queue + this.queue.unshift({ segment, timeoutPromise }); + } + + async pop() { + if (this.isEmpty()) { + return null; + } + const item = this.queue.shift(); + await item.timeoutPromise; + return item.segment; + } + + isEmpty() { + return this.queue.length === 0; + } +} + +class BytesBuffer { + private buffer: Uint8Array; + private length: number; + + constructor(size: number) { + this.buffer = new Uint8Array(size); + this.length = 0; + } + + read(output: Uint8Array): number { + const readLength = Math.min(this.length, output.length); + if (readLength == 0) { + return 0; + } + output.set(this.buffer.subarray(0, readLength)); + this.buffer.copyWithin(0, readLength, this.length); + this.length -= readLength; + return readLength; + } + + write(data: Uint8Array): number { + const newLength = this.length + data.length; + if (newLength > this.buffer.length) { + return 0; + } + this.buffer.set(data, this.length); + this.length = newLength; + return data.length; + } + + bytesAvailable() { + return this.length; + } + + isEmpty() { + return this.bytesAvailable() === 0; + } +} diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 42cab15..c17e387 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -61,14 +61,7 @@ export class TcpModule { const flags = new Flags().withSyn(); const srcPort: Port = this.getNextPortNumber(); const seqNum = getInitialSeqNumber(); - const synSegment = new TcpSegment( - srcPort, - dstPort, - seqNum, - 0, - flags, - new Uint8Array(), - ); + const synSegment = new TcpSegment(srcPort, dstPort, seqNum, 0, flags); // Send SYN sendIpPacket(this.host, dstHost, synSegment); @@ -302,227 +295,6 @@ export class TcpListener { } } -// TODO: add overflow checks -class TcpState { - private srcPort: Port; - private dstPort: Port; - - // Buffer of data received - private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); - private readClosed = false; - - // Buffer of data to be sent - private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); - private writeClosed = false; - - // See https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.1 - // SND.UNA - private sendUnacknowledged: number; - // SND.NXT - private sendNext: number; - // SND.WND - private sendWindow: number; - // SND.UP - // private sendUrgentPointer; - // SND.WL1 - private seqNumForLastWindowUpdate: number; - // SND.WL2 - private ackNumForLastWindowUpdate: number; - // ISS - private initialSendSeqNum; - - // RCV.NXT - private recvNext: number; - // RCV.WND - private recvWindow: number; - - // IRS - private initialRecvSeqNum: number; - - constructor(srcPort: Port, dstPort: Port) { - this.srcPort = srcPort; - this.dstPort = dstPort; - this.initialSendSeqNum = getInitialSeqNumber(); - } - - processSynAck(segment: TcpSegment) { - if (!segment.flags.syn || !segment.flags.ack) { - return false; - } - if (segment.acknowledgementNumber !== this.initialSendSeqNum + 1) { - return false; - } - } - - read(output: Uint8Array): number { - const readLength = this.readBuffer.read(output); - if (readLength === 0 && this.readClosed) { - return -1; - } - return readLength; - } - - write(input: Uint8Array): number { - if (this.writeClosed) { - throw new Error("write closed"); - } - return this.writeBuffer.write(input); - } - - recvSegment(receivedSegment: TcpSegment) { - // Sanity check: ports match with expected - if ( - receivedSegment.destinationPort !== this.dstPort || - receivedSegment.sourcePort !== this.srcPort - ) { - throw new Error("segment not for this socket"); - } - // Check the sequence number is valid - const segSeq = receivedSegment.sequenceNumber; - const segLen = receivedSegment.data.length; - if (!this.isSeqNumValid(segSeq, segLen)) { - return false; - } - - // TODO: check RST flag - - // For now this only accepts ACK segments. - // 3WHS are handled outside of this function - if (!receivedSegment.flags.ack) { - return false; - } - if (!this.recvAck(receivedSegment)) { - return false; - } - - // Process the segment data - // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ - if (receivedSegment.sequenceNumber === this.recvNext) { - let receivedData = receivedSegment.data; - // NOTE: for simplicity, we ignore cases where the data is only partially - // inside the window - if (receivedData.length > this.recvWindow) { - throw new Error("buffer overflow"); - } - this.readBuffer.write(receivedData); - this.recvNext = receivedSegment.sequenceNumber + receivedData.length; - this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); - } - - // If FIN, mark read end as closed - if (receivedSegment.flags.fin) { - this.readClosed = true; - } - return true; - } - - nextSegment(): TcpSegment | null { - const segment = new TcpSegment( - 0, - 0, - this.seqNum, - this.ackNum, - new Flags(), - new Uint8Array(), - ); - return segment; - } - - // utils - - private isSeqNumValid(segSeq: number, segLen: number) { - const lengthIsZero = segLen === 0; - const windowIsZero = this.recvWindow === 0; - - if (lengthIsZero && windowIsZero) { - return segSeq === this.recvNext; - } else if (lengthIsZero && !windowIsZero) { - return this.isInReceiveWindow(segSeq); - } else if (!lengthIsZero && windowIsZero) { - return false; - } else { - return ( - this.isInReceiveWindow(segSeq) || - this.isInReceiveWindow(segSeq + segLen - 1) - ); - } - } - - private isInReceiveWindow(n: number) { - return this.recvNext <= n && n < this.recvNext + this.recvWindow; - } - - private recvAck(segment: TcpSegment) { - // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 - // If the ACK is for a packet not yet sent, drop it - if (this.sendNext < segment.acknowledgementNumber) { - return false; - } - // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK - if (this.sendUnacknowledged <= segment.acknowledgementNumber) { - this.sendUnacknowledged = segment.acknowledgementNumber; - if (this.isSegmentNewer(segment)) { - // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. - this.sendWindow = segment.window; - this.seqNumForLastWindowUpdate = segment.sequenceNumber; - this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; - } - } - } - - private isSegmentNewer(segment: TcpSegment): boolean { - // Since both SEQ and ACK numbers are monotonic, we can use - // them to determine if the segment is newer than the last - // one that was used for updating the window - // - // SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ and SND.WL2 =< SEG.ACK) - return ( - this.seqNumForLastWindowUpdate < segment.sequenceNumber || - (this.seqNumForLastWindowUpdate === segment.sequenceNumber && - this.ackNumForLastWindowUpdate <= segment.acknowledgementNumber) - ); - } -} - -class BytesBuffer { - private buffer: Uint8Array; - private length: number; - - constructor(size: number) { - this.buffer = new Uint8Array(size); - this.length = 0; - } - - read(output: Uint8Array): number { - const readLength = Math.min(this.length, output.length); - if (readLength == 0) { - return 0; - } - output.set(this.buffer.subarray(0, readLength)); - this.buffer.copyWithin(0, readLength, this.length); - this.length -= readLength; - return readLength; - } - - write(data: Uint8Array): number { - const newLength = this.length + data.length; - if (newLength > this.buffer.length) { - return 0; - } - this.buffer.set(data, this.length); - this.length = newLength; - return data.length; - } - - bytesAvailable() { - return this.length; - } - - isEmpty() { - return this.bytesAvailable() === 0; - } -} - function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { const viewgraph = src.viewgraph; From a8adedc63f606309d665be94a45923becbaa76ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:28:39 -0300 Subject: [PATCH 03/40] refactor: move TcpSocket to new implementation --- src/types/network-modules/tcp/tcpState.ts | 225 ++++++++++++++++------ src/types/network-modules/tcpModule.ts | 157 ++++----------- 2 files changed, 208 insertions(+), 174 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 7acb52b..f68eac3 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -1,4 +1,11 @@ +import { EthernetFrame } from "../../../packets/ethernet"; +import { IpPayload, IPv4Packet } from "../../../packets/ip"; import { Flags, TcpSegment } from "../../../packets/tcp"; +import { sendViewPacket } from "../../packet"; +import { ViewHost } from "../../view-devices"; +import { ViewNetworkDevice } from "../../view-devices/vNetworkDevice"; +import { AsyncQueue } from "../asyncQueue"; +import { SegmentWithIp } from "../tcpModule"; // TODO: import type Port = number; @@ -9,10 +16,36 @@ function getInitialSeqNumber() { return Math.floor(Math.random() * 0xffffffff); } +function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { + const viewgraph = src.viewgraph; + + // TODO: use MAC and IP of the interfaces used + let nextHopMac = dst.mac; + const path = viewgraph.getPathBetween(src.id, dst.id); + if (!path) return; + for (const id of path.slice(1)) { + const device = viewgraph.getDevice(id); + // if there’s a router in the middle, first send frame to router mac + if (device instanceof ViewNetworkDevice) { + nextHopMac = device.mac; + break; + } + } + const ipPacket = new IPv4Packet(src.ip, dst.ip, payload); + const frame = new EthernetFrame(src.mac, nextHopMac, ipPacket); + + sendViewPacket(src.viewgraph, src.id, frame); +} + // TODO: add overflow checks -class TcpState { +export class TcpState { + private srcHost: ViewHost; private srcPort: Port; + private dstHost: ViewHost; private dstPort: Port; + private tcpQueue: AsyncQueue; + private sendQueue = new AsyncQueue(); + private sendingPackets: Promise | null = null; // Buffer of data received private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); @@ -20,7 +53,7 @@ class TcpState { // Buffer of data to be sent private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); - private writeClosed = false; + private writeClosedSeqnum = -1; // See https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.1 // SND.UNA @@ -46,23 +79,26 @@ class TcpState { // IRS // private initialRecvSeqNum: number; - private sendSegment: (tcpSegment: TcpSegment) => void; - constructor( + srcHost: ViewHost, srcPort: Port, + dstHost: ViewHost, dstPort: Port, - sendSegment: (tcpSegment: TcpSegment) => void, + tcpQueue: AsyncQueue, ) { + this.srcHost = srcHost; this.srcPort = srcPort; + this.dstHost = dstHost; this.dstPort = dstPort; + this.tcpQueue = tcpQueue; + this.initialSendSeqNum = getInitialSeqNumber(); - this.sendSegment = sendSegment; } startConnection() { const flags = new Flags().withSyn(); const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); - this.sendSegment(segment); + sendIpPacket(this.srcHost, this.dstHost, segment); } recvSynAck(segment: TcpSegment) { @@ -79,7 +115,12 @@ class TcpState { const ackSegment = this.newSegment(this.sendNext, this.recvNext); ackSegment.withFlags(new Flags().withAck()); - this.sendSegment(ackSegment); + sendIpPacket(this.srcHost, this.dstHost, ackSegment); + + // start sending packets + if (!this.sendingPackets) { + this.sendingPackets = this.mainLoop(); + } } recvSegment(receivedSegment: TcpSegment) { @@ -104,7 +145,7 @@ class TcpState { if (!receivedSegment.flags.ack) { return false; } - if (!this.recvAck(receivedSegment)) { + if (!this.processAck(receivedSegment)) { return false; } @@ -120,12 +161,21 @@ class TcpState { this.readBuffer.write(receivedData); this.recvNext = receivedSegment.sequenceNumber + receivedData.length; this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); + // We should send back an ACK segment + this.notifySendPackets(); } // If FIN, mark read end as closed if (receivedSegment.flags.fin) { + // The flag counts as a byte + this.recvNext++; this.readClosed = true; } + + // start sending packets + if (!this.sendingPackets) { + this.sendingPackets = this.mainLoop(); + } return true; } @@ -134,27 +184,28 @@ class TcpState { if (readLength === 0 && this.readClosed) { return -1; } + this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); return readLength; } write(input: Uint8Array): number { - if (this.writeClosed) { + if (this.writeClosedSeqnum >= 0) { throw new Error("write closed"); } const writeLength = this.writeBuffer.write(input); if (this.sendWindow > 0 && writeLength > 0) { - this.sendSegment(this.produceSegment()); + this.notifySendPackets(); } return writeLength; } closeWrite() { - this.writeClosed = true; + this.writeClosedSeqnum = this.sendNext; const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( new Flags().withFin().withAck(), ); this.sendNext++; - this.sendSegment(segment); + sendIpPacket(this.srcHost, this.dstHost, segment); } // utils @@ -163,21 +214,6 @@ class TcpState { return new TcpSegment(this.srcPort, this.dstPort, seqNum, ackNum); } - private produceSegment() { - const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( - new Flags().withAck(), - ); - - if (this.writeBuffer.bytesAvailable() > 0 && this.sendWindow > 0) { - const data = new Uint8Array(this.sendWindow); - const writeLength = this.writeBuffer.read(data); - segment.withData(data.subarray(0, writeLength)); - this.sendNext += writeLength; - this.sendWindow -= writeLength; - } - return segment; - } - private isSeqNumValid(segSeq: number, segLen: number) { const lengthIsZero = segLen === 0; const windowIsZero = this.recvWindow === 0; @@ -200,7 +236,7 @@ class TcpState { return this.recvNext <= n && n < this.recvNext + this.recvWindow; } - private recvAck(segment: TcpSegment) { + private processAck(segment: TcpSegment) { // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 // If the ACK is for a packet not yet sent, drop it if (this.sendNext < segment.acknowledgementNumber) { @@ -230,42 +266,100 @@ class TcpState { this.ackNumForLastWindowUpdate <= segment.acknowledgementNumber) ); } -} -interface RetransmissionQueueItem { - segment: TcpSegment; - timeoutPromise: Promise; -} + private notifiedSendPackets = false; -function sleep(ms?: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} + private notifySendPackets() { + if (this.notifiedSendPackets) { + return; + } + this.notifiedSendPackets = true; + setTimeout(() => this.sendQueue.push(undefined), 50); + } -const RETRANSMIT_TIMEOUT = 60 * 1000; + private async mainLoop() { + const MAX_SEGMENT_SIZE = 1400; -class RetransmissionQueue { - private queue: RetransmissionQueueItem[] = []; + let recheckPromise = this.sendQueue.pop(); + let receivedSegmentPromise = this.tcpQueue.pop(); - push(segment: TcpSegment) { - const timeoutPromise = sleep(RETRANSMIT_TIMEOUT); - // Inserts the segment at the beginning of the queue - this.queue.unshift({ segment, timeoutPromise }); - } + let retransmitTimeoutId: NodeJS.Timeout | null = null; + + const clearTimer = () => { + if (retransmitTimeoutId === null) { + return; + } + clearTimeout(retransmitTimeoutId); + retransmitTimeoutId = null; + }; + + const setTimer = () => { + if (retransmitTimeoutId === null) { + return; + } + retransmitTimeoutId = setTimeout(() => { + retransmitTimeoutId = null; + this.sendNext = this.sendUnacknowledged; + this.notifySendPackets(); + }, RETRANSMIT_TIMEOUT); + }; + + while (true) { + const result = await Promise.race([ + recheckPromise, + receivedSegmentPromise, + ]); + + if (result !== undefined) { + receivedSegmentPromise = this.tcpQueue.pop(); + if (!this.recvSegment(result.segment)) { + continue; + } + // If we got a valid segment, we can refresh the timer + clearTimer(); + } else { + recheckPromise = this.sendQueue.pop(); + } - async pop() { - if (this.isEmpty()) { - return null; + do { + const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withAck(), + ); + + const sendSize = Math.min(this.sendWindowSize(), MAX_SEGMENT_SIZE); + if (sendSize > 0) { + const data = new Uint8Array(sendSize); + const writeLength = this.writeBuffer.peek(this.sendNext, data); + + if (writeLength > 0) { + segment.withData(data.subarray(0, writeLength)); + this.sendNext += writeLength; + } + } + segment.window = this.recvWindow; + + if (this.sendNext === this.writeClosedSeqnum) { + this.sendNext++; + segment.flags.withFin(); + } + sendIpPacket(this.srcHost, this.dstHost, segment); + // Repeat until we have no more data to send + } while (this.writeBuffer.bytesAvailable() > this.sendWindowSize()); } - const item = this.queue.shift(); - await item.timeoutPromise; - return item.segment; } - isEmpty() { - return this.queue.length === 0; + private sendWindowSize() { + // TODO: add congestion control + return this.sendUnacknowledged + this.sendWindow - this.sendNext; } } +function sleep(ms?: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const RETRANSMIT_TIMEOUT = 60 * 1000; + class BytesBuffer { private buffer: Uint8Array; private length: number; @@ -275,14 +369,29 @@ class BytesBuffer { this.length = 0; } - read(output: Uint8Array): number { - const readLength = Math.min(this.length, output.length); + peek(offset: number, output: Uint8Array): number { + if (offset > this.length) { + return 0; + } + const readLength = Math.min(this.length - offset, output.length); if (readLength == 0) { return 0; } - output.set(this.buffer.subarray(0, readLength)); - this.buffer.copyWithin(0, readLength, this.length); - this.length -= readLength; + output.set(this.buffer.subarray(offset, readLength + offset)); + return readLength; + } + + shift(offset: number) { + if (offset > this.length) { + throw new Error("offset is greater than length"); + } + this.buffer.copyWithin(0, offset, this.length); + this.length -= offset; + } + + read(output: Uint8Array): number { + const readLength = this.peek(0, output); + this.shift(readLength); return readLength; } diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index c17e387..29cbb4c 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -5,6 +5,7 @@ import { sendViewPacket } from "../packet"; import { ViewHost } from "../view-devices"; import { ViewNetworkDevice } from "../view-devices/vNetworkDevice"; import { AsyncQueue } from "./asyncQueue"; +import { TcpState } from "./tcp/tcpState"; type Port = number; @@ -13,7 +14,7 @@ interface IpAndPort { port: Port; } -interface SegmentWithIp { +export interface SegmentWithIp { srcIp: IpAddress; segment: TcpSegment; } @@ -58,35 +59,30 @@ export class TcpModule { } async connect(dstHost: ViewHost, dstPort: Port) { - const flags = new Flags().withSyn(); const srcPort: Port = this.getNextPortNumber(); - const seqNum = getInitialSeqNumber(); - const synSegment = new TcpSegment(srcPort, dstPort, seqNum, 0, flags); - // Send SYN - sendIpPacket(this.host, dstHost, synSegment); - - // Receive SYN-ACK - // TODO: check packet is valid response const filter = { ip: dstHost.ip, port: dstPort }; const tcpQueue = this.initNewQueue(srcPort, filter); - // TODO: validate response - await tcpQueue.pop(); - - const ackFlags = new Flags().withAck(); - - // Send ACK - const ackSegment = new TcpSegment( + const tcpState = new TcpState( + this.host, srcPort, + dstHost, dstPort, - 0, - 0, - ackFlags, - new Uint8Array(), + tcpQueue, ); - sendIpPacket(this.host, dstHost, ackSegment); - return new TcpSocket(this.host, srcPort, dstHost, dstPort, tcpQueue); + const skt = new TcpSocket(tcpState); + + // Retry on failure + for (let i = 0; i < 3; i++) { + tcpState.startConnection(); + const response = await tcpQueue.pop(); + const ok = tcpState.recvSynAck(response.segment); + if (ok) { + return skt; + } + } + return null; } async listenOn(port: Port): Promise { @@ -144,31 +140,10 @@ export class TcpModule { const MAX_BUFFER_SIZE = 0xffff; export class TcpSocket { - private srcHost: ViewHost; - private srcPort: Port; - - private dstHost: ViewHost; - private dstPort: Port; - - private tcpQueue: AsyncQueue; - private readClosed = false; - private writeClosed = false; + private tcpState: TcpState; - private readBuffer = new Uint8Array(MAX_BUFFER_SIZE); - private bufferLength = 0; - - constructor( - srcHost: ViewHost, - srcPort: Port, - dstHost: ViewHost, - dstPort: Port, - tcpQueue: AsyncQueue, - ) { - this.srcHost = srcHost; - this.dstHost = dstHost; - this.srcPort = srcPort; - this.dstPort = dstPort; - this.tcpQueue = tcpQueue; + constructor(tcpState: TcpState) { + this.tcpState = tcpState; } /** @@ -179,71 +154,15 @@ export class TcpSocket { * @returns the number of bytes read */ async read(buffer: Uint8Array) { - // While we don't have data, wait for more packets - while (this.bufferLength < buffer.length && !this.readClosed) { - const { segment } = await this.tcpQueue.pop(); - // TODO: validate payload - const data = segment.data; - const newLength = this.bufferLength + data.length; - if (newLength > MAX_BUFFER_SIZE) { - throw new Error("Buffer overflow"); - } - this.readBuffer.set(data, this.bufferLength); - this.bufferLength = newLength; - - // If segment has FIN, the connection was closed - if (segment.flags.fin) { - this.readClosed = true; - break; - } - } - // Copy partially if connection was closed, if not, fill the buffer - const readLength = Math.min(this.bufferLength, buffer.length); - if (readLength === 0) { - if (this.readClosed) { - console.error("tried to read from a closed socket"); - return -1; - } - return 0; - } - // Copy the data to the buffer - buffer.set(this.readBuffer.subarray(0, readLength)); - this.readBuffer.copyWithin(0, readLength + 1, this.bufferLength); - this.bufferLength -= readLength; - return readLength; + return this.tcpState.read(buffer); } async write(content: Uint8Array) { - if (this.writeClosed) { - console.error("tried to write to a closed socket"); - return -1; - } - // TODO: split content in multiple segments - const contentLength = content.length; - // TODO: use correct ACK numbers - const segment = new TcpSegment( - this.srcPort, - this.dstPort, - 0, - 0, - new Flags().withAck(), - content, - ); - sendIpPacket(this.srcHost, this.dstHost, segment); - return contentLength; + return this.tcpState.write(content); } closeWrite() { - this.writeClosed = true; - const segment = new TcpSegment( - this.srcPort, - this.dstPort, - 0, - 0, - new Flags().withFin().withAck(), - new Uint8Array(), - ); - sendIpPacket(this.srcHost, this.dstHost, segment); + this.tcpState.closeWrite(); } } @@ -268,7 +187,22 @@ export class TcpListener { async next(): Promise { const { segment, srcIp } = await this.tcpQueue.pop(); - // TODO: validate segment + const dst = this.host.viewgraph.getDeviceByIP(srcIp); + if (!dst || !(dst instanceof ViewHost)) { + console.warn("sender device not found or not a host"); + // Wait for next packet + return this.next(); + } + const ipAndPort = { ip: srcIp, port: segment.sourcePort }; + const queue = this.tcpModule.initNewQueue(this.port, ipAndPort); + + const tcpState = new TcpState( + this.host, + this.port, + dst, + segment.sourcePort, + queue, + ); // Send SYN-ACK const seqNum = getInitialSeqNumber(); @@ -280,18 +214,9 @@ export class TcpListener { new Flags().withSyn().withAck(), new Uint8Array(), ); - const dst = this.host.viewgraph.getDeviceByIP(srcIp); - if (!dst || !(dst instanceof ViewHost)) { - console.warn("sender device not found or not a host"); - // Wait for next packet - return this.next(); - } sendIpPacket(this.host, dst, ackSegment); - const ipAndPort = { ip: srcIp, port: segment.sourcePort }; - const queue = this.tcpModule.initNewQueue(this.port, ipAndPort); - - return new TcpSocket(this.host, this.port, dst, segment.sourcePort, queue); + return new TcpSocket(tcpState); } } From c19e42a18f397b0137b7d04c76e5c05c03109528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:24:23 -0300 Subject: [PATCH 04/40] refactor: rewrite the handleSegment function --- src/packets/tcp.ts | 7 +- src/types/network-modules/tcp/tcpState.ts | 230 ++++++++++++++++++---- 2 files changed, 198 insertions(+), 39 deletions(-) diff --git a/src/packets/tcp.ts b/src/packets/tcp.ts index c970670..28c682b 100644 --- a/src/packets/tcp.ts +++ b/src/packets/tcp.ts @@ -9,6 +9,9 @@ import { Ports } from "./ip"; export const TCP_FLAGS_KEY = "tcp_flags"; +// 2 bytes number +export type Port = number; + export class Flags { // Urgent Pointer field significant public urg = false; @@ -92,10 +95,10 @@ export class TcpSegment implements IpPayload { // // 2 bytes // The source port number. - sourcePort: number; + sourcePort: Port; // 2 bytes // The destination port number. - destinationPort: number; + destinationPort: Port; // 4 bytes // The sequence number of the first data octet in this segment (except // when SYN is present). If SYN is present the sequence number is the diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index f68eac3..e20366e 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -1,14 +1,25 @@ import { EthernetFrame } from "../../../packets/ethernet"; import { IpPayload, IPv4Packet } from "../../../packets/ip"; -import { Flags, TcpSegment } from "../../../packets/tcp"; +import { Flags, Port, TcpSegment } from "../../../packets/tcp"; import { sendViewPacket } from "../../packet"; import { ViewHost } from "../../view-devices"; import { ViewNetworkDevice } from "../../view-devices/vNetworkDevice"; import { AsyncQueue } from "../asyncQueue"; import { SegmentWithIp } from "../tcpModule"; -// TODO: import -type Port = number; +enum TcpStateEnum { + // CLOSED = 0, + // LISTEN = 1, + SYN_SENT = 2, + SYN_RECEIVED = 3, + ESTABLISHED = 4, + FIN_WAIT_1 = 5, + FIN_WAIT_2 = 6, + CLOSE_WAIT = 7, + CLOSING = 8, + LAST_ACK = 9, + TIME_WAIT = 10, +} const MAX_BUFFER_SIZE = 0xffff; @@ -55,13 +66,16 @@ export class TcpState { private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); private writeClosedSeqnum = -1; + private state: TcpStateEnum; + + // TCP state variables // See https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.1 // SND.UNA private sendUnacknowledged: number; // SND.NXT private sendNext: number; // SND.WND - private sendWindow: number; + private sendWindow = MAX_BUFFER_SIZE; // SND.UP // private sendUrgentPointer: number; // SND.WL1 @@ -69,7 +83,7 @@ export class TcpState { // SND.WL2 private ackNumForLastWindowUpdate: number; // ISS - private initialSendSeqNum; + private initialSendSeqNum: number; // RCV.NXT private recvNext: number; @@ -77,7 +91,7 @@ export class TcpState { private recvWindow = MAX_BUFFER_SIZE; // IRS - // private initialRecvSeqNum: number; + private initialRecvSeqNum: number; constructor( srcHost: ViewHost, @@ -90,9 +104,42 @@ export class TcpState { this.srcPort = srcPort; this.dstHost = dstHost; this.dstPort = dstPort; + this.tcpQueue = tcpQueue; + // Handle incoming segments in background + (async () => { + while (true) { + const { segment } = await this.tcpQueue.pop(); + this.handleSegment(segment); + } + })(); + } + + // Open active connection + connect() { + // Initialize the TCB + this.initialSendSeqNum = getInitialSeqNumber(); + this.sendNext = this.initialSendSeqNum + 1; + this.sendUnacknowledged = this.initialSendSeqNum; + + // Send a SYN + const flags = new Flags().withSyn(); + const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); + sendIpPacket(this.srcHost, this.dstHost, segment); + + // Move to SYN_SENT state + this.state = TcpStateEnum.SYN_SENT; + } + + // Accept passive connection + accept(synSegment: TcpSegment) { + // Initialize the TCB this.initialSendSeqNum = getInitialSeqNumber(); + this.sendNext = this.initialSendSeqNum + 1; + this.sendUnacknowledged = this.initialSendSeqNum; + + this.handleSegment(synSegment); } startConnection() { @@ -123,59 +170,160 @@ export class TcpState { } } - recvSegment(receivedSegment: TcpSegment) { + private handleSegment(segment: TcpSegment) { // Sanity check: ports match with expected if ( - receivedSegment.destinationPort !== this.dstPort || - receivedSegment.sourcePort !== this.srcPort + segment.destinationPort !== this.dstPort || + segment.sourcePort !== this.srcPort ) { throw new Error("segment not for this socket"); } + const { flags } = segment; + if (this.state === TcpStateEnum.SYN_SENT) { + // First, check the ACK bit + if (flags.ack) { + const ack = segment.acknowledgementNumber; + if (ack <= this.initialSendSeqNum || ack > this.sendNext) { + if (flags.rst) { + return false; + } + this.newSegment(ack, 0).withFlags(new Flags().withRst()); + return false; + } + // Try to process ACK + if (!this.isAckValid(segment.acknowledgementNumber)) { + return false; + } + } + if (flags.rst) { + // TODO: handle gracefully + if (flags.ack) { + // drop the segment, enter CLOSED state, delete TCB, and return + throw new Error("error: connection reset"); + } else { + return false; + } + } + if (flags.syn) { + this.recvNext = segment.sequenceNumber + 1; + this.initialRecvSeqNum = segment.sequenceNumber; + if (flags.ack) { + this.sendUnacknowledged = segment.acknowledgementNumber; + } + if (flags.ack) { + // It's a valid SYN-ACK + // Process the segment normally + this.state = TcpStateEnum.ESTABLISHED; + return this.handleSegmentData(segment); + } else { + // It's a SYN + if (segment.data.length > 0) { + throw new Error("SYN segment with data not supported"); + } + this.sendWindow = segment.window; + this.seqNumForLastWindowUpdate = segment.sequenceNumber; + this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; + // Send SYN-ACK + this.newSegment(this.initialSendSeqNum, this.recvNext).withFlags( + new Flags().withSyn().withAck(), + ); + this.state = TcpStateEnum.SYN_RECEIVED; + } + } + return flags.rst || flags.syn; + } // Check the sequence number is valid - const segSeq = receivedSegment.sequenceNumber; - const segLen = receivedSegment.data.length; + const segSeq = segment.sequenceNumber; + const segLen = segment.data.length; if (!this.isSeqNumValid(segSeq, segLen)) { return false; } - // TODO: check RST flag + // TODO: handle RST or SYN flags + if (flags.rst || flags.syn) { + // TODO: handle this gracefully + throw new Error("error: RST bit set"); + } - // For now this only accepts ACK segments. - // 3WHS are handled outside of this function - if (!receivedSegment.flags.ack) { + // If the ACK bit is off, drop the segment. + if (!flags.ack) { return false; } - if (!this.processAck(receivedSegment)) { - return false; + if (this.state === TcpStateEnum.SYN_RECEIVED) { + if (!this.isAckValid(segment.acknowledgementNumber)) { + // TODO: send a RST + this.newSegment(segment.acknowledgementNumber, 0).withFlags( + new Flags().withRst(), + ); + return false; + } + this.state = TcpStateEnum.ESTABLISHED; + this.sendWindow = segment.window; + this.seqNumForLastWindowUpdate = segment.sequenceNumber; + this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; + } else if ( + this.state === TcpStateEnum.ESTABLISHED || + this.state === TcpStateEnum.FIN_WAIT_1 || + this.state === TcpStateEnum.FIN_WAIT_2 || + this.state === TcpStateEnum.CLOSE_WAIT || + this.state === TcpStateEnum.CLOSING + ) { + if (segment.acknowledgementNumber <= this.sendUnacknowledged) { + // Ignore the ACK + } else if (segment.acknowledgementNumber > this.sendNext) { + // TODO: send an ACK + return false; + } else { + this.sendUnacknowledged = segment.acknowledgementNumber; + } + if (!this.processAck(segment)) { + return false; + } + + if (this.state === TcpStateEnum.FIN_WAIT_1) { + if (this.sendUnacknowledged === this.writeClosedSeqnum) { + this.state = TcpStateEnum.FIN_WAIT_2; + } + } } // Process the segment data + if (!this.handleSegmentData(segment)) { + return false; + } + + if (flags.fin) { + this.recvNext++; + this.readClosed = true; + // TODO: send an ACK + } + + return true; + } + + private handleSegmentData(segment: TcpSegment) { // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ - if (receivedSegment.sequenceNumber === this.recvNext) { - let receivedData = receivedSegment.data; - // NOTE: for simplicity, we ignore cases where the data is only partially - // inside the window - if (receivedData.length > this.recvWindow) { - throw new Error("buffer overflow"); - } - this.readBuffer.write(receivedData); - this.recvNext = receivedSegment.sequenceNumber + receivedData.length; - this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); - // We should send back an ACK segment - this.notifySendPackets(); + if (segment.sequenceNumber !== this.recvNext) { + return false; } + let receivedData = segment.data; + // NOTE: for simplicity, we ignore cases where the data is only partially + // inside the window + if (receivedData.length > this.recvWindow) { + throw new Error("buffer overflow"); + } + this.readBuffer.write(receivedData); + this.recvNext = segment.sequenceNumber + receivedData.length; + this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); + // We should send back an ACK segment + this.notifySendPackets(); // If FIN, mark read end as closed - if (receivedSegment.flags.fin) { + if (segment.flags.fin) { // The flag counts as a byte this.recvNext++; this.readClosed = true; } - - // start sending packets - if (!this.sendingPackets) { - this.sendingPackets = this.mainLoop(); - } return true; } @@ -236,6 +384,10 @@ export class TcpState { return this.recvNext <= n && n < this.recvNext + this.recvWindow; } + private isAckValid(ackNum: number) { + return this.sendUnacknowledged < ackNum && ackNum <= this.sendNext; + } + private processAck(segment: TcpSegment) { // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 // If the ACK is for a packet not yet sent, drop it @@ -243,7 +395,10 @@ export class TcpState { return false; } // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK - if (this.sendUnacknowledged <= segment.acknowledgementNumber) { + if ( + this.sendUnacknowledged === undefined || + this.sendUnacknowledged <= segment.acknowledgementNumber + ) { this.sendUnacknowledged = segment.acknowledgementNumber; if (this.isSegmentNewer(segment)) { // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. @@ -261,6 +416,7 @@ export class TcpState { // // SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ and SND.WL2 =< SEG.ACK) return ( + this.seqNumForLastWindowUpdate === undefined || this.seqNumForLastWindowUpdate < segment.sequenceNumber || (this.seqNumForLastWindowUpdate === segment.sequenceNumber && this.ackNumForLastWindowUpdate <= segment.acknowledgementNumber) @@ -312,7 +468,7 @@ export class TcpState { if (result !== undefined) { receivedSegmentPromise = this.tcpQueue.pop(); - if (!this.recvSegment(result.segment)) { + if (!this.handleSegment(result.segment)) { continue; } // If we got a valid segment, we can refresh the timer From f98229011616071737c79800285ee3effe636851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:19:19 -0300 Subject: [PATCH 05/40] fix --- src/programs/http_client.ts | 2 +- src/types/network-modules/tcp/tcpState.ts | 172 ++++++++++++++-------- src/types/network-modules/tcpModule.ts | 78 ++++------ src/types/packet.ts | 5 +- 4 files changed, 143 insertions(+), 114 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index 9e56820..2f051fb 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -139,7 +139,7 @@ export class HttpServer extends ProgramBase { async serveClient(socket: TcpSocket) { const buffer = new Uint8Array(1024).fill(0); - const readLength = await socket.read(buffer); + const readLength = await socket.readAll(buffer); // eslint-disable-next-line @typescript-eslint/no-unused-vars const readContents = buffer.slice(0, readLength); diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index e20366e..92ed6c7 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -56,10 +56,12 @@ export class TcpState { private dstPort: Port; private tcpQueue: AsyncQueue; private sendQueue = new AsyncQueue(); - private sendingPackets: Promise | null = null; + private connectionQueue = new AsyncQueue(); + private retransmissionQueue = new RetransmissionQueue(); // Buffer of data received private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private readChannel = new AsyncQueue(); private readClosed = false; // Buffer of data to be sent @@ -107,17 +109,11 @@ export class TcpState { this.tcpQueue = tcpQueue; - // Handle incoming segments in background - (async () => { - while (true) { - const { segment } = await this.tcpQueue.pop(); - this.handleSegment(segment); - } - })(); + this.mainLoop(); } // Open active connection - connect() { + async connect() { // Initialize the TCB this.initialSendSeqNum = getInitialSeqNumber(); this.sendNext = this.initialSendSeqNum + 1; @@ -130,16 +126,28 @@ export class TcpState { // Move to SYN_SENT state this.state = TcpStateEnum.SYN_SENT; + await this.connectionQueue.pop(); } // Accept passive connection accept(synSegment: TcpSegment) { + if (!synSegment.flags.syn) { + return false; + } // Initialize the TCB this.initialSendSeqNum = getInitialSeqNumber(); this.sendNext = this.initialSendSeqNum + 1; this.sendUnacknowledged = this.initialSendSeqNum; - this.handleSegment(synSegment); + this.state = TcpStateEnum.SYN_RECEIVED; + this.recvNext = synSegment.sequenceNumber + 1; + this.initialRecvSeqNum = synSegment.sequenceNumber; + + // Send a SYN-ACK + const flags = new Flags().withSyn().withAck(); + const segment = this.newSegment(this.initialSendSeqNum, this.recvNext); + sendIpPacket(this.srcHost, this.dstHost, segment.withFlags(flags)); + return true; } startConnection() { @@ -163,18 +171,13 @@ export class TcpState { const ackSegment = this.newSegment(this.sendNext, this.recvNext); ackSegment.withFlags(new Flags().withAck()); sendIpPacket(this.srcHost, this.dstHost, ackSegment); - - // start sending packets - if (!this.sendingPackets) { - this.sendingPackets = this.mainLoop(); - } } private handleSegment(segment: TcpSegment) { // Sanity check: ports match with expected if ( - segment.destinationPort !== this.dstPort || - segment.sourcePort !== this.srcPort + segment.sourcePort !== this.dstPort || + segment.destinationPort !== this.srcPort ) { throw new Error("segment not for this socket"); } @@ -214,6 +217,7 @@ export class TcpState { // It's a valid SYN-ACK // Process the segment normally this.state = TcpStateEnum.ESTABLISHED; + this.connectionQueue.push(undefined); return this.handleSegmentData(segment); } else { // It's a SYN @@ -251,13 +255,13 @@ export class TcpState { } if (this.state === TcpStateEnum.SYN_RECEIVED) { if (!this.isAckValid(segment.acknowledgementNumber)) { - // TODO: send a RST this.newSegment(segment.acknowledgementNumber, 0).withFlags( new Flags().withRst(), ); return false; } this.state = TcpStateEnum.ESTABLISHED; + this.connectionQueue.push(undefined); this.sendWindow = segment.window; this.seqNumForLastWindowUpdate = segment.sequenceNumber; this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; @@ -271,7 +275,9 @@ export class TcpState { if (segment.acknowledgementNumber <= this.sendUnacknowledged) { // Ignore the ACK } else if (segment.acknowledgementNumber > this.sendNext) { - // TODO: send an ACK + this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withAck(), + ); return false; } else { this.sendUnacknowledged = segment.acknowledgementNumber; @@ -295,7 +301,10 @@ export class TcpState { if (flags.fin) { this.recvNext++; this.readClosed = true; - // TODO: send an ACK + this.readChannel.push(0); + this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withAck(), + ); } return true; @@ -313,6 +322,7 @@ export class TcpState { throw new Error("buffer overflow"); } this.readBuffer.write(receivedData); + this.readChannel.push(receivedData.length); this.recvNext = segment.sequenceNumber + receivedData.length; this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); // We should send back an ACK segment @@ -323,11 +333,17 @@ export class TcpState { // The flag counts as a byte this.recvNext++; this.readClosed = true; + this.readChannel.push(0); } return true; } - read(output: Uint8Array): number { + async read(output: Uint8Array): Promise { + // Wait for there to be data in the read buffer + while (this.readBuffer.isEmpty() && !this.readClosed) { + await this.readChannel.pop(); + } + // Consume the data and return it const readLength = this.readBuffer.read(output); if (readLength === 0 && this.readClosed) { return -1; @@ -348,12 +364,9 @@ export class TcpState { } closeWrite() { - this.writeClosedSeqnum = this.sendNext; - const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( - new Flags().withFin().withAck(), - ); - this.sendNext++; - sendIpPacket(this.srcHost, this.dstHost, segment); + this.writeClosedSeqnum = + this.sendUnacknowledged + this.writeBuffer.bytesAvailable(); + this.notifySendPackets(); } // utils @@ -438,43 +451,27 @@ export class TcpState { let recheckPromise = this.sendQueue.pop(); let receivedSegmentPromise = this.tcpQueue.pop(); - - let retransmitTimeoutId: NodeJS.Timeout | null = null; - - const clearTimer = () => { - if (retransmitTimeoutId === null) { - return; - } - clearTimeout(retransmitTimeoutId); - retransmitTimeoutId = null; - }; - - const setTimer = () => { - if (retransmitTimeoutId === null) { - return; - } - retransmitTimeoutId = setTimeout(() => { - retransmitTimeoutId = null; - this.sendNext = this.sendUnacknowledged; - this.notifySendPackets(); - }, RETRANSMIT_TIMEOUT); - }; + let retransmitPromise = this.retransmissionQueue.pop(); while (true) { const result = await Promise.race([ recheckPromise, receivedSegmentPromise, + retransmitPromise, ]); - if (result !== undefined) { + if (result === undefined) { + recheckPromise = this.sendQueue.pop(); + } else if ("segment" in result) { receivedSegmentPromise = this.tcpQueue.pop(); - if (!this.handleSegment(result.segment)) { - continue; + if (this.handleSegment(result.segment)) { + this.retransmissionQueue.ack(this.recvNext); } - // If we got a valid segment, we can refresh the timer - clearTimer(); - } else { - recheckPromise = this.sendQueue.pop(); + continue; + } else if ("seqNum" in result) { + retransmitPromise = this.retransmissionQueue.pop(); + this.sendPacket(result.seqNum, result.size); + continue; } do { @@ -485,7 +482,8 @@ export class TcpState { const sendSize = Math.min(this.sendWindowSize(), MAX_SEGMENT_SIZE); if (sendSize > 0) { const data = new Uint8Array(sendSize); - const writeLength = this.writeBuffer.peek(this.sendNext, data); + const offset = this.sendNext - this.sendUnacknowledged; + const writeLength = this.writeBuffer.peek(offset, data); if (writeLength > 0) { segment.withData(data.subarray(0, writeLength)); @@ -499,22 +497,78 @@ export class TcpState { segment.flags.withFin(); } sendIpPacket(this.srcHost, this.dstHost, segment); + this.retransmissionQueue.push( + segment.sequenceNumber, + segment.data.length, + ); // Repeat until we have no more data to send } while (this.writeBuffer.bytesAvailable() > this.sendWindowSize()); } } + private sendPacket(seqNum: number, size: number) { + const segment = this.newSegment(seqNum, this.recvNext).withFlags( + new Flags().withAck(), + ); + + const data = new Uint8Array(size); + const offset = seqNum - this.sendUnacknowledged; + const writeLength = this.writeBuffer.peek(offset, data); + + if (writeLength > 0) { + segment.withData(data.subarray(0, writeLength)); + } + segment.window = this.recvWindow; + + if (seqNum + size === this.writeClosedSeqnum) { + segment.flags.withFin(); + } + this.retransmissionQueue.push(segment.sequenceNumber, segment.data.length); + sendIpPacket(this.srcHost, this.dstHost, segment); + } + private sendWindowSize() { // TODO: add congestion control return this.sendUnacknowledged + this.sendWindow - this.sendNext; } } -function sleep(ms?: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); +const RETRANSMIT_TIMEOUT = 60 * 1000; + +interface RetransmissionQueueItem { + seqNum: number; + size: number; } -const RETRANSMIT_TIMEOUT = 60 * 1000; +class RetransmissionQueue { + private timeoutQueue: [RetransmissionQueueItem, NodeJS.Timeout][] = []; + private itemQueue: AsyncQueue = new AsyncQueue(); + + push(seqNum: number, size: number) { + const item = { + seqNum, + size, + }; + const timeoutId = setTimeout(() => { + this.itemQueue.push(item); + }, RETRANSMIT_TIMEOUT); + this.timeoutQueue.push([item, timeoutId]); + } + + async pop() { + return await this.itemQueue.pop(); + } + + ack(ackNum: number) { + this.timeoutQueue = this.timeoutQueue.filter(([item, timeoutId]) => { + if (item.seqNum < ackNum || item.seqNum + item.size <= ackNum) { + clearTimeout(timeoutId); + return false; + } + return true; + }); + } +} class BytesBuffer { private buffer: Uint8Array; diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 29cbb4c..7a07602 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -70,19 +70,9 @@ export class TcpModule { dstPort, tcpQueue, ); + await tcpState.connect(); - const skt = new TcpSocket(tcpState); - - // Retry on failure - for (let i = 0; i < 3; i++) { - tcpState.startConnection(); - const response = await tcpQueue.pop(); - const ok = tcpState.recvSynAck(response.segment); - if (ok) { - return skt; - } - } - return null; + return new TcpSocket(tcpState); } async listenOn(port: Port): Promise { @@ -148,8 +138,8 @@ export class TcpSocket { /** * Reads data from the connection into the given buffer. - * This reads enough data to fill the buffer, but may read - * less in case the connection is closed. + * This waits until there's a non-zero amount available, + * or the connection is closed. * @param buffer to copy the contents to * @returns the number of bytes read */ @@ -157,6 +147,25 @@ export class TcpSocket { return this.tcpState.read(buffer); } + /** + * Reads data from the connection into the given buffer. + * This reads enough data to fill the buffer, but may read + * less in case the connection is closed. + * @param buffer to copy the contents to + * @returns the number of bytes read + */ + async readAll(buffer: Uint8Array) { + let bytesRead = 0; + while (bytesRead < buffer.length) { + const read = await this.tcpState.read(buffer.subarray(bytesRead)); + if (read === 0) { + break; + } + bytesRead += read; + } + return bytesRead; + } + async write(content: Uint8Array) { return this.tcpState.write(content); } @@ -203,44 +212,11 @@ export class TcpListener { segment.sourcePort, queue, ); - - // Send SYN-ACK - const seqNum = getInitialSeqNumber(); - const ackSegment = new TcpSegment( - this.port, - segment.sourcePort, - seqNum, - segment.sequenceNumber, - new Flags().withSyn().withAck(), - new Uint8Array(), - ); - sendIpPacket(this.host, dst, ackSegment); + // TODO + if (!tcpState.accept(segment)) { + return this.next(); + } return new TcpSocket(tcpState); } } - -function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { - const viewgraph = src.viewgraph; - - // TODO: use MAC and IP of the interfaces used - let nextHopMac = dst.mac; - const path = viewgraph.getPathBetween(src.id, dst.id); - if (!path) return; - for (const id of path.slice(1)) { - const device = viewgraph.getDevice(id); - // if there’s a router in the middle, first send frame to router mac - if (device instanceof ViewNetworkDevice) { - nextHopMac = device.mac; - break; - } - } - const ipPacket = new IPv4Packet(src.ip, dst.ip, payload); - const frame = new EthernetFrame(src.mac, nextHopMac, ipPacket); - - sendViewPacket(src.viewgraph, src.id, frame); -} - -function getInitialSeqNumber() { - return Math.floor(Math.random() * 0xffffffff); -} diff --git a/src/types/packet.ts b/src/types/packet.ts index ba708e0..86619b5 100644 --- a/src/types/packet.ts +++ b/src/types/packet.ts @@ -10,7 +10,6 @@ import { RightBar } from "../graphics/right_bar"; import { Position } from "./common"; import { ViewGraph } from "./graphs/viewgraph"; import { Layer, layerIncluded } from "./layer"; -//import { EchoMessage } from "../packets/icmp"; import { DataGraph, DeviceId } from "./graphs/datagraph"; import { EthernetFrame, IP_PROTOCOL_TYPE } from "../packets/ethernet"; import { @@ -101,8 +100,8 @@ export class Packet extends Graphics { this.cursor = "pointer"; this.on("click", this.onClick, this); this.on("tap", this.onClick, this); - // register in Packet Manger - ctx.getViewGraph().getPacketManager().registerPacket(this); + // register in Packet Manager + viewgraph.getPacketManager().registerPacket(this); } setProgress(progress: number) { From 9ec0f585473201a3aadc61645d46a646b2ec5f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:24:06 -0300 Subject: [PATCH 06/40] fix: use correct value for read's return --- src/types/network-modules/tcpModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 7a07602..4f92525 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -158,7 +158,7 @@ export class TcpSocket { let bytesRead = 0; while (bytesRead < buffer.length) { const read = await this.tcpState.read(buffer.subarray(bytesRead)); - if (read === 0) { + if (read === -1) { break; } bytesRead += read; From b64d4c38f0bf098c99caac95ad7a0c9d5b3ec3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:26:18 -0300 Subject: [PATCH 07/40] chore: fix lint errors --- src/packets/tcp.ts | 4 ++-- src/types/network-modules/tcp/tcpState.ts | 4 ++-- src/types/network-modules/tcpModule.ts | 9 ++------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/packets/tcp.ts b/src/packets/tcp.ts index 28c682b..64e8817 100644 --- a/src/packets/tcp.ts +++ b/src/packets/tcp.ts @@ -150,8 +150,8 @@ export class TcpSegment implements IpPayload { constructor( srcPort: number, dstPort: number, - seqNum: number = 0, - ackNum: number = 0, + seqNum = 0, + ackNum = 0, flags: Flags = new Flags(), data: Uint8Array = new Uint8Array(0), ) { diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 92ed6c7..30c8166 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -315,7 +315,7 @@ export class TcpState { if (segment.sequenceNumber !== this.recvNext) { return false; } - let receivedData = segment.data; + const receivedData = segment.data; // NOTE: for simplicity, we ignore cases where the data is only partially // inside the window if (receivedData.length > this.recvWindow) { @@ -542,7 +542,7 @@ interface RetransmissionQueueItem { class RetransmissionQueue { private timeoutQueue: [RetransmissionQueueItem, NodeJS.Timeout][] = []; - private itemQueue: AsyncQueue = new AsyncQueue(); + private itemQueue = new AsyncQueue(); push(seqNum: number, size: number) { const item = { diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 4f92525..921700c 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -1,9 +1,6 @@ -import { EthernetFrame } from "../../packets/ethernet"; -import { IpAddress, IpPayload, IPv4Packet } from "../../packets/ip"; -import { Flags, TcpSegment } from "../../packets/tcp"; -import { sendViewPacket } from "../packet"; +import { IpAddress } from "../../packets/ip"; +import { TcpSegment } from "../../packets/tcp"; import { ViewHost } from "../view-devices"; -import { ViewNetworkDevice } from "../view-devices/vNetworkDevice"; import { AsyncQueue } from "./asyncQueue"; import { TcpState } from "./tcp/tcpState"; @@ -127,8 +124,6 @@ export class TcpModule { } } -const MAX_BUFFER_SIZE = 0xffff; - export class TcpSocket { private tcpState: TcpState; From 9551c54fa46fc3720cf50ee5f072cfbf13bcdc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:29:05 -0300 Subject: [PATCH 08/40] chore: use readAll --- src/programs/http_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index 2f051fb..5800213 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -66,7 +66,7 @@ export class HttpClient extends ProgramBase { // Read response const buffer = new Uint8Array(1024); - const readLength = await socket.read(buffer); + const readLength = await socket.readAll(buffer); if (readLength < 0) { console.error("HttpClient failed to read from socket"); return; From 4824ffdad7478931398d3b1500f7207604a5cb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:24:29 -0300 Subject: [PATCH 09/40] fix: cleanup port handler when stopping server --- src/programs/http_client.ts | 16 ++++++++++++---- src/types/network-modules/tcpModule.ts | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index 5800213..f358265 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -3,7 +3,8 @@ import { ProgramInfo } from "../graphics/renderables/device_info"; import { DeviceId } from "../types/graphs/datagraph"; import { ViewGraph } from "../types/graphs/viewgraph"; import { Layer } from "../types/layer"; -import { TcpSocket } from "../types/network-modules/tcpModule"; +import { AsyncQueue } from "../types/network-modules/asyncQueue"; +import { TcpListener, TcpSocket } from "../types/network-modules/tcpModule"; import { ViewHost } from "../types/view-devices"; import { ProgramBase } from "./program_base"; @@ -85,6 +86,8 @@ export class HttpServer extends ProgramBase { private port: number; + private stopChannel = new AsyncQueue<"stop">(); + protected _parseInputs(inputs: string[]): void { if (inputs.length !== 0) { console.error( @@ -105,8 +108,7 @@ export class HttpServer extends ProgramBase { } protected _stop() { - // Nothing to do - // TODO: stop serving requests + this.stopChannel.push("stop"); } private async serveHttpRequests() { @@ -124,11 +126,17 @@ export class HttpServer extends ProgramBase { return; } + let stopPromise = this.stopChannel.pop(); + while (true) { - const socket = await listener.next(); + const socket = await Promise.race([stopPromise, listener.next()]); + if (socket === "stop") { + break; + } this.serveClient(socket); } + listener.close(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 921700c..8baf0cc 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -102,6 +102,18 @@ export class TcpModule { return queue; } + closeQueue(port: Port, filter?: IpAndPort) { + let handlerMap = this.tcpQueues.get(port); + if (!handlerMap) { + return; + } + const key = filter ? [filter.ip, filter.port].toString() : MATCH_ALL_KEY; + handlerMap.delete(key); + if (handlerMap.size === 0) { + this.tcpQueues.delete(port); + } + } + // Port number to use for the next connection. // The number is arbitrary private nextPortNumber: Port = STARTING_PORT; @@ -214,4 +226,8 @@ export class TcpListener { return new TcpSocket(tcpState); } + + close() { + this.tcpModule.closeQueue(this.port); + } } From b85c6120c852129888768b0c60b7b4cac40b8d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:26:23 -0300 Subject: [PATCH 10/40] fix: cleanup before retrying when listening --- src/types/network-modules/tcpModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 8baf0cc..fceb0be 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -219,8 +219,8 @@ export class TcpListener { segment.sourcePort, queue, ); - // TODO if (!tcpState.accept(segment)) { + this.tcpModule.closeQueue(this.port, ipAndPort); return this.next(); } From a9b2889c5c183b6110efea6b9ac790316aff56c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:43:57 -0300 Subject: [PATCH 11/40] fix: make timeout scale with simspeed --- src/types/network-modules/tcp/tcpState.ts | 36 ++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 30c8166..6eb28b0 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -1,3 +1,4 @@ +import { Ticker } from "pixi.js"; import { EthernetFrame } from "../../../packets/ethernet"; import { IpPayload, IPv4Packet } from "../../../packets/ip"; import { Flags, Port, TcpSegment } from "../../../packets/tcp"; @@ -6,6 +7,7 @@ import { ViewHost } from "../../view-devices"; import { ViewNetworkDevice } from "../../view-devices/vNetworkDevice"; import { AsyncQueue } from "../asyncQueue"; import { SegmentWithIp } from "../tcpModule"; +import { GlobalContext } from "../../../context"; enum TcpStateEnum { // CLOSED = 0, @@ -57,7 +59,7 @@ export class TcpState { private tcpQueue: AsyncQueue; private sendQueue = new AsyncQueue(); private connectionQueue = new AsyncQueue(); - private retransmissionQueue = new RetransmissionQueue(); + private retransmissionQueue: RetransmissionQueue; // Buffer of data received private readBuffer = new BytesBuffer(MAX_BUFFER_SIZE); @@ -109,6 +111,8 @@ export class TcpState { this.tcpQueue = tcpQueue; + this.retransmissionQueue = new RetransmissionQueue(this.srcHost.ctx); + this.mainLoop(); } @@ -443,7 +447,7 @@ export class TcpState { return; } this.notifiedSendPackets = true; - setTimeout(() => this.sendQueue.push(undefined), 50); + setTimeout(() => this.sendQueue.push(undefined), 5); } private async mainLoop() { @@ -533,7 +537,7 @@ export class TcpState { } } -const RETRANSMIT_TIMEOUT = 60 * 1000; +const RETRANSMIT_TIMEOUT = 15 * 1000; interface RetransmissionQueueItem { seqNum: number; @@ -541,18 +545,30 @@ interface RetransmissionQueueItem { } class RetransmissionQueue { - private timeoutQueue: [RetransmissionQueueItem, NodeJS.Timeout][] = []; + private timeoutQueue: [RetransmissionQueueItem, (t: Ticker) => void][] = []; private itemQueue = new AsyncQueue(); + private ctx: GlobalContext; + + constructor(ctx: GlobalContext) { + this.ctx = ctx; + } + push(seqNum: number, size: number) { const item = { seqNum, size, }; - const timeoutId = setTimeout(() => { - this.itemQueue.push(item); - }, RETRANSMIT_TIMEOUT); - this.timeoutQueue.push([item, timeoutId]); + let progress = 0; + const tick = (ticker: Ticker) => { + progress += ticker.elapsedMS * this.ctx.getCurrentSpeed(); + if (progress >= RETRANSMIT_TIMEOUT) { + this.itemQueue.push(item); + Ticker.shared.remove(tick, this); + } + }; + Ticker.shared.add(tick, this); + this.timeoutQueue.push([item, tick]); } async pop() { @@ -560,9 +576,9 @@ class RetransmissionQueue { } ack(ackNum: number) { - this.timeoutQueue = this.timeoutQueue.filter(([item, timeoutId]) => { + this.timeoutQueue = this.timeoutQueue.filter(([item, tick]) => { if (item.seqNum < ackNum || item.seqNum + item.size <= ackNum) { - clearTimeout(timeoutId); + Ticker.shared.remove(tick, this); return false; } return true; From b46c7d8902c2f999c5da7c7f209866a0a7466ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:55:06 -0300 Subject: [PATCH 12/40] feat: add packet drop animation --- src/types/network-modules/tcp/tcpState.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 6eb28b0..9de9c2d 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -2,7 +2,7 @@ import { Ticker } from "pixi.js"; import { EthernetFrame } from "../../../packets/ethernet"; import { IpPayload, IPv4Packet } from "../../../packets/ip"; import { Flags, Port, TcpSegment } from "../../../packets/tcp"; -import { sendViewPacket } from "../../packet"; +import { dropPacket, sendViewPacket } from "../../packet"; import { ViewHost } from "../../view-devices"; import { ViewNetworkDevice } from "../../view-devices/vNetworkDevice"; import { AsyncQueue } from "../asyncQueue"; @@ -314,6 +314,12 @@ export class TcpState { return true; } + private dropSegment(segment: TcpSegment) { + const packet = new IPv4Packet(this.srcHost.ip, this.dstHost.ip, segment); + const frame = new EthernetFrame(this.srcHost.mac, this.dstHost.mac, packet); + dropPacket(this.srcHost.viewgraph, this.srcHost.id, frame); + } + private handleSegmentData(segment: TcpSegment) { // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ if (segment.sequenceNumber !== this.recvNext) { @@ -470,6 +476,8 @@ export class TcpState { receivedSegmentPromise = this.tcpQueue.pop(); if (this.handleSegment(result.segment)) { this.retransmissionQueue.ack(this.recvNext); + } else { + this.dropSegment(result.segment); } continue; } else if ("seqNum" in result) { From aa230920997502c41330a5affde60d86bfb7f903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:49:37 -0300 Subject: [PATCH 13/40] fix: take into account overflows --- src/types/network-modules/tcp/tcpState.ts | 65 +++++++++++++++-------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 9de9c2d..c6c9008 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -50,7 +50,8 @@ function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { sendViewPacket(src.viewgraph, src.id, frame); } -// TODO: add overflow checks +const u32_MODULUS = 0x100000000; + export class TcpState { private srcHost: ViewHost; private srcPort: Port; @@ -120,7 +121,7 @@ export class TcpState { async connect() { // Initialize the TCB this.initialSendSeqNum = getInitialSeqNumber(); - this.sendNext = this.initialSendSeqNum + 1; + this.sendNext = (this.initialSendSeqNum + 1) % u32_MODULUS; this.sendUnacknowledged = this.initialSendSeqNum; // Send a SYN @@ -140,11 +141,11 @@ export class TcpState { } // Initialize the TCB this.initialSendSeqNum = getInitialSeqNumber(); - this.sendNext = this.initialSendSeqNum + 1; + this.sendNext = (this.initialSendSeqNum + 1) % u32_MODULUS; this.sendUnacknowledged = this.initialSendSeqNum; this.state = TcpStateEnum.SYN_RECEIVED; - this.recvNext = synSegment.sequenceNumber + 1; + this.recvNext = (synSegment.sequenceNumber + 1) % u32_MODULUS; this.initialRecvSeqNum = synSegment.sequenceNumber; // Send a SYN-ACK @@ -164,11 +165,14 @@ export class TcpState { if (!segment.flags.syn || !segment.flags.ack) { return false; } - if (segment.acknowledgementNumber !== this.initialSendSeqNum + 1) { + if ( + segment.acknowledgementNumber !== + (this.initialSendSeqNum + 1) % u32_MODULUS + ) { return false; } - this.recvNext = segment.sequenceNumber + 1; - // this.initialRecvSeqNum = segment.sequenceNumber; + this.recvNext = (segment.sequenceNumber + 1) % u32_MODULUS; + this.initialRecvSeqNum = segment.sequenceNumber; this.sendNext = segment.acknowledgementNumber; this.sendWindow = segment.window; @@ -212,7 +216,7 @@ export class TcpState { } } if (flags.syn) { - this.recvNext = segment.sequenceNumber + 1; + this.recvNext = (segment.sequenceNumber + 1) % u32_MODULUS; this.initialRecvSeqNum = segment.sequenceNumber; if (flags.ack) { this.sendUnacknowledged = segment.acknowledgementNumber; @@ -303,7 +307,7 @@ export class TcpState { } if (flags.fin) { - this.recvNext++; + this.recvNext = (this.recvNext + 1) % u32_MODULUS; this.readClosed = true; this.readChannel.push(0); this.newSegment(this.sendNext, this.recvNext).withFlags( @@ -333,7 +337,8 @@ export class TcpState { } this.readBuffer.write(receivedData); this.readChannel.push(receivedData.length); - this.recvNext = segment.sequenceNumber + receivedData.length; + this.recvNext = + (segment.sequenceNumber + receivedData.length) % u32_MODULUS; this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); // We should send back an ACK segment this.notifySendPackets(); @@ -341,7 +346,7 @@ export class TcpState { // If FIN, mark read end as closed if (segment.flags.fin) { // The flag counts as a byte - this.recvNext++; + this.recvNext = (this.recvNext + 1) % u32_MODULUS; this.readClosed = true; this.readChannel.push(0); } @@ -375,7 +380,8 @@ export class TcpState { closeWrite() { this.writeClosedSeqnum = - this.sendUnacknowledged + this.writeBuffer.bytesAvailable(); + (this.sendUnacknowledged + this.writeBuffer.bytesAvailable()) % + u32_MODULUS; this.notifySendPackets(); } @@ -398,16 +404,23 @@ export class TcpState { } else { return ( this.isInReceiveWindow(segSeq) || - this.isInReceiveWindow(segSeq + segLen - 1) + this.isInReceiveWindow((segSeq + segLen - 1) % u32_MODULUS) ); } } private isInReceiveWindow(n: number) { - return this.recvNext <= n && n < this.recvNext + this.recvWindow; + const recvWindowHigh = (this.recvNext + this.recvWindow) % u32_MODULUS; + if (recvWindowHigh < this.recvNext) { + return this.recvNext <= n || n < recvWindowHigh; + } + return this.recvNext <= n && n < recvWindowHigh; } private isAckValid(ackNum: number) { + if (this.sendNext < this.sendUnacknowledged) { + return this.sendUnacknowledged < ackNum || ackNum <= this.sendNext; + } return this.sendUnacknowledged < ackNum && ackNum <= this.sendNext; } @@ -463,7 +476,7 @@ export class TcpState { let receivedSegmentPromise = this.tcpQueue.pop(); let retransmitPromise = this.retransmissionQueue.pop(); - while (true) { + while (!this.readClosed || this.writeClosedSeqnum === -1) { const result = await Promise.race([ recheckPromise, receivedSegmentPromise, @@ -494,18 +507,20 @@ export class TcpState { const sendSize = Math.min(this.sendWindowSize(), MAX_SEGMENT_SIZE); if (sendSize > 0) { const data = new Uint8Array(sendSize); - const offset = this.sendNext - this.sendUnacknowledged; + const offset = + (u32_MODULUS + this.sendNext - this.sendUnacknowledged) % + u32_MODULUS; const writeLength = this.writeBuffer.peek(offset, data); if (writeLength > 0) { segment.withData(data.subarray(0, writeLength)); - this.sendNext += writeLength; + this.sendNext = (this.sendNext + writeLength) % u32_MODULUS; } } segment.window = this.recvWindow; if (this.sendNext === this.writeClosedSeqnum) { - this.sendNext++; + this.sendNext = (this.sendNext + 1) % u32_MODULUS; segment.flags.withFin(); } sendIpPacket(this.srcHost, this.dstHost, segment); @@ -524,7 +539,8 @@ export class TcpState { ); const data = new Uint8Array(size); - const offset = seqNum - this.sendUnacknowledged; + const offset = + (u32_MODULUS + seqNum - this.sendUnacknowledged) % u32_MODULUS; const writeLength = this.writeBuffer.peek(offset, data); if (writeLength > 0) { @@ -532,7 +548,7 @@ export class TcpState { } segment.window = this.recvWindow; - if (seqNum + size === this.writeClosedSeqnum) { + if ((seqNum + size) % u32_MODULUS === this.writeClosedSeqnum) { segment.flags.withFin(); } this.retransmissionQueue.push(segment.sequenceNumber, segment.data.length); @@ -541,7 +557,9 @@ export class TcpState { private sendWindowSize() { // TODO: add congestion control - return this.sendUnacknowledged + this.sendWindow - this.sendNext; + return ( + (this.sendUnacknowledged + this.sendWindow - this.sendNext) % u32_MODULUS + ); } } @@ -585,7 +603,10 @@ class RetransmissionQueue { ack(ackNum: number) { this.timeoutQueue = this.timeoutQueue.filter(([item, tick]) => { - if (item.seqNum < ackNum || item.seqNum + item.size <= ackNum) { + if ( + item.seqNum < ackNum || + (item.seqNum + item.size) % u32_MODULUS <= ackNum + ) { Ticker.shared.remove(tick, this); return false; } From 84a433cca9abf006913320387ca53fdda234e1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:52:36 -0300 Subject: [PATCH 14/40] fix: compute windows correctly --- src/types/network-modules/tcp/tcpState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index c6c9008..0091bac 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -372,7 +372,7 @@ export class TcpState { throw new Error("write closed"); } const writeLength = this.writeBuffer.write(input); - if (this.sendWindow > 0 && writeLength > 0) { + if (this.sendWindowSize() > 0 && writeLength > 0) { this.notifySendPackets(); } return writeLength; From 682a2d83c80e71244eab269ffaca2a1d7cbb5287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:04:45 -0300 Subject: [PATCH 15/40] fix: reset flag after sending packet --- src/types/network-modules/tcp/tcpState.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 0091bac..3c573c5 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -310,9 +310,7 @@ export class TcpState { this.recvNext = (this.recvNext + 1) % u32_MODULUS; this.readClosed = true; this.readChannel.push(0); - this.newSegment(this.sendNext, this.recvNext).withFlags( - new Flags().withAck(), - ); + this.notifySendPackets(); } return true; @@ -342,14 +340,6 @@ export class TcpState { this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); // We should send back an ACK segment this.notifySendPackets(); - - // If FIN, mark read end as closed - if (segment.flags.fin) { - // The flag counts as a byte - this.recvNext = (this.recvNext + 1) % u32_MODULUS; - this.readClosed = true; - this.readChannel.push(0); - } return true; } @@ -485,6 +475,7 @@ export class TcpState { if (result === undefined) { recheckPromise = this.sendQueue.pop(); + this.notifiedSendPackets = false; } else if ("segment" in result) { receivedSegmentPromise = this.tcpQueue.pop(); if (this.handleSegment(result.segment)) { From b5c73e6131e2d1c15bb6f1d6916e30d55d3c134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:10:43 -0300 Subject: [PATCH 16/40] chore: added logging for dropped packets --- src/types/network-modules/tcp/tcpState.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 3c573c5..0746c4f 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -196,13 +196,16 @@ export class TcpState { const ack = segment.acknowledgementNumber; if (ack <= this.initialSendSeqNum || ack > this.sendNext) { if (flags.rst) { + console.debug("Invalid SYN_SENT ACK with RST"); return false; } + console.debug("Invalid SYN_SENT ACK, sending RST"); this.newSegment(ack, 0).withFlags(new Flags().withRst()); return false; } // Try to process ACK if (!this.isAckValid(segment.acknowledgementNumber)) { + console.debug("Invalid SYN_SENT ACK"); return false; } } @@ -212,6 +215,7 @@ export class TcpState { // drop the segment, enter CLOSED state, delete TCB, and return throw new Error("error: connection reset"); } else { + console.debug("SYN_SENT RST without ACK, dropping segment"); return false; } } @@ -242,12 +246,17 @@ export class TcpState { this.state = TcpStateEnum.SYN_RECEIVED; } } - return flags.rst || flags.syn; + if (!(flags.rst || flags.syn)) { + console.debug("SYN_SENT segment without SYN or RST"); + return false; + } + return true; } // Check the sequence number is valid const segSeq = segment.sequenceNumber; const segLen = segment.data.length; if (!this.isSeqNumValid(segSeq, segLen)) { + console.debug("Sequence number not valid"); return false; } @@ -259,10 +268,12 @@ export class TcpState { // If the ACK bit is off, drop the segment. if (!flags.ack) { + console.debug("ACK bit is off, dropping segment"); return false; } if (this.state === TcpStateEnum.SYN_RECEIVED) { if (!this.isAckValid(segment.acknowledgementNumber)) { + console.debug("ACK invalid, dropping segment"); this.newSegment(segment.acknowledgementNumber, 0).withFlags( new Flags().withRst(), ); @@ -283,6 +294,7 @@ export class TcpState { if (segment.acknowledgementNumber <= this.sendUnacknowledged) { // Ignore the ACK } else if (segment.acknowledgementNumber > this.sendNext) { + console.debug("ACK for future segment, dropping segment"); this.newSegment(this.sendNext, this.recvNext).withFlags( new Flags().withAck(), ); @@ -291,6 +303,7 @@ export class TcpState { this.sendUnacknowledged = segment.acknowledgementNumber; } if (!this.processAck(segment)) { + console.debug("ACK processing failed, dropping segment"); return false; } @@ -303,6 +316,7 @@ export class TcpState { // Process the segment data if (!this.handleSegmentData(segment)) { + console.debug("Segment data processing failed, dropping segment"); return false; } From 1fe984ec1ba902f5516537a709c325d416050fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:06:55 -0300 Subject: [PATCH 17/40] chore: fix lints --- src/programs/http_client.ts | 4 ++-- src/types/network-modules/tcpModule.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index f358265..f831984 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -4,7 +4,7 @@ import { DeviceId } from "../types/graphs/datagraph"; import { ViewGraph } from "../types/graphs/viewgraph"; import { Layer } from "../types/layer"; import { AsyncQueue } from "../types/network-modules/asyncQueue"; -import { TcpListener, TcpSocket } from "../types/network-modules/tcpModule"; +import { TcpSocket } from "../types/network-modules/tcpModule"; import { ViewHost } from "../types/view-devices"; import { ProgramBase } from "./program_base"; @@ -126,7 +126,7 @@ export class HttpServer extends ProgramBase { return; } - let stopPromise = this.stopChannel.pop(); + const stopPromise = this.stopChannel.pop(); while (true) { const socket = await Promise.race([stopPromise, listener.next()]); diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index fceb0be..16f25a5 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -103,7 +103,7 @@ export class TcpModule { } closeQueue(port: Port, filter?: IpAndPort) { - let handlerMap = this.tcpQueues.get(port); + const handlerMap = this.tcpQueues.get(port); if (!handlerMap) { return; } From 0009390edae9ff67a8176b32d5b12a7a4953e08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:46:25 -0300 Subject: [PATCH 18/40] feat: set seqnum = 0 at start --- src/types/network-modules/tcp/tcpState.ts | 4 +++- test/tcp.test.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 0746c4f..316eac4 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -26,7 +26,9 @@ enum TcpStateEnum { const MAX_BUFFER_SIZE = 0xffff; function getInitialSeqNumber() { - return Math.floor(Math.random() * 0xffffffff); + // For random seqnums use: + // return Math.floor(Math.random() * 0xffffffff); + return 0; } function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { diff --git a/test/tcp.test.ts b/test/tcp.test.ts index f0d08b3..375d466 100644 --- a/test/tcp.test.ts +++ b/test/tcp.test.ts @@ -1,4 +1,4 @@ -import { IpAddress, TCP_PROTOCOL_NUMBER } from "../src/packets/ip"; +import { IpAddress } from "../src/packets/ip"; import { Flags, TcpSegment } from "../src/packets/tcp"; describe("TCP module", () => { From 482fb880f008725897703bf88dd50d75ccd3afc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:57:50 -0300 Subject: [PATCH 19/40] fix: add missing return --- src/types/network-modules/tcp/tcpState.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 316eac4..7062d80 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -286,7 +286,8 @@ export class TcpState { this.sendWindow = segment.window; this.seqNumForLastWindowUpdate = segment.sequenceNumber; this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; - } else if ( + } + if ( this.state === TcpStateEnum.ESTABLISHED || this.state === TcpStateEnum.FIN_WAIT_1 || this.state === TcpStateEnum.FIN_WAIT_2 || @@ -430,7 +431,7 @@ export class TcpState { return this.sendUnacknowledged < ackNum && ackNum <= this.sendNext; } - private processAck(segment: TcpSegment) { + private processAck(segment: TcpSegment): boolean { // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 // If the ACK is for a packet not yet sent, drop it if (this.sendNext < segment.acknowledgementNumber) { @@ -449,6 +450,7 @@ export class TcpState { this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; } } + return true; } private isSegmentNewer(segment: TcpSegment): boolean { From 2b05a67f085ef190cf9694863018cbd62309aea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 26 Apr 2025 15:06:17 -0300 Subject: [PATCH 20/40] fix: handle syn increasing sequence number --- src/types/network-modules/tcp/tcpState.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 7062d80..20baf89 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -232,7 +232,11 @@ export class TcpState { // Process the segment normally this.state = TcpStateEnum.ESTABLISHED; this.connectionQueue.push(undefined); - return this.handleSegmentData(segment); + if (!this.handleSegmentData(segment)) { + console.debug("Segment data processing failed"); + return false; + } + return true; } else { // It's a SYN if (segment.data.length > 0) { @@ -341,7 +345,10 @@ export class TcpState { private handleSegmentData(segment: TcpSegment) { // NOTE: for simplicity, we ignore cases where RCV.NXT != SEG.SEQ - if (segment.sequenceNumber !== this.recvNext) { + const seqNum = segment.flags.syn + ? (segment.sequenceNumber + 1) % u32_MODULUS + : segment.sequenceNumber; + if (seqNum !== this.recvNext) { return false; } const receivedData = segment.data; @@ -352,8 +359,7 @@ export class TcpState { } this.readBuffer.write(receivedData); this.readChannel.push(receivedData.length); - this.recvNext = - (segment.sequenceNumber + receivedData.length) % u32_MODULUS; + this.recvNext = (seqNum + receivedData.length) % u32_MODULUS; this.recvWindow = MAX_BUFFER_SIZE - this.readBuffer.bytesAvailable(); // We should send back an ACK segment this.notifySendPackets(); @@ -499,6 +505,7 @@ export class TcpState { if (this.handleSegment(result.segment)) { this.retransmissionQueue.ack(this.recvNext); } else { + console.log("Dropping segment"); this.dropSegment(result.segment); } continue; From 9d3a47c2126b5dd20b603eeddeb8b5c0b42f1da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 19:50:53 -0300 Subject: [PATCH 21/40] chore: remove unneeded TODOs --- src/packets/ethernet.ts | 1 - src/types/undo-redo/moves/addRemoveEdge.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/packets/ethernet.ts b/src/packets/ethernet.ts index 329d8c2..516ae1f 100644 --- a/src/packets/ethernet.ts +++ b/src/packets/ethernet.ts @@ -100,7 +100,6 @@ export class EthernetFrame { type: number; // 46-1500 bytes // If the payload is smaller than 46 bytes, it is padded. - // TODO: make this an interface // The payload payload: FramePayload; // 4 bytes diff --git a/src/types/undo-redo/moves/addRemoveEdge.ts b/src/types/undo-redo/moves/addRemoveEdge.ts index 0f97afd..a98abd7 100644 --- a/src/types/undo-redo/moves/addRemoveEdge.ts +++ b/src/types/undo-redo/moves/addRemoveEdge.ts @@ -84,7 +84,6 @@ export abstract class AddRemoveEdgeMove extends BaseMove { return false; } - // TODO: store routing tables this.state = { removedData: edgeData }; // Deselect to avoid showing the information of the deleted edge // TODO: this isnt needed I think From dd5748164a787e8d196126c5ba613ef2a59e5e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:05:17 -0300 Subject: [PATCH 22/40] feat: allow choosing resource size in client program --- src/programs/http_client.ts | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index f831984..1de32c8 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -8,19 +8,35 @@ import { TcpSocket } from "../types/network-modules/tcpModule"; import { ViewHost } from "../types/view-devices"; import { ProgramBase } from "./program_base"; +const RESOURCE_MAP = { + "/small": generateResource(1024), + "/medium": generateResource(102400), + "/large": generateResource(10485760), +}; + +function generateResource(size: number): Uint8Array { + const resource = new Uint8Array(size); + for (let i = 0; i < size; i++) { + resource[i] = Math.floor(Math.random() * 256); + } + return resource; +} + export class HttpClient extends ProgramBase { static readonly PROGRAM_NAME = "Send HTTP request"; private dstId: DeviceId; + private resource: string; protected _parseInputs(inputs: string[]): void { - if (inputs.length !== 1) { + if (inputs.length !== 2) { console.error( - "HttpClient requires 1 input. " + inputs.length + " were given.", + "HttpClient requires 2 input. " + inputs.length + " were given.", ); return; } this.dstId = parseInt(inputs[0]); + this.resource = inputs[1]; } protected _run() { @@ -50,13 +66,15 @@ export class HttpClient extends ProgramBase { return; } - // Encode dummy HTTP request - const httpRequest = "GET / HTTP/1.1\r\nHost: " + dstDevice.ip + "\r\n\r\n"; - const content = new TextEncoder().encode(httpRequest); + // Encode HTTP request + const httpRequest = getContentRequest( + this.runner.ip.toString(), + this.resource, + ); // Write request const socket = await this.runner.tcpConnect(this.dstId); - const wrote = await socket.write(content); + const wrote = await socket.write(httpRequest); if (wrote < 0) { console.error("HttpClient failed to write to socket"); return; @@ -75,12 +93,26 @@ export class HttpClient extends ProgramBase { } static getProgramInfo(viewgraph: ViewGraph, srcId: DeviceId): ProgramInfo { + const sizeOptions = [ + { value: "/small", text: "1 KB" }, + { value: "/medium", text: "100 KB" }, + { value: "/large", text: "10 MB" }, + ]; + const programInfo = new ProgramInfo(this.PROGRAM_NAME); programInfo.withDestinationDropdown(viewgraph, srcId, Layer.App); + programInfo.withDropdown("Size of requested resource", sizeOptions); return programInfo; } } +function getContentRequest(host: string, resource: string): Uint8Array { + const httpRequest = + "GET " + resource + " HTTP/1.1\r\nHost: " + host + "\r\n\r\n"; + const content = new TextEncoder().encode(httpRequest); + return content; +} + export class HttpServer extends ProgramBase { static readonly PROGRAM_NAME = "Serve HTTP requests"; From 3bb288f8bc475d540c2162bd5d76d68a2ac314aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:52:46 -0300 Subject: [PATCH 23/40] fix: make write wait until buffer has space --- src/programs/http_client.ts | 34 +++++++++++++++----- src/types/network-modules/tcp/tcpState.ts | 39 ++++++++++++++++------- src/types/network-modules/tcpModule.ts | 9 +++++- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index 1de32c8..a322999 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -8,11 +8,11 @@ import { TcpSocket } from "../types/network-modules/tcpModule"; import { ViewHost } from "../types/view-devices"; import { ProgramBase } from "./program_base"; -const RESOURCE_MAP = { - "/small": generateResource(1024), - "/medium": generateResource(102400), - "/large": generateResource(10485760), -}; +const RESOURCE_MAP = new Map([ + ["/small", generateResource(1024)], + ["/medium", generateResource(102400)], + ["/large", generateResource(10485760)], +]); function generateResource(size: number): Uint8Array { const resource = new Uint8Array(size); @@ -188,13 +188,31 @@ export class HttpServer extends ProgramBase { return; } - // TODO: validate request + const requestContents = new TextDecoder().decode(readContents); + const matches = requestContents.match(/GET (.+) HTTP\/1.1/); + if (!matches || matches.length < 2) { + console.error("HttpServer failed to parse request"); + return; + } + const resourceContents = RESOURCE_MAP.get(matches[1]); + if (!resourceContents) { + console.error("HttpServer failed to find requested resource"); + return; + } // Encode dummy HTTP response - const httpResponse = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; + const httpResponse = + "HTTP/1.1 200 OK\r\nContent-Length: " + + resourceContents.length + + "\r\n\r\n"; const content = new TextEncoder().encode(httpResponse); const wrote = await socket.write(content); - if (wrote < 0) { + if (wrote <= 0) { + console.error("HttpServer failed to write to socket"); + return; + } + const wrote2 = await socket.write(resourceContents); + if (wrote2 <= 0) { console.error("HttpServer failed to write to socket"); return; } diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 20baf89..0ca1f48 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -71,6 +71,7 @@ export class TcpState { // Buffer of data to be sent private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); + private writeChannel = new AsyncQueue(); private writeClosedSeqnum = -1; private state: TcpStateEnum; @@ -380,15 +381,24 @@ export class TcpState { return readLength; } - write(input: Uint8Array): number { + async write(input: Uint8Array): Promise { if (this.writeClosedSeqnum >= 0) { throw new Error("write closed"); } - const writeLength = this.writeBuffer.write(input); - if (this.sendWindowSize() > 0 && writeLength > 0) { - this.notifySendPackets(); + let totalWrote = 0; + while (totalWrote < input.length) { + const writeLength = this.writeBuffer.write(input.subarray(totalWrote)); + if (writeLength === 0) { + // Buffer is full, wait for space + await this.writeChannel.pop(); + } else { + totalWrote += writeLength; + if (this.sendWindowSize() > 0) { + this.notifySendPackets(); + } + } } - return writeLength; + return totalWrote; } closeWrite() { @@ -504,8 +514,8 @@ export class TcpState { receivedSegmentPromise = this.tcpQueue.pop(); if (this.handleSegment(result.segment)) { this.retransmissionQueue.ack(this.recvNext); + this.writeChannel.push(0); } else { - console.log("Dropping segment"); this.dropSegment(result.segment); } continue; @@ -544,8 +554,12 @@ export class TcpState { segment.sequenceNumber, segment.data.length, ); - // Repeat until we have no more data to send - } while (this.writeBuffer.bytesAvailable() > this.sendWindowSize()); + // Repeat until we have no more data to send, or the window is full + } while ( + this.writeBuffer.bytesAvailable() > + this.sendNext - this.sendUnacknowledged && + this.sendWindowSize() > 0 + ); } } @@ -667,13 +681,14 @@ class BytesBuffer { } write(data: Uint8Array): number { - const newLength = this.length + data.length; - if (newLength > this.buffer.length) { + if (this.length === this.buffer.length) { return 0; } - this.buffer.set(data, this.length); + const newLength = Math.min(this.buffer.length, this.length + data.length); + const dataSlice = data.subarray(0, newLength - this.length); + this.buffer.set(dataSlice, this.length); this.length = newLength; - return data.length; + return dataSlice.length; } bytesAvailable() { diff --git a/src/types/network-modules/tcpModule.ts b/src/types/network-modules/tcpModule.ts index 16f25a5..5547867 100644 --- a/src/types/network-modules/tcpModule.ts +++ b/src/types/network-modules/tcpModule.ts @@ -173,8 +173,15 @@ export class TcpSocket { return bytesRead; } + /** + * Writes data from the given buffer into the connection. + * This returns immediately, unless the connection is closed + * or the underlying buffer is full. + * @param buffer to copy the contents from + * @returns the number of bytes written + */ async write(content: Uint8Array) { - return this.tcpState.write(content); + return await this.tcpState.write(content); } closeWrite() { From 697a8bde31056b4c595e777f197fd40eec89f57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:00:36 -0300 Subject: [PATCH 24/40] feat: add delay between sent packets --- src/programs/http_client.ts | 13 +++-- src/types/network-modules/tcp/tcpState.ts | 64 +++++++++++------------ 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index a322999..379f085 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -85,10 +85,15 @@ export class HttpClient extends ProgramBase { // Read response const buffer = new Uint8Array(1024); - const readLength = await socket.readAll(buffer); - if (readLength < 0) { - console.error("HttpClient failed to read from socket"); - return; + const expectedLength = RESOURCE_MAP.get(this.resource)!.length; + let totalRead = 0; + while (totalRead < expectedLength) { + const readLength = await socket.read(buffer); + if (readLength < 0) { + console.error("HttpClient failed to read from socket"); + return; + } + totalRead += readLength; } } diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 0ca1f48..84e3bb8 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -490,7 +490,7 @@ export class TcpState { return; } this.notifiedSendPackets = true; - setTimeout(() => this.sendQueue.push(undefined), 5); + setTimeout(() => this.sendQueue.push(undefined), 15); } private async mainLoop() { @@ -525,41 +525,41 @@ export class TcpState { continue; } - do { - const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( - new Flags().withAck(), - ); + const segment = this.newSegment(this.sendNext, this.recvNext).withFlags( + new Flags().withAck(), + ); - const sendSize = Math.min(this.sendWindowSize(), MAX_SEGMENT_SIZE); - if (sendSize > 0) { - const data = new Uint8Array(sendSize); - const offset = - (u32_MODULUS + this.sendNext - this.sendUnacknowledged) % - u32_MODULUS; - const writeLength = this.writeBuffer.peek(offset, data); - - if (writeLength > 0) { - segment.withData(data.subarray(0, writeLength)); - this.sendNext = (this.sendNext + writeLength) % u32_MODULUS; - } - } - segment.window = this.recvWindow; + const sendSize = Math.min(this.sendWindowSize(), MAX_SEGMENT_SIZE); + if (sendSize > 0) { + const data = new Uint8Array(sendSize); + const offset = + (u32_MODULUS + this.sendNext - this.sendUnacknowledged) % u32_MODULUS; + const writeLength = this.writeBuffer.peek(offset, data); - if (this.sendNext === this.writeClosedSeqnum) { - this.sendNext = (this.sendNext + 1) % u32_MODULUS; - segment.flags.withFin(); + if (writeLength > 0) { + segment.withData(data.subarray(0, writeLength)); + this.sendNext = (this.sendNext + writeLength) % u32_MODULUS; } - sendIpPacket(this.srcHost, this.dstHost, segment); - this.retransmissionQueue.push( - segment.sequenceNumber, - segment.data.length, - ); - // Repeat until we have no more data to send, or the window is full - } while ( - this.writeBuffer.bytesAvailable() > - this.sendNext - this.sendUnacknowledged && - this.sendWindowSize() > 0 + } + segment.window = this.recvWindow; + + if (this.sendNext === this.writeClosedSeqnum) { + this.sendNext = (this.sendNext + 1) % u32_MODULUS; + segment.flags.withFin(); + } + sendIpPacket(this.srcHost, this.dstHost, segment); + this.retransmissionQueue.push( + segment.sequenceNumber, + segment.data.length, ); + // Repeat until we have no more data to send, or the window is full + const bytesInFlight = this.sendNext - this.sendUnacknowledged; + if ( + this.writeBuffer.bytesAvailable() > bytesInFlight && + this.sendWindowSize() > 0 + ) { + this.notifySendPackets(); + } } } From 7afdac3ed758727514530dbe22ee87a0bf2676d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:07:44 -0300 Subject: [PATCH 25/40] feat: add a fixed congestion window --- src/types/network-modules/tcp/tcpState.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 84e3bb8..b92e066 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -24,6 +24,8 @@ enum TcpStateEnum { } const MAX_BUFFER_SIZE = 0xffff; +const MAX_SEGMENT_SIZE = 1400; +const u32_MODULUS = 0x100000000; // 2^32 function getInitialSeqNumber() { // For random seqnums use: @@ -52,8 +54,6 @@ function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { sendViewPacket(src.viewgraph, src.id, frame); } -const u32_MODULUS = 0x100000000; - export class TcpState { private srcHost: ViewHost; private srcPort: Port; @@ -101,6 +101,8 @@ export class TcpState { // IRS private initialRecvSeqNum: number; + private cwnd = MAX_SEGMENT_SIZE; + constructor( srcHost: ViewHost, srcPort: Port, @@ -494,8 +496,6 @@ export class TcpState { } private async mainLoop() { - const MAX_SEGMENT_SIZE = 1400; - let recheckPromise = this.sendQueue.pop(); let receivedSegmentPromise = this.tcpQueue.pop(); let retransmitPromise = this.retransmissionQueue.pop(); @@ -587,9 +587,12 @@ export class TcpState { private sendWindowSize() { // TODO: add congestion control - return ( - (this.sendUnacknowledged + this.sendWindow - this.sendNext) % u32_MODULUS - ); + const rwnd = this.sendWindow; + const cwnd = this.cwnd; + + const windowSize = Math.min(rwnd, cwnd); + const bytesInFlight = this.sendNext - this.sendUnacknowledged; + return (windowSize - bytesInFlight) % u32_MODULUS; } } From f993f8a154b48c02608eae1ea52429260b9b6de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:16:24 -0300 Subject: [PATCH 26/40] feat: increase cwnd for each received ACK --- src/types/network-modules/tcp/tcpState.ts | 35 +++++++---------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index b92e066..db86ac7 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -101,7 +101,9 @@ export class TcpState { // IRS private initialRecvSeqNum: number; + private sshthreshold = -1; private cwnd = MAX_SEGMENT_SIZE; + private dupAckCount = 0; constructor( srcHost: ViewHost, @@ -311,10 +313,15 @@ export class TcpState { return false; } else { this.sendUnacknowledged = segment.acknowledgementNumber; + this.cwnd += MAX_SEGMENT_SIZE; } - if (!this.processAck(segment)) { - console.debug("ACK processing failed, dropping segment"); - return false; + + // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK + if (this.isSegmentNewer(segment)) { + // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. + this.sendWindow = segment.window; + this.seqNumForLastWindowUpdate = segment.sequenceNumber; + this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; } if (this.state === TcpStateEnum.FIN_WAIT_1) { @@ -449,28 +456,6 @@ export class TcpState { return this.sendUnacknowledged < ackNum && ackNum <= this.sendNext; } - private processAck(segment: TcpSegment): boolean { - // From https://datatracker.ietf.org/doc/html/rfc9293#section-3.10.7.4-2.5.2.2.2.3.1 - // If the ACK is for a packet not yet sent, drop it - if (this.sendNext < segment.acknowledgementNumber) { - return false; - } - // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK - if ( - this.sendUnacknowledged === undefined || - this.sendUnacknowledged <= segment.acknowledgementNumber - ) { - this.sendUnacknowledged = segment.acknowledgementNumber; - if (this.isSegmentNewer(segment)) { - // set SND.WND <- SEG.WND, set SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. - this.sendWindow = segment.window; - this.seqNumForLastWindowUpdate = segment.sequenceNumber; - this.ackNumForLastWindowUpdate = segment.acknowledgementNumber; - } - } - return true; - } - private isSegmentNewer(segment: TcpSegment): boolean { // Since both SEQ and ACK numbers are monotonic, we can use // them to determine if the segment is newer than the last From dc6481e6455d1d48c6d43d03e55b418fd352d4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 1 May 2025 17:26:21 -0300 Subject: [PATCH 27/40] feat: implement congestion control state machine --- src/types/network-modules/tcp/tcpState.ts | 135 ++++++++++++++++++++-- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index db86ac7..7cde53b 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -24,7 +24,7 @@ enum TcpStateEnum { } const MAX_BUFFER_SIZE = 0xffff; -const MAX_SEGMENT_SIZE = 1400; +const MAX_SEGMENT_SIZE = 1460; const u32_MODULUS = 0x100000000; // 2^32 function getInitialSeqNumber() { @@ -101,9 +101,7 @@ export class TcpState { // IRS private initialRecvSeqNum: number; - private sshthreshold = -1; - private cwnd = MAX_SEGMENT_SIZE; - private dupAckCount = 0; + private congestionControl: CongestionControl = new CongestionControl(); constructor( srcHost: ViewHost, @@ -303,7 +301,12 @@ export class TcpState { this.state === TcpStateEnum.CLOSE_WAIT || this.state === TcpStateEnum.CLOSING ) { - if (segment.acknowledgementNumber <= this.sendUnacknowledged) { + if (segment.acknowledgementNumber === this.sendUnacknowledged) { + // Duplicate ACK + if (!this.congestionControl.notifyDupAck()) { + // TODO: Retransmit missing segment + } + } else if (segment.acknowledgementNumber < this.sendUnacknowledged) { // Ignore the ACK } else if (segment.acknowledgementNumber > this.sendNext) { console.debug("ACK for future segment, dropping segment"); @@ -312,8 +315,15 @@ export class TcpState { ); return false; } else { + const acknowledgedBytes = + (u32_MODULUS + + segment.acknowledgementNumber - + this.sendUnacknowledged) % + u32_MODULUS; + this.congestionControl.notifyAck(acknowledgedBytes); this.sendUnacknowledged = segment.acknowledgementNumber; - this.cwnd += MAX_SEGMENT_SIZE; + // Transmit new segments + this.notifySendPackets(); } // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK @@ -506,7 +516,9 @@ export class TcpState { continue; } else if ("seqNum" in result) { retransmitPromise = this.retransmissionQueue.pop(); + // Retransmit the segment this.sendPacket(result.seqNum, result.size); + this.congestionControl.notifyTimeout(); continue; } @@ -573,7 +585,7 @@ export class TcpState { private sendWindowSize() { // TODO: add congestion control const rwnd = this.sendWindow; - const cwnd = this.cwnd; + const cwnd = this.congestionControl.getCwnd(); const windowSize = Math.min(rwnd, cwnd); const bytesInFlight = this.sendNext - this.sendUnacknowledged; @@ -687,3 +699,112 @@ class BytesBuffer { return this.bytesAvailable() === 0; } } + +type CongestionControlStateBehavior = + | SlowStart + | CongestionAvoidance + | FastRecovery; + +class CongestionControl { + private state: CongestionControlState; + private stateBehavior: CongestionControlStateBehavior; + + constructor() { + this.state = { + cwnd: 1 * MAX_SEGMENT_SIZE, + ssthresh: Infinity, + dupAckCount: 0, + }; + this.stateBehavior = new SlowStart(); + } + + getCwnd(): number { + return this.state.cwnd; + } + + notifyDupAck(): boolean { + this.stateBehavior.handleAck(this.state, 0); + return this.state.dupAckCount === 3; + } + + notifyAck(byteCount: number) { + this.stateBehavior.handleAck(this.state, byteCount); + } + + notifyTimeout() { + this.state.ssthresh = Math.floor(this.state.cwnd / 2); + this.state.cwnd = 1 * MAX_SEGMENT_SIZE; + this.state.dupAckCount = 0; + + this.stateBehavior = new SlowStart(); + } +} + +interface CongestionControlState { + // The congestion window size, as a number of MSS + cwnd: number; + // The slow start threshold + ssthresh: number; + // The number of duplicate ACKs received + dupAckCount: number; +} + +class SlowStart { + handleAck( + state: CongestionControlState, + byteCount: number, + ): CongestionControlStateBehavior { + if (byteCount === 0) { + // Duplicate ACK + state.dupAckCount++; + if (state.dupAckCount === 3) { + return this; + } + state.ssthresh = Math.floor(state.cwnd / 2); + state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; + return new FastRecovery(); + } + state.dupAckCount = 0; + state.cwnd += byteCount; + if (state.cwnd >= state.ssthresh) { + return new CongestionAvoidance(); + } + return this; + } +} + +class CongestionAvoidance { + handleAck( + state: CongestionControlState, + byteCount: number, + ): CongestionControlStateBehavior { + if (byteCount === 0) { + // Duplicate ACK + state.dupAckCount++; + if (state.dupAckCount === 3) { + return this; + } + state.ssthresh = Math.floor(state.cwnd / 2); + state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; + return new FastRecovery(); + } + state.dupAckCount = 0; + state.cwnd += (byteCount * MAX_SEGMENT_SIZE) / state.cwnd; + return this; + } +} + +class FastRecovery { + handleAck( + state: CongestionControlState, + byteCount: number, + ): CongestionControlStateBehavior { + if (byteCount === 0) { + // Duplicate ACK + state.cwnd += MAX_SEGMENT_SIZE; + return this; + } + state.cwnd = state.ssthresh; + return new CongestionAvoidance(); + } +} From e9c4dba71240fb2a7d5ad4ba54e6b07c21c19d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 1 May 2025 17:34:10 -0300 Subject: [PATCH 28/40] feat: retransmit first window segment on third duplicate ACK --- src/types/network-modules/tcp/tcpState.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 7cde53b..a45db15 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -304,7 +304,7 @@ export class TcpState { if (segment.acknowledgementNumber === this.sendUnacknowledged) { // Duplicate ACK if (!this.congestionControl.notifyDupAck()) { - // TODO: Retransmit missing segment + this.retransmissionQueue.retransmitFirstSegment(); } } else if (segment.acknowledgementNumber < this.sendUnacknowledged) { // Ignore the ACK @@ -643,6 +643,22 @@ class RetransmissionQueue { return true; }); } + + retransmitFirstSegment() { + if (this.timeoutQueue.length === 0) { + return; + } + let firstSegmentItem = this.timeoutQueue[0]; + this.timeoutQueue.forEach((element) => { + if (element[0].seqNum < firstSegmentItem[0].seqNum) { + firstSegmentItem = element; + } + }); + // Remove the segment from the queue + this.ack(firstSegmentItem[0].seqNum + 1); + // Mark the segment for retransmission + this.itemQueue.push(firstSegmentItem[0]); + } } class BytesBuffer { From ea773fc97426460b3217bc2730d437463bfdada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 1 May 2025 17:35:25 -0300 Subject: [PATCH 29/40] chore: fix lints --- src/programs/http_client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/programs/http_client.ts b/src/programs/http_client.ts index 379f085..d54d997 100644 --- a/src/programs/http_client.ts +++ b/src/programs/http_client.ts @@ -85,7 +85,7 @@ export class HttpClient extends ProgramBase { // Read response const buffer = new Uint8Array(1024); - const expectedLength = RESOURCE_MAP.get(this.resource)!.length; + const expectedLength = RESOURCE_MAP.get(this.resource)?.length || 0; let totalRead = 0; while (totalRead < expectedLength) { const readLength = await socket.read(buffer); @@ -186,7 +186,6 @@ export class HttpServer extends ProgramBase { const buffer = new Uint8Array(1024).fill(0); const readLength = await socket.readAll(buffer); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const readContents = buffer.slice(0, readLength); if (readLength < 0) { console.error("HttpServer failed to read from socket"); From b4d26f36b17592fbcf32304a6e00e4922542a676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 2 May 2025 20:08:49 -0300 Subject: [PATCH 30/40] fix: retransmit without triggering timeout --- src/types/network-modules/tcp/tcpState.ts | 52 ++++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index a45db15..15a70e4 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -55,6 +55,7 @@ function sendIpPacket(src: ViewHost, dst: ViewHost, payload: IpPayload) { } export class TcpState { + private ctx: GlobalContext; private srcHost: ViewHost; private srcPort: Port; private dstHost: ViewHost; @@ -101,7 +102,7 @@ export class TcpState { // IRS private initialRecvSeqNum: number; - private congestionControl: CongestionControl = new CongestionControl(); + private congestionControl = new CongestionControl(); constructor( srcHost: ViewHost, @@ -110,6 +111,7 @@ export class TcpState { dstPort: Port, tcpQueue: AsyncQueue, ) { + this.ctx = srcHost.ctx; this.srcHost = srcHost; this.srcPort = srcPort; this.dstHost = dstHost; @@ -301,12 +303,16 @@ export class TcpState { this.state === TcpStateEnum.CLOSE_WAIT || this.state === TcpStateEnum.CLOSING ) { - if (segment.acknowledgementNumber === this.sendUnacknowledged) { - // Duplicate ACK - if (!this.congestionControl.notifyDupAck()) { - this.retransmissionQueue.retransmitFirstSegment(); + if (segment.acknowledgementNumber <= this.sendUnacknowledged) { + if ( + segment.acknowledgementNumber === this.sendUnacknowledged && + segment.acknowledgementNumber !== this.writeClosedSeqnum + 1 + ) { + // Duplicate ACK + if (!this.congestionControl.notifyDupAck()) { + this.retransmitFirstSegment(); + } } - } else if (segment.acknowledgementNumber < this.sendUnacknowledged) { // Ignore the ACK } else if (segment.acknowledgementNumber > this.sendNext) { console.debug("ACK for future segment, dropping segment"); @@ -487,7 +493,10 @@ export class TcpState { return; } this.notifiedSendPackets = true; - setTimeout(() => this.sendQueue.push(undefined), 15); + setTimeout( + () => this.sendQueue.push(undefined), + 150 * this.ctx.getCurrentSpeed(), + ); } private async mainLoop() { @@ -582,6 +591,16 @@ export class TcpState { sendIpPacket(this.srcHost, this.dstHost, segment); } + private retransmitFirstSegment() { + // Remove item from the queue + const item = this.retransmissionQueue.getFirstSegment(); + if (!item) { + return; + } + // Resend packet + this.sendPacket(item.seqNum, item.size); + } + private sendWindowSize() { // TODO: add congestion control const rwnd = this.sendWindow; @@ -593,7 +612,7 @@ export class TcpState { } } -const RETRANSMIT_TIMEOUT = 15 * 1000; +const RETRANSMIT_TIMEOUT = 60 * 1000; interface RetransmissionQueueItem { seqNum: number; @@ -611,10 +630,7 @@ class RetransmissionQueue { } push(seqNum: number, size: number) { - const item = { - seqNum, - size, - }; + const item = { seqNum, size }; let progress = 0; const tick = (ticker: Ticker) => { progress += ticker.elapsedMS * this.ctx.getCurrentSpeed(); @@ -644,7 +660,7 @@ class RetransmissionQueue { }); } - retransmitFirstSegment() { + getFirstSegment() { if (this.timeoutQueue.length === 0) { return; } @@ -656,8 +672,7 @@ class RetransmissionQueue { }); // Remove the segment from the queue this.ack(firstSegmentItem[0].seqNum + 1); - // Mark the segment for retransmission - this.itemQueue.push(firstSegmentItem[0]); + return firstSegmentItem[0]; } } @@ -752,6 +767,7 @@ class CongestionControl { this.state.cwnd = 1 * MAX_SEGMENT_SIZE; this.state.dupAckCount = 0; + console.log("TCP Timeout. Switching to Slow Start"); this.stateBehavior = new SlowStart(); } } @@ -778,11 +794,15 @@ class SlowStart { } state.ssthresh = Math.floor(state.cwnd / 2); state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; + console.log("Triple duplicate ACK received. Switching to Fast Recovery"); return new FastRecovery(); } state.dupAckCount = 0; state.cwnd += byteCount; if (state.cwnd >= state.ssthresh) { + console.log( + "Reached the Slow Start Threshold. Switching to Congestion Avoidance", + ); return new CongestionAvoidance(); } return this; @@ -802,6 +822,7 @@ class CongestionAvoidance { } state.ssthresh = Math.floor(state.cwnd / 2); state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; + console.log("Triple duplicate ACK received. Switching to Fast Recovery"); return new FastRecovery(); } state.dupAckCount = 0; @@ -821,6 +842,7 @@ class FastRecovery { return this; } state.cwnd = state.ssthresh; + console.log("Fast recovery finished. Switching to Congestion Avoidance"); return new CongestionAvoidance(); } } From b127b0efc8d55685d7cbce54cd652a3210507e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 2 May 2025 20:33:46 -0300 Subject: [PATCH 31/40] feat: estimate RTT of connection --- src/types/network-modules/tcp/tcpState.ts | 92 ++++++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 15a70e4..9a094e6 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -102,6 +102,7 @@ export class TcpState { // IRS private initialRecvSeqNum: number; + private rttEstimator: RTTEstimator; private congestionControl = new CongestionControl(); constructor( @@ -120,6 +121,7 @@ export class TcpState { this.tcpQueue = tcpQueue; this.retransmissionQueue = new RetransmissionQueue(this.srcHost.ctx); + this.rttEstimator = new RTTEstimator(this.srcHost.ctx); this.mainLoop(); } @@ -136,6 +138,8 @@ export class TcpState { const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); sendIpPacket(this.srcHost, this.dstHost, segment); + this.rttEstimator.startMeasurement(this.initialSendSeqNum); + // Move to SYN_SENT state this.state = TcpStateEnum.SYN_SENT; await this.connectionQueue.pop(); @@ -159,15 +163,18 @@ export class TcpState { const flags = new Flags().withSyn().withAck(); const segment = this.newSegment(this.initialSendSeqNum, this.recvNext); sendIpPacket(this.srcHost, this.dstHost, segment.withFlags(flags)); + this.rttEstimator.startMeasurement(this.initialSendSeqNum); return true; } + // TODO: remove unused startConnection() { const flags = new Flags().withSyn(); const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); sendIpPacket(this.srcHost, this.dstHost, segment); } + // TODO: remove unused recvSynAck(segment: TcpSegment) { if (!segment.flags.syn || !segment.flags.ack) { return false; @@ -328,6 +335,7 @@ export class TcpState { u32_MODULUS; this.congestionControl.notifyAck(acknowledgedBytes); this.sendUnacknowledged = segment.acknowledgementNumber; + this.rttEstimator.finishMeasurement(segment.acknowledgementNumber); // Transmit new segments this.notifySendPackets(); } @@ -526,7 +534,7 @@ export class TcpState { } else if ("seqNum" in result) { retransmitPromise = this.retransmissionQueue.pop(); // Retransmit the segment - this.sendPacket(result.seqNum, result.size); + this.resendPacket(result.seqNum, result.size); this.congestionControl.notifyTimeout(); continue; } @@ -553,6 +561,7 @@ export class TcpState { this.sendNext = (this.sendNext + 1) % u32_MODULUS; segment.flags.withFin(); } + this.rttEstimator.startMeasurement(segment.sequenceNumber); sendIpPacket(this.srcHost, this.dstHost, segment); this.retransmissionQueue.push( segment.sequenceNumber, @@ -569,7 +578,7 @@ export class TcpState { } } - private sendPacket(seqNum: number, size: number) { + private resendPacket(seqNum: number, size: number) { const segment = this.newSegment(seqNum, this.recvNext).withFlags( new Flags().withAck(), ); @@ -589,6 +598,7 @@ export class TcpState { } this.retransmissionQueue.push(segment.sequenceNumber, segment.data.length); sendIpPacket(this.srcHost, this.dstHost, segment); + this.rttEstimator.discardMeasurement(segment.sequenceNumber); } private retransmitFirstSegment() { @@ -598,7 +608,7 @@ export class TcpState { return; } // Resend packet - this.sendPacket(item.seqNum, item.size); + this.resendPacket(item.seqNum, item.size); } private sendWindowSize() { @@ -846,3 +856,79 @@ class FastRecovery { return new CongestionAvoidance(); } } + +// Weight for the latest RTT sample when estimating the RTT. +// The recommended value is 1/8 +const RTT_SAMPLE_WEIGHT = 0.125; +// Weight for the latest sample when estimating the deviation. +// The recommended value is 1/4 +const DEV_SAMPLE_WEIGHT = 0.25; + +class RTTEstimator { + private ctx: GlobalContext; + // Estimated Round Trip Time + // Initially set to 60 seconds + private estimatedRTT = 60 * 1000; + private devRTT = 0; + + private measuring = false; + private currentSample = { + seqNum: 0, + rtt: 0, + }; + + constructor(ctx: GlobalContext) { + this.ctx = ctx; + } + + getRtt() { + return this.estimatedRTT + 4 * this.devRTT; + } + + startMeasurement(seqNum: number) { + if (this.measuring) { + return; + } + // Start measuring the RTT for the segment + this.currentSample.rtt = 0; + this.currentSample.seqNum = seqNum; + this.measuring = true; + Ticker.shared.add(this.measureTick, this); + } + + finishMeasurement(ackNum: number) { + if (!this.measuring || ackNum < this.currentSample.seqNum) { + // Ignore the ACK + return; + } + // Stop measuring the RTT for the segment + this.measuring = false; + Ticker.shared.remove(this.measureTick, this); + + // Update the estimated RTT and deviation + const sampleRTT = this.currentSample.rtt; + + this.estimatedRTT = + (1 - RTT_SAMPLE_WEIGHT) * this.estimatedRTT + + RTT_SAMPLE_WEIGHT * sampleRTT; + + this.devRTT = + (1 - DEV_SAMPLE_WEIGHT) * this.devRTT + + DEV_SAMPLE_WEIGHT * Math.abs(sampleRTT - this.estimatedRTT); + } + + discardMeasurement(seqNum: number) { + if (!this.measuring || seqNum !== this.currentSample.seqNum) { + return; + } + // Stop measuring the RTT for the segment + this.measuring = false; + Ticker.shared.remove(this.measureTick, this); + } + + private measureTick(ticker: Ticker) { + // Update the current sample's RTT + // NOTE: we do this to account for the simulation's speed + this.currentSample.rtt += ticker.elapsedMS * this.ctx.getCurrentSpeed(); + } +} From b9b536183afdf2c2eb4dc9513c61745a872e3172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 2 May 2025 20:36:54 -0300 Subject: [PATCH 32/40] feat: use estimated RTT for timeouts --- src/types/network-modules/tcp/tcpState.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 9a094e6..c2e892b 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -120,8 +120,11 @@ export class TcpState { this.tcpQueue = tcpQueue; - this.retransmissionQueue = new RetransmissionQueue(this.srcHost.ctx); this.rttEstimator = new RTTEstimator(this.srcHost.ctx); + this.retransmissionQueue = new RetransmissionQueue( + this.srcHost.ctx, + this.rttEstimator, + ); this.mainLoop(); } @@ -622,8 +625,6 @@ export class TcpState { } } -const RETRANSMIT_TIMEOUT = 60 * 1000; - interface RetransmissionQueueItem { seqNum: number; size: number; @@ -634,9 +635,11 @@ class RetransmissionQueue { private itemQueue = new AsyncQueue(); private ctx: GlobalContext; + private rttEstimator: RTTEstimator; - constructor(ctx: GlobalContext) { + constructor(ctx: GlobalContext, rttEstimator: RTTEstimator) { this.ctx = ctx; + this.rttEstimator = rttEstimator; } push(seqNum: number, size: number) { @@ -644,7 +647,7 @@ class RetransmissionQueue { let progress = 0; const tick = (ticker: Ticker) => { progress += ticker.elapsedMS * this.ctx.getCurrentSpeed(); - if (progress >= RETRANSMIT_TIMEOUT) { + if (progress >= this.rttEstimator.getRtt()) { this.itemQueue.push(item); Ticker.shared.remove(tick, this); } From 6dd5b7af50c747402124f838a466e4b9f23846b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 2 May 2025 21:18:13 -0300 Subject: [PATCH 33/40] fix: remove bytes from buffer once ACKed --- src/types/network-modules/tcp/tcpState.ts | 49 +++++++++++++++-------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index c2e892b..e287fd8 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -331,16 +331,7 @@ export class TcpState { ); return false; } else { - const acknowledgedBytes = - (u32_MODULUS + - segment.acknowledgementNumber - - this.sendUnacknowledged) % - u32_MODULUS; - this.congestionControl.notifyAck(acknowledgedBytes); - this.sendUnacknowledged = segment.acknowledgementNumber; - this.rttEstimator.finishMeasurement(segment.acknowledgementNumber); - // Transmit new segments - this.notifySendPackets(); + this.processAck(segment); } // If SND.UNA < SEG.ACK =< SND.NXT, set SND.UNA <- SEG.ACK @@ -497,6 +488,35 @@ export class TcpState { ); } + private processAck(segment: TcpSegment) { + const ackNum = segment.acknowledgementNumber; + // Don't count the FIN or SYN bytes + const finByte = + (ackNum === this.writeClosedSeqnum + 1 ? 1 : 0) + + (ackNum === this.initialSendSeqNum + 1 ? 1 : 0); + + const acknowledgedBytes = + (u32_MODULUS + ackNum - this.sendUnacknowledged - finByte) % u32_MODULUS; + + if (acknowledgedBytes === 0) { + return; + } + + // Remove ACKed bytes from queue + this.retransmissionQueue.ack(ackNum); + this.writeBuffer.shift(acknowledgedBytes); + this.writeChannel.push(0); + + // Notify Congestion Control module + this.congestionControl.notifyAck(acknowledgedBytes); + this.sendUnacknowledged = ackNum; + // Update RTT estimations + this.rttEstimator.finishMeasurement(ackNum); + + // Transmit new segments + this.notifySendPackets(); + } + private notifiedSendPackets = false; private notifySendPackets() { @@ -527,10 +547,7 @@ export class TcpState { this.notifiedSendPackets = false; } else if ("segment" in result) { receivedSegmentPromise = this.tcpQueue.pop(); - if (this.handleSegment(result.segment)) { - this.retransmissionQueue.ack(this.recvNext); - this.writeChannel.push(0); - } else { + if (!this.handleSegment(result.segment)) { this.dropSegment(result.segment); } continue; @@ -870,8 +887,8 @@ const DEV_SAMPLE_WEIGHT = 0.25; class RTTEstimator { private ctx: GlobalContext; // Estimated Round Trip Time - // Initially set to 60 seconds - private estimatedRTT = 60 * 1000; + // Initially set to 20 seconds + private estimatedRTT = 20 * 1000; private devRTT = 0; private measuring = false; From f2510a38de967f9990bb11b09c37520e674e7fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 4 May 2025 20:21:17 -0300 Subject: [PATCH 34/40] fix: set initial timeout to 60s --- src/types/network-modules/tcp/tcpState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index e287fd8..0fd82b8 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -888,7 +888,7 @@ class RTTEstimator { private ctx: GlobalContext; // Estimated Round Trip Time // Initially set to 20 seconds - private estimatedRTT = 20 * 1000; + private estimatedRTT = 60 * 1000; private devRTT = 0; private measuring = false; From 528666ee9a8990f9c36c07a288ce910907094d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 10 May 2025 16:24:39 -0300 Subject: [PATCH 35/40] fix: use different flag for write closed --- src/types/network-modules/tcp/tcpState.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 261e02d..690613c 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -87,6 +87,7 @@ export class TcpState { private writeBuffer = new BytesBuffer(MAX_BUFFER_SIZE); private writeChannel = new AsyncQueue(); private writeClosedSeqnum = -1; + private writeClosed = false; private state: TcpStateEnum; @@ -517,6 +518,10 @@ export class TcpState { (ackNum === this.writeClosedSeqnum + 1 ? 1 : 0) + (ackNum === this.initialSendSeqNum + 1 ? 1 : 0); + if (ackNum === this.writeClosedSeqnum + 1) { + this.writeClosed = true; + } + const acknowledgedBytes = (u32_MODULUS + ackNum - this.sendUnacknowledged - finByte) % u32_MODULUS; @@ -548,7 +553,7 @@ export class TcpState { this.notifiedSendPackets = true; setTimeout( () => this.sendQueue.push(undefined), - 150 * this.ctx.getCurrentSpeed(), + 10 * this.ctx.getCurrentSpeed(), ); } @@ -557,7 +562,7 @@ export class TcpState { let receivedSegmentPromise = this.tcpQueue.pop(); let retransmitPromise = this.retransmissionQueue.pop(); - while (!this.readClosed || this.writeClosedSeqnum === -1) { + while (!this.readClosed || !this.writeClosed) { const result = await Promise.race([ recheckPromise, receivedSegmentPromise, From 05fabb4ce410a08916c82a2a6981031f2d4b6028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 10 May 2025 16:25:00 -0300 Subject: [PATCH 36/40] fix: use single timer for retransmission timeout As the TCP spec says... --- src/types/network-modules/tcp/tcpState.ts | 85 ++++++++++++++--------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 690613c..85e3b91 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -652,7 +652,7 @@ export class TcpState { private retransmitFirstSegment() { // Remove item from the queue - const item = this.retransmissionQueue.getFirstSegment(); + const item = this.retransmissionQueue.popFirstSegment(); if (!item) { return; } @@ -677,8 +677,9 @@ interface RetransmissionQueueItem { } class RetransmissionQueue { - private timeoutQueue: [RetransmissionQueueItem, (t: Ticker) => void][] = []; - private itemQueue = new AsyncQueue(); + private timeoutQueue = new AsyncQueue(); + private timeoutTick: (t: Ticker) => void = null; + private itemQueue: RetransmissionQueueItem[] = []; private ctx: GlobalContext; private rttEstimator: RTTEstimator; @@ -690,48 +691,70 @@ class RetransmissionQueue { push(seqNum: number, size: number) { const item = { seqNum, size }; - let progress = 0; - const tick = (ticker: Ticker) => { - progress += ticker.elapsedMS * this.ctx.getCurrentSpeed(); - if (progress >= this.rttEstimator.getRtt()) { - this.itemQueue.push(item); - Ticker.shared.remove(tick, this); - } - }; - Ticker.shared.add(tick, this); - this.timeoutQueue.push([item, tick]); + this.itemQueue.push(item); + this.itemQueue.sort((a, b) => a.seqNum - b.seqNum); + + if (!this.timeoutTick) { + this.startTimer(); + } } + /** + * Waits for the timeout to expire and returns the first item in the queue. + * @returns the first item in the queue + */ async pop() { - return await this.itemQueue.pop(); + await this.timeoutQueue.pop(); + const firstItem = this.itemQueue.shift(); + if (this.itemQueue.length > 0) { + this.startTimer(); + } + return firstItem; } ack(ackNum: number) { - this.timeoutQueue = this.timeoutQueue.filter(([item, tick]) => { - if ( + this.itemQueue = this.itemQueue.filter((item) => { + return !( item.seqNum < ackNum || (item.seqNum + item.size) % u32_MODULUS <= ackNum - ) { - Ticker.shared.remove(tick, this); - return false; - } - return true; + ); }); + if (this.itemQueue.length === 0) { + this.stopTimer(); + } } - getFirstSegment() { - if (this.timeoutQueue.length === 0) { + popFirstSegment() { + if (this.itemQueue.length === 0) { return; } - let firstSegmentItem = this.timeoutQueue[0]; - this.timeoutQueue.forEach((element) => { - if (element[0].seqNum < firstSegmentItem[0].seqNum) { - firstSegmentItem = element; - } - }); + const firstSegmentItem = this.itemQueue[0]; // Remove the segment from the queue - this.ack(firstSegmentItem[0].seqNum + 1); - return firstSegmentItem[0]; + this.ack(firstSegmentItem.seqNum + 1); + return firstSegmentItem; + } + + private startTimer() { + if (this.timeoutTick) { + return; + } + let progress = 0; + const tick = (ticker: Ticker) => { + progress += ticker.elapsedMS * this.ctx.getCurrentSpeed(); + if (progress >= this.rttEstimator.getRtt()) { + this.timeoutQueue.push(undefined); + this.stopTimer(); + } + }; + this.timeoutTick = tick; + Ticker.shared.add(this.timeoutTick, this); + } + + private stopTimer() { + if (this.timeoutTick) { + Ticker.shared.remove(this.timeoutTick, this); + this.timeoutTick = null; + } } } From fff8fcf74a537b5baab95b22b5bb926932ae58b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 10 May 2025 19:58:12 -0300 Subject: [PATCH 37/40] chore: remove unused functions --- src/types/network-modules/tcp/tcpState.ts | 30 ----------------------- 1 file changed, 30 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 85e3b91..afe8ea0 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -191,36 +191,6 @@ export class TcpState { return true; } - // TODO: remove unused - startConnection() { - const flags = new Flags().withSyn(); - const segment = this.newSegment(this.initialSendSeqNum, 0).withFlags(flags); - // TODO: check what to do in case the packet couldn't be sent - sendIpPacket(this.srcHost, this.dstHost, segment); - } - - // TODO: remove unused - recvSynAck(segment: TcpSegment) { - if (!segment.flags.syn || !segment.flags.ack) { - return false; - } - if ( - segment.acknowledgementNumber !== - (this.initialSendSeqNum + 1) % u32_MODULUS - ) { - return false; - } - this.recvNext = (segment.sequenceNumber + 1) % u32_MODULUS; - this.initialRecvSeqNum = segment.sequenceNumber; - this.sendNext = segment.acknowledgementNumber; - this.sendWindow = segment.window; - - const ackSegment = this.newSegment(this.sendNext, this.recvNext); - ackSegment.withFlags(new Flags().withAck()); - // TODO: check what to do in case the packet couldn't be sent - sendIpPacket(this.srcHost, this.dstHost, ackSegment); - } - private handleSegment(segment: TcpSegment) { // Sanity check: ports match with expected if ( From 326330b4ddd8983172eb5a780c614fb18579888b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 10 May 2025 20:01:51 -0300 Subject: [PATCH 38/40] chore: address TODOs --- src/types/network-modules/tcp/tcpState.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index afe8ea0..b830c49 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -185,8 +185,10 @@ export class TcpState { // Send a SYN-ACK const flags = new Flags().withSyn().withAck(); const segment = this.newSegment(this.initialSendSeqNum, this.recvNext); - // TODO: check what to do in case the packet couldn't be sent - sendIpPacket(this.srcHost, this.dstHost, segment.withFlags(flags)); + + if (!sendIpPacket(this.srcHost, this.dstHost, segment.withFlags(flags))) { + return false; + } this.rttEstimator.startMeasurement(this.initialSendSeqNum); return true; } @@ -578,9 +580,11 @@ export class TcpState { this.sendNext = (this.sendNext + 1) % u32_MODULUS; segment.flags.withFin(); } - this.rttEstimator.startMeasurement(segment.sequenceNumber); - // TODO: check what to do in case the packet couldn't be sent - sendIpPacket(this.srcHost, this.dstHost, segment); + + // Ignore failed sends + if (sendIpPacket(this.srcHost, this.dstHost, segment)) { + this.rttEstimator.startMeasurement(segment.sequenceNumber); + } this.retransmissionQueue.push( segment.sequenceNumber, segment.data.length, @@ -615,9 +619,10 @@ export class TcpState { segment.flags.withFin(); } this.retransmissionQueue.push(segment.sequenceNumber, segment.data.length); - // TODO: check what to do in case the packet couldn't be sent - sendIpPacket(this.srcHost, this.dstHost, segment); - this.rttEstimator.discardMeasurement(segment.sequenceNumber); + // Ignore failed sends + if (sendIpPacket(this.srcHost, this.dstHost, segment)) { + this.rttEstimator.discardMeasurement(segment.sequenceNumber); + } } private retransmitFirstSegment() { @@ -631,7 +636,6 @@ export class TcpState { } private sendWindowSize() { - // TODO: add congestion control const rwnd = this.sendWindow; const cwnd = this.congestionControl.getCwnd(); From 2580522c38d3b66ee7c0ad2c40c1e0bc9c92bb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 11 May 2025 13:11:58 -0300 Subject: [PATCH 39/40] fix: invert if condition --- src/types/network-modules/tcp/tcpState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index b830c49..9a8edd2 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -845,7 +845,7 @@ class SlowStart { if (byteCount === 0) { // Duplicate ACK state.dupAckCount++; - if (state.dupAckCount === 3) { + if (state.dupAckCount !== 3) { return this; } state.ssthresh = Math.floor(state.cwnd / 2); @@ -873,7 +873,7 @@ class CongestionAvoidance { if (byteCount === 0) { // Duplicate ACK state.dupAckCount++; - if (state.dupAckCount === 3) { + if (state.dupAckCount !== 3) { return this; } state.ssthresh = Math.floor(state.cwnd / 2); From 7e6bd1b3bb44e77a702d39fe942c04a06fa11b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 11 May 2025 13:12:56 -0300 Subject: [PATCH 40/40] refactor: move repeated logic to helper --- src/types/network-modules/tcp/tcpState.ts | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/types/network-modules/tcp/tcpState.ts b/src/types/network-modules/tcp/tcpState.ts index 9a8edd2..8cc48f4 100644 --- a/src/types/network-modules/tcp/tcpState.ts +++ b/src/types/network-modules/tcp/tcpState.ts @@ -837,21 +837,27 @@ interface CongestionControlState { dupAckCount: number; } +function handleDupAck( + state: CongestionControlState, + currentState: CongestionControlStateBehavior, +) { + state.dupAckCount++; + if (state.dupAckCount !== 3) { + return currentState; + } + state.ssthresh = Math.floor(state.cwnd / 2); + state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; + console.log("Triple duplicate ACK received. Switching to Fast Recovery"); + return new FastRecovery(); +} + class SlowStart { handleAck( state: CongestionControlState, byteCount: number, ): CongestionControlStateBehavior { if (byteCount === 0) { - // Duplicate ACK - state.dupAckCount++; - if (state.dupAckCount !== 3) { - return this; - } - state.ssthresh = Math.floor(state.cwnd / 2); - state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; - console.log("Triple duplicate ACK received. Switching to Fast Recovery"); - return new FastRecovery(); + return handleDupAck(state, this); } state.dupAckCount = 0; state.cwnd += byteCount; @@ -871,15 +877,7 @@ class CongestionAvoidance { byteCount: number, ): CongestionControlStateBehavior { if (byteCount === 0) { - // Duplicate ACK - state.dupAckCount++; - if (state.dupAckCount !== 3) { - return this; - } - state.ssthresh = Math.floor(state.cwnd / 2); - state.cwnd = state.ssthresh + 3 * MAX_SEGMENT_SIZE; - console.log("Triple duplicate ACK received. Switching to Fast Recovery"); - return new FastRecovery(); + return handleDupAck(state, this); } state.dupAckCount = 0; state.cwnd += (byteCount * MAX_SEGMENT_SIZE) / state.cwnd;