Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/packets/ip.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FramePayload, IP_PROTOCOL_TYPE } from "./ethernet";

// Taken from here: https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
export const ICMP_PROTOCOL_NUMBER = 1;
export const TCP_PROTOCOL_NUMBER = 6;
export const UDP_PROTOCOL_NUMBER = 17;
Expand All @@ -9,6 +10,7 @@ export class EmptyPayload implements IpPayload {
return new Uint8Array(0);
}
protocol() {
// This number is reserved for experimental protocols
return 0xfd;
}
}
Expand Down Expand Up @@ -102,7 +104,9 @@ export class IpAddressGenerator {
}

export interface IpPayload {
// The bytes equivalent of the payload
toBytes(): Uint8Array;
// The number of the protocol
protocol(): number;
}

Expand Down Expand Up @@ -242,7 +246,7 @@ export class IPv4Packet implements FramePayload {
export function computeIpChecksum(octets: Uint8Array): number {
const sum = octets.reduce((acc, octet, i) => {
return acc + (octet << (8 * (1 - (i % 2))));
});
}, 0);
const checksum = sum & 0xffff;
const carry = sum >> 16;
return 0xffff ^ (checksum + carry);
Expand Down
226 changes: 226 additions & 0 deletions src/packets/tcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import {
computeIpChecksum,
IpAddress,
IpPayload,
TCP_PROTOCOL_NUMBER,
} from "./ip";

export class Flags {
// Urgent Pointer field significant
public urg = false;
// Acknowledgment field significant
public ack = false;
// Push function
public psh = false;
// Reset the connection
public rst = false;
// Synchronize sequence numbers
public syn = false;
// No more data from sender
public fin = false;

// 6 bits
toByte(): number {
return [this.urg, this.ack, this.psh, this.rst, this.syn, this.fin].reduce(
(acc, flag, index) => {
return acc | bitSet(flag, 5 - index);
},
0,
);
}

withUrg(urg = true): Flags {
this.urg = urg;
return this;
}

withAck(ack = true): Flags {
this.ack = ack;
return this;
}

withPsh(psh = true): Flags {
this.psh = psh;
return this;
}

withRst(rst = true): Flags {
this.rst = rst;
return this;
}

withSyn(syn = true): Flags {
this.syn = syn;
return this;
}

withFin(fin = true): Flags {
this.fin = fin;
return this;
}
}

function bitSet(value: boolean, bit: number): number {
return value ? 1 << bit : 0;
}

export class TcpSegment implements IpPayload {
// Info taken from the original RFC: https://www.ietf.org/rfc/rfc793.txt
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Source Port | Destination Port |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Sequence Number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Acknowledgment Number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Data | |U|A|P|R|S|F| |
// | Offset| Reserved |R|C|S|S|Y|I| Window |
// | | |G|K|H|T|N|N| |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Checksum | Urgent Pointer |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Options | Padding |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | data |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// 2 bytes
// The source port number.
sourcePort: number;
// 2 bytes
// The destination port number.
destinationPort: number;
// 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
// initial sequence number (ISN) and the first data octet is ISN+1.
sequenceNumber: number;
// 4 bytes
// If the ACK control bit is set this field contains the value of the
// next sequence number the sender of the segment is expecting to
// receive. Once a connection is established this is always sent.
acknowledgementNumber: number;

// 4 bits
// 4-byte offset from the start of the TCP segment to the start of the data
readonly dataOffset: number = 5;

// 6 bits
// Reserved for future use. Must be zero.
readonly reserved: number = 0;

// Control bits
// 6 bits
flags: Flags;

// 2 bytes
// The number of data octets beginning with the one indicated in the
// acknowledgment field which the sender of this segment is willing to
// accept.
public window = 0xffff;

// 2 bytes
get checksum(): number {
return this.computeChecksum();
}

// 2 bytes
readonly urgentPointer = 0;

// 0-40 bytes
// TODO: implement options
// options: Option[];

// Variable size
data: Uint8Array;

// Used for the checksum calculation
srcIpAddress: IpAddress;
dstIpAddress: IpAddress;

constructor(
srcPort: number,
dstPort: number,
seqNum: number,
ackNum: number,
flags: Flags,
data: Uint8Array,
) {
checkUint(srcPort, 16);
checkUint(dstPort, 16);
checkUint(seqNum, 32);
checkUint(ackNum, 32);

this.sourcePort = srcPort;
this.destinationPort = dstPort;
this.sequenceNumber = seqNum;
this.acknowledgementNumber = ackNum;
this.flags = flags;
this.data = data;
}

computeChecksum(): number {
const segmentBytes = this.toBytes({ withChecksum: false });

const pseudoHeaderBytes = Uint8Array.from([
...this.srcIpAddress.octets,
...this.dstIpAddress.octets,
0,
TCP_PROTOCOL_NUMBER,
...uintToBytes(segmentBytes.length, 2),
]);
const totalBytes = Uint8Array.from([...pseudoHeaderBytes, ...segmentBytes]);
return computeIpChecksum(totalBytes);
}

// ### IpPayload ###
toBytes({
withChecksum = true,
}: { withChecksum?: boolean } = {}): Uint8Array {
const checksum = withChecksum ? this.checksum : 0;
return Uint8Array.from([
...uintToBytes(this.sourcePort, 2),
...uintToBytes(this.destinationPort, 2),
...uintToBytes(this.sequenceNumber, 4),
...uintToBytes(this.acknowledgementNumber, 4),
(this.dataOffset << 4) | this.reserved,
((this.reserved & 0b11) << 6) | this.flags.toByte(),
...uintToBytes(this.window, 2),
...uintToBytes(checksum, 2),
...uintToBytes(this.urgentPointer, 2),
...this.data,
]);
}

protocol(): number {
return TCP_PROTOCOL_NUMBER;
}
// ### IpPayload ###
}

function checkUint(n: number, numBits: number): void {
if (numBits > 32) {
throw new Error("Bitwidth more than 32 not supported");
}
// >>> 0 is to turn this into an unsigned integer again
// https://stackoverflow.com/a/34897012
const max = numBits === 32 ? 0xffffffff : ((1 << numBits) >>> 0) - 1;
if (n < 0 || n > max) {
throw new Error("Invalid value for uint" + numBits + ": " + n);
}
}

function uintToBytes(n: number, numBytes: number): Uint8Array {
const original = n;
const bytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i++) {
bytes[numBytes - i - 1] = n & 0xff;
n >>>= 8;
}
if (n !== 0) {
throw new Error("Value too large for " + numBytes + " bytes: " + original);
}
return bytes;
}
60 changes: 60 additions & 0 deletions test/tcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { IpAddress, TCP_PROTOCOL_NUMBER } from "../src/packets/ip";
import { Flags, TcpSegment } from "../src/packets/tcp";

describe("TCP module", () => {
test("constructor works", () => {
new TcpSegment(0, 0, 0, 0, new Flags(), Uint8Array.of());
new TcpSegment(
0xffff,
0xffff,
0xffffffff,
0xffffffff,
new Flags(),
Uint8Array.of(),
);
});

const flags = new Flags().withAck().withRst();

let testSegment = new TcpSegment(
0x4e0,
0xbf08,
0,
0x9b84d209,
flags,
Uint8Array.of(),
);
testSegment.window = 0;
testSegment.srcIpAddress = IpAddress.parse("127.0.0.1");
testSegment.dstIpAddress = IpAddress.parse("127.0.0.1");

test("Checksum works", () => {
const expectedChecksum = 0x8057;
expect(testSegment.checksum).toBe(expectedChecksum);
});

test("toBytes works", () => {
const bytes = Uint8Array.from([
// Source port
0x04, 0xe0,
// Destination port
0xbf, 0x08,
// Sequence Number
0x00, 0x00, 0x00, 0x00,
// Acknowledgment Number
0x9b, 0x84, 0xd2, 0x09,
// Data offset (4 bits) = 5
// Reserved (4 bits) = 0
// Flags (RST+ACK) = 0x14
0x50, 0x14,
// Window
0x00, 0x00,
// Checksum
0x80, 0x57,
// Urgent pointer
0x00, 0x00,
// No data
]);
expect(testSegment.toBytes().toString()).toBe(bytes.toString());
});
});
Loading