Skip to content

Commit cabd95b

Browse files
authored
feat: Ethernet frames (#78)
1 parent 1b5e6c0 commit cabd95b

File tree

7 files changed

+293
-36
lines changed

7 files changed

+293
-36
lines changed

package-lock.json

Lines changed: 60 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
22
"dependencies": {
33
"@pixi/filter-outline": "^5.2.0",
4+
"@tsxper/crc32": "^2.1.3",
45
"pixi-viewport": "^5.0.3",
56
"pixi.js": "^8.4.1"
67
},
78
"name": "netsim",
89
"version": "1.0.0",
910
"private": true,
1011
"devDependencies": {
11-
"@eslint/js": "^9.14.0",
12+
"@eslint/js": "9.18.0",
1213
"@types/eslint__js": "^8.42.3",
1314
"@types/jest": "^29.5.14",
14-
"@types/pixi.js": "^5.0.0",
1515
"css-loader": "7.1.2",
1616
"eslint": "^9.14.0",
1717
"html-webpack-plugin": "^5.6.0",

src/packets/ethernet.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { CRC32 } from "@tsxper/crc32";
2+
3+
// From https://en.wikipedia.org/wiki/EtherType
4+
export const IP_PROTOCOL_TYPE = 0x0800;
5+
export const ARP_PROTOCOL_TYPE = 0x0806;
6+
export const IPV6_PROTOCOL_TYPE = 0x86dd;
7+
8+
/// Medium Access Control (MAC) address
9+
export class MacAddress {
10+
// 6 bytes
11+
octets: Uint8Array;
12+
13+
constructor(octets: Uint8Array) {
14+
if (octets.length !== 6) {
15+
throw new Error("Invalid MAC address");
16+
}
17+
this.octets = octets;
18+
}
19+
20+
// Parse MAC address from a string representation (00:1b:63:84:45:e6)
21+
static parse(addrString: string): MacAddress {
22+
const octets = new Uint8Array(6);
23+
addrString.split(":").forEach((octet, i) => {
24+
const octetInt = parseInt(octet, 16);
25+
if (isNaN(octetInt) || octetInt < 0 || octetInt > 255) {
26+
throw new Error(`Invalid MAC address: ${addrString}`);
27+
}
28+
octets[i] = octetInt;
29+
});
30+
return new this(octets);
31+
}
32+
33+
// Turn to string
34+
toString(): string {
35+
return Array.from(this.octets)
36+
.map((d) => d.toString(16))
37+
.join(":");
38+
}
39+
40+
// Check if two MAC addresses are equal.
41+
equals(other: MacAddress): boolean {
42+
return this.octets.every((octet, index) => octet === other.octets[index]);
43+
}
44+
}
45+
46+
const crc32 = new CRC32();
47+
48+
const MINIMUM_PAYLOAD_SIZE = 46;
49+
50+
export class EthernetFrame {
51+
// Info taken from Computer Networking: A Top-Down Approach
52+
53+
// 8 bytes
54+
// 7 bytes preamble and 1 byte start of frame delimiter
55+
// Used to synchronize the communication
56+
// TODO: should we mention this somewhere?
57+
// readonly preamble = new Uint8Array([
58+
// 0b10101010, 0b10101010, 0b10101010, 0b10101010, 0b10101010, 0b10101010,
59+
// 0b10101010, 0b10101011,
60+
// ]);
61+
62+
// 6 bytes
63+
// Destination MAC address
64+
destination: MacAddress;
65+
// 6 bytes
66+
// Source MAC address
67+
source: MacAddress;
68+
// 2 bytes
69+
// The payload's type
70+
type: number;
71+
// 46-1500 bytes
72+
// If the payload is smaller than 46 bytes, it is padded.
73+
// TODO: make this an interface
74+
// The payload
75+
payload: FramePayload;
76+
// 4 bytes
77+
// Cyclic Redundancy Check (CRC)
78+
get crc(): number {
79+
// Computation doesn't include preamble
80+
const frameBytes = this.toBytes({
81+
withChecksum: false,
82+
});
83+
return crc32.forBytes(frameBytes);
84+
}
85+
86+
constructor(
87+
source: MacAddress,
88+
destination: MacAddress,
89+
payload: FramePayload,
90+
) {
91+
this.destination = destination;
92+
this.source = source;
93+
this.type = payload.type();
94+
this.payload = payload;
95+
}
96+
97+
toBytes({ withChecksum = true }: { withChecksum?: boolean } = {}) {
98+
let checksum: number[] = [];
99+
if (withChecksum) {
100+
const crc = this.crc;
101+
checksum = [crc & 0xff, (crc >> 8) & 0xff, (crc >> 16) & 0xff, crc >> 24];
102+
}
103+
let payload = this.payload.toBytes();
104+
if (payload.length < MINIMUM_PAYLOAD_SIZE) {
105+
const padding = new Array(MINIMUM_PAYLOAD_SIZE - payload.length);
106+
payload = Uint8Array.from([...payload, ...padding]);
107+
}
108+
return Uint8Array.from([
109+
...this.destination.octets,
110+
...this.source.octets,
111+
this.type >> 8,
112+
this.type & 0xff,
113+
...payload,
114+
...checksum,
115+
]);
116+
}
117+
}
118+
119+
export interface FramePayload {
120+
toBytes(): Uint8Array;
121+
type(): number;
122+
}

src/packets/ip.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { FramePayload, IP_PROTOCOL_TYPE } from "./ethernet";
2+
13
export const ICMP_PROTOCOL_NUMBER = 1;
24
export const TCP_PROTOCOL_NUMBER = 6;
35
export const UDP_PROTOCOL_NUMBER = 17;
@@ -11,7 +13,10 @@ export class EmptyPayload implements IpPayload {
1113
}
1214
}
1315

16+
/// Internet Protocol (IP) address
17+
// TODO: support IPv6?
1418
export class IpAddress {
19+
// 4 bytes
1520
octets: Uint8Array;
1621

1722
constructor(octets: Uint8Array) {
@@ -117,7 +122,7 @@ export interface IpPayload {
117122
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
118123
// | Options | Padding |
119124
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
120-
export class IPv4Packet {
125+
export class IPv4Packet implements FramePayload {
121126
// IP version field
122127
// 4 bits
123128
readonly version = 4;
@@ -189,7 +194,7 @@ export class IPv4Packet {
189194
}: {
190195
withChecksum?: boolean;
191196
withPayload?: boolean;
192-
}) {
197+
} = {}) {
193198
let checksum = 0;
194199
if (withChecksum) {
195200
checksum = this.headerChecksum;
@@ -228,6 +233,10 @@ export class IPv4Packet {
228233
const result = computeIpChecksum(octets);
229234
return result === 0;
230235
}
236+
237+
type(): number {
238+
return IP_PROTOCOL_TYPE;
239+
}
231240
}
232241

233242
export function computeIpChecksum(octets: Uint8Array): number {

0 commit comments

Comments
 (0)