Skip to content

Commit 8661555

Browse files
authored
Merge pull request #14 from chipmk/feat/bridge
Bridge interface
2 parents 27b52aa + f804433 commit 8661555

18 files changed

+599
-142
lines changed

README.md

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
55
## Features
66

7-
- **Portable:** User-space network stack implemented on top of [`lwIP` + WASM](#why-lwip)
7+
- **Portable:** User-space network stack built on [`lwIP` + WASM](#why-lwip)
88
- **Tun/Tap:** L3 and L2 hooks using virtual [`TunInterface`](#tun-interface) and [`TapInterface`](#tap-interface)
9+
- **Bridge:** Create a virtual switch/LAN by [`bridging`](#bridge-interface) multiple interfaces together
910
- **TCP API:** Establish TCP connections over the virtual network stack using [clients](#connecttcp) and [servers](#listentcp)
1011
- **UDP API:** Send and receive UDP datagrams over the virtual network stack using [sockets](#openudp)
1112
- **Cross platform**: Built on web standard APIs (`ReadableStream`, `WritableStream`, etc)
@@ -152,19 +153,22 @@ for await (const connection of listener) {
152153

153154
For more info, see [`TcpListener`](#tcplistener).
154155

156+
The above example connects a single VM to the `NetworkStack`. If you wish to connect multiple VMs together on a shared LAN (ie. a virtual switch), you can create a [`BridgeInterface`](#bridge-interface) to join multiple tap interfaces together.
157+
155158
## Network interfaces
156159

157-
3 types of interfaces are available:
160+
4 types of interfaces are available:
158161

159162
- [Loopback](#loopback-interface): Loop packets back onto itself (ie. `localhost`)
160163
- [Tun](#tun-interface): Hook into IP packets (L3)
161164
- [Tap](#tap-interface): Hook into ethernet frames (L2)
165+
- [Bridge](#bridge-interface): Bridge multiple tap interfaces together to create a virtual switch
162166

163-
These interfaces are designed to resemble their counterparts in a traditional host network stack.
167+
These interfaces are designed to resemble their counterparts in a real network stack.
164168

165169
### Loopback interface
166170

167-
A loopback interface simply forwards packets back on to itself. It's akin to 127.0.0.1 (`localhost`) on a traditional network stack.
171+
A loopback interface simply forwards packets back on to itself. It's akin to 127.0.0.1 (`localhost`) on a typical network stack.
168172

169173
```ts
170174
const loopbackInterface = await stack.createLoopbackInterface({
@@ -193,8 +197,6 @@ const connection = await stack.connectTcp({
193197
});
194198
```
195199

196-
You can create as many loopback interfaces as you wish.
197-
198200
### Tun interface
199201

200202
A tun interface hooks into inbound and outbound IP packets (L3).
@@ -255,15 +257,12 @@ const connection = await stack.connectTcp({
255257
...
256258
```
257259

258-
You can create as many tun interfaces as you wish.
259-
260260
### Tap interface
261261

262262
A tap interface hooks into inbound and outbound ethernet frames (L2).
263263

264264
```ts
265265
const tapInterface = await stack.createTapInterface({
266-
mac: '01:23:45:67:89:ab',
267266
ip: '196.168.1.1/24',
268267
});
269268
```
@@ -326,11 +325,71 @@ const connection = await stack.connectTcp({
326325
...
327326
```
328327

329-
You can create as many tap interfaces as you wish.
328+
Note that `mac` and `ip` are optional parameters for `createTapInterface()`. If you don't provide a MAC address, a random one will be generated. If you don't provide an IP address, the interface will not respond to ARP requests or send ARP requests for unknown IP addresses. Typically you would only omit the IP address if you are using the tap interface as part of a [bridge](#bridge-interface).
329+
330+
### Bridge interface
331+
332+
A bridge interface bridges two or more tap interfaces together into a single logical interface with its own MAC and IP address. It operates at the ethernet level (L2) and will automatically forward frames between the interfaces based on the destination MAC address.
333+
334+
```ts
335+
const port1 = await stack.createTapInterface();
336+
const port2 = await stack.createTapInterface();
337+
338+
const bridge = await stack.createBridgeInterface({
339+
ports: [port1, port2],
340+
ip: '192.168.1.1/24',
341+
});
342+
```
343+
344+
A bridge is what you would use to connect multiple VMs together into a virtual LAN.
345+
346+
```ts
347+
import { createV86NetworkStream } from '@tcpip/v86';
348+
349+
// ...
350+
351+
const vm1 = new V86();
352+
const vm2 = new V86();
353+
const vm1Nic = createV86NetworkStream(vm1);
354+
const vm2Nic = createV86NetworkStream(vm2);
355+
356+
const port1 = await stack.createTapInterface();
357+
const port2 = await stack.createTapInterface();
358+
359+
// Connect port1 to vm1
360+
port1.readable.pipeTo(vm1Nic.writable);
361+
vm1Nic.readable.pipeTo(port1.writable);
362+
363+
// Connect port2 to vm2
364+
port2.readable.pipeTo(vm2Nic.writable);
365+
vm2Nic.readable.pipeTo(port2.writable);
366+
367+
// Bridge the two ports together
368+
const bridge = await stack.createBridgeInterface({
369+
ports: [port1, port2],
370+
ip: '192.168.1.1/24',
371+
});
372+
```
373+
374+
In the above example, `vm1` and `vm2` are attached together via a shared LAN. We treat the tcpip.js stack as the virtual router/switch where each VM connects to their own [tap interface](#tap-interface) (`port1` and `port2`) which are then bridged together. The bridge interface has its own MAC and IP address (`192.168.1.1`), representing the address of the virtual router. This follows the exact same bridging pattern that a physical router would in a real network.
375+
376+
Notice that we intentionally don't set IP addresses on the tap interfaces - they are only used to forward ethernet frames to/from the bridge. The bridge interface itself is where we set the IP address that the VMs can communicate with.
377+
378+
This allows you to, for example, host a TCP server on the router itself in order to communicate with the VMs from JavaScript. You would simply create a TCP server on the stack like so:
379+
380+
```ts
381+
const listener = await stack.listenTcp({
382+
port: 80,
383+
});
384+
```
385+
386+
The server would be accessible to any VM connected to the bridge via `192.168.1.1:80`. For more information on TCP, see the [TCP API](#tcp-api).
387+
388+
Note that `BridgeInterface` does not expose its own `readable` or `writable` stream - instead you would send and receive frames through each `TapInterface` port that is part of the bridge.
330389

331390
### Other interfaces
332391

333-
Looking for another type of interface? See [Future plans](#future-plans).
392+
Looking for another type of network interface? See [Future plans](#future-plans).
334393

335394
### Removing interfaces
336395

@@ -591,7 +650,6 @@ _Background:_ Vite optimizes dependencies during development to improve build ti
591650
- [ ] DNS API
592651
- [ ] mDNS API
593652
- [ ] Hosts file
594-
- [ ] Bridge interface
595653
- [ ] Experimental Wireguard interface
596654
- [ ] Node.js net polyfill
597655
- [ ] Deno net polyfill

packages/tcpip/src/bindings/base.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export abstract class Bindings<Imports, Exports> {
2020
return new UniquePointer(this.exports.malloc(size), this.exports.free);
2121
}
2222

23-
copyToMemory(data: Uint8Array) {
24-
const length = data.length;
23+
copyToMemory(data: ArrayBuffer) {
24+
const bytes = new Uint8Array(data);
25+
const length = bytes.length;
2526
const pointer = this.smartMalloc(length);
2627

2728
const memoryView = new Uint8Array(
@@ -30,7 +31,7 @@ export abstract class Bindings<Imports, Exports> {
3031
length
3132
);
3233

33-
memoryView.set(data);
34+
memoryView.set(bytes);
3435

3536
return pointer;
3637
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { serializeMacAddress, type MacAddress } from '../protocols/ethernet.js';
2+
import { serializeIPv4Cidr, type IPv4Cidr } from '../protocols/ipv4.js';
3+
import type { Pointer } from '../types.js';
4+
import { generateMacAddress } from '../util.js';
5+
import { Bindings } from './base.js';
6+
import { tapInterfaceHooks, type TapInterface } from './tap-interface.js';
7+
8+
type BridgeInterfaceHandle = Pointer;
9+
10+
export type BridgeImports = {};
11+
12+
export type BridgeExports = {
13+
create_bridge_interface(
14+
macAddress: Pointer,
15+
ipAddress: Pointer,
16+
netmask: Pointer,
17+
ports: Pointer,
18+
ports_length: number
19+
): BridgeInterfaceHandle;
20+
remove_bridge_interface(handle: BridgeInterfaceHandle): void;
21+
};
22+
23+
export class BridgeBindings extends Bindings<BridgeImports, BridgeExports> {
24+
interfaces = new Map<BridgeInterfaceHandle, BridgeInterface>();
25+
26+
imports = {};
27+
28+
async create(options: BridgeInterfaceOptions) {
29+
const macAddress = options.mac
30+
? serializeMacAddress(options.mac)
31+
: generateMacAddress();
32+
33+
const { ipAddress, netmask } = options.ip
34+
? serializeIPv4Cidr(options.ip)
35+
: {};
36+
37+
using macAddressPtr = this.copyToMemory(macAddress);
38+
using ipAddressPtr = ipAddress ? this.copyToMemory(ipAddress) : undefined;
39+
using netmaskPtr = netmask ? this.copyToMemory(netmask) : undefined;
40+
const portHandles = new Uint32Array(
41+
options.ports.map((port) =>
42+
Number(tapInterfaceHooks.getOuter(port).handle)
43+
)
44+
);
45+
46+
using portHandlesPtr = this.copyToMemory(portHandles.buffer);
47+
48+
const handle = this.exports.create_bridge_interface(
49+
macAddressPtr,
50+
ipAddressPtr ?? 0,
51+
netmaskPtr ?? 0,
52+
portHandlesPtr,
53+
options.ports.length
54+
);
55+
56+
const bridgeInterface = new BridgeInterface();
57+
this.interfaces.set(handle, bridgeInterface);
58+
59+
return bridgeInterface;
60+
}
61+
62+
async remove(bridgeInterface: BridgeInterface) {
63+
for (const [handle, loopback] of this.interfaces.entries()) {
64+
if (loopback === bridgeInterface) {
65+
this.exports.remove_bridge_interface(handle);
66+
this.interfaces.delete(handle);
67+
return;
68+
}
69+
}
70+
}
71+
}
72+
73+
export type BridgeInterfaceOptions = {
74+
ports: TapInterface[];
75+
mac?: MacAddress;
76+
ip?: IPv4Cidr;
77+
};
78+
79+
export class BridgeInterface {}

packages/tcpip/src/bindings/loopback-interface.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ export class LoopbackBindings extends Bindings<
3030
};
3131

3232
async create(options: LoopbackInterfaceOptions) {
33-
const { ipAddress, netmask } = serializeIPv4Cidr(options.ip);
33+
const { ipAddress, netmask } = options.ip
34+
? serializeIPv4Cidr(options.ip)
35+
: {};
3436

35-
using ipAddressPtr = this.copyToMemory(ipAddress);
36-
using netmaskPtr = this.copyToMemory(netmask);
37+
using ipAddressPtr = ipAddress ? this.copyToMemory(ipAddress) : undefined;
38+
using netmaskPtr = netmask ? this.copyToMemory(netmask) : undefined;
3739

3840
const handle = this.exports.create_loopback_interface(
39-
ipAddressPtr,
40-
netmaskPtr
41+
ipAddressPtr ?? 0,
42+
netmaskPtr ?? 0
4143
);
4244

4345
const loopbackInterface = this.interfaces.get(handle);
@@ -61,6 +63,6 @@ export class LoopbackBindings extends Bindings<
6163
}
6264

6365
export type LoopbackInterfaceOptions = {
64-
ip: IPv4Cidr;
66+
ip?: IPv4Cidr;
6567
};
6668
export class LoopbackInterface {}

packages/tcpip/src/bindings/tap-interface.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import { Bindings } from './base.js';
1+
import { LwipError } from '../lwip/errors.js';
22
import { serializeMacAddress, type MacAddress } from '../protocols/ethernet.js';
33
import { serializeIPv4Cidr, type IPv4Cidr } from '../protocols/ipv4.js';
44
import type { Pointer } from '../types.js';
55
import {
66
ExtendedReadableStream,
77
fromReadable,
8+
generateMacAddress,
89
Hooks,
910
nextMicrotask,
1011
} from '../util.js';
12+
import { Bindings } from './base.js';
1113

1214
type TapInterfaceHandle = Pointer;
1315

1416
type TapInterfaceOuterHooks = {
17+
handle: TapInterfaceHandle;
1518
sendFrame(frame: Uint8Array): void;
1619
};
1720

1821
type TapInterfaceInnerHooks = {
1922
receiveFrame(frame: Uint8Array): void;
2023
};
2124

22-
const tapInterfaceHooks = new Hooks<
25+
export const tapInterfaceHooks = new Hooks<
2326
TapInterface,
2427
TapInterfaceOuterHooks,
2528
TapInterfaceInnerHooks
@@ -45,7 +48,9 @@ export type TapExports = {
4548
handle: TapInterfaceHandle,
4649
frame: Pointer,
4750
length: number
48-
): void;
51+
): number;
52+
enable_tap_interface(handle: TapInterfaceHandle): void;
53+
disable_tap_interface(handle: TapInterfaceHandle): void;
4954
};
5055

5156
export class TapBindings extends Bindings<TapImports, TapExports> {
@@ -56,9 +61,18 @@ export class TapBindings extends Bindings<TapImports, TapExports> {
5661
const tapInterface = new TapInterface();
5762

5863
tapInterfaceHooks.setOuter(tapInterface, {
64+
handle,
5965
sendFrame: (frame) => {
6066
const framePtr = this.copyToMemory(frame);
61-
this.exports.send_tap_interface(handle, framePtr, frame.length);
67+
const result = this.exports.send_tap_interface(
68+
handle,
69+
framePtr,
70+
frame.length
71+
);
72+
73+
if (result !== LwipError.ERR_OK) {
74+
throw new Error(`failed to send frame: ${result}`);
75+
}
6276
},
6377
});
6478

@@ -89,17 +103,22 @@ export class TapBindings extends Bindings<TapImports, TapExports> {
89103
};
90104

91105
async create(options: TapInterfaceOptions) {
92-
const macAddress = serializeMacAddress(options.mac);
93-
const { ipAddress, netmask } = serializeIPv4Cidr(options.ip);
106+
const macAddress = options.mac
107+
? serializeMacAddress(options.mac)
108+
: generateMacAddress();
109+
110+
const { ipAddress, netmask } = options.ip
111+
? serializeIPv4Cidr(options.ip)
112+
: {};
94113

95114
using macAddressPtr = this.copyToMemory(macAddress);
96-
using ipAddressPtr = this.copyToMemory(ipAddress);
97-
using netmaskPtr = this.copyToMemory(netmask);
115+
using ipAddressPtr = ipAddress ? this.copyToMemory(ipAddress) : undefined;
116+
using netmaskPtr = netmask ? this.copyToMemory(netmask) : undefined;
98117

99118
const handle = this.exports.create_tap_interface(
100119
macAddressPtr,
101-
ipAddressPtr,
102-
netmaskPtr
120+
ipAddressPtr ?? 0,
121+
netmaskPtr ?? 0
103122
);
104123

105124
const tapInterface = this.interfaces.get(handle);
@@ -123,8 +142,8 @@ export class TapBindings extends Bindings<TapImports, TapExports> {
123142
}
124143

125144
export type TapInterfaceOptions = {
126-
mac: MacAddress;
127-
ip: IPv4Cidr;
145+
mac?: MacAddress;
146+
ip?: IPv4Cidr;
128147
};
129148

130149
export class TapInterface {
@@ -147,7 +166,7 @@ export class TapInterface {
147166
throw new Error('readable stream not initialized');
148167
}
149168

150-
this.#readableController?.enqueue(frame);
169+
this.#readableController.enqueue(frame);
151170
},
152171
});
153172

@@ -164,7 +183,11 @@ export class TapInterface {
164183

165184
this.writable = new WritableStream({
166185
write: (packet) => {
167-
tapInterfaceHooks.getOuter(this).sendFrame(packet);
186+
try {
187+
tapInterfaceHooks.getOuter(this).sendFrame(packet);
188+
} catch (err) {
189+
console.error('tap interface send failed', err);
190+
}
168191
},
169192
});
170193
}

0 commit comments

Comments
 (0)