Skip to content

feat: expose mac, ip, and netmask from interfaces #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ const connection = await stack.connectTcp({
});
```

The interface's IP address and subnet mask can be retrieved using the `ip` and `netmask` properties:

```ts
const loopbackInterface = await stack.createLoopbackInterface({
ip: '127.0.0.1/8',
});

console.log(loopbackInterface.ip); // 127.0.0.1
console.log(loopbackInterface.netmask); // 255.0.0.0
```

### Tun interface

A tun interface hooks into inbound and outbound IP packets (L3).
Expand Down Expand Up @@ -257,6 +268,17 @@ const connection = await stack.connectTcp({
...
```

The interface's IP address and subnet mask can be retrieved using the `ip` and `netmask` properties:

```ts
const tunInterface = await stack.createTunInterface({
ip: '192.168.1.1/24',
});

console.log(tunInterface.ip); // 192.168.1.1
console.log(tunInterface.netmask); // 255.255.255.0
```

### Tap interface

A tap interface hooks into inbound and outbound ethernet frames (L2).
Expand Down Expand Up @@ -327,6 +349,21 @@ const connection = await stack.connectTcp({

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).

The interface's MAC address, IP address, and subnet mask can be retrieved using the `mac`, `ip` and `netmask` properties:

```ts
const tapInterface = await stack.createTapInterface({
mac: '02:00:00:00:00:01',
ip: '196.168.1.1/24',
});

console.log(tapInterface.mac); // 02:00:00:00:00:01
console.log(tapInterface.ip); // 192.168.1.1
console.log(tapInterface.netmask); // 255.255.255.0
```

This is particularly useful when you let the tap interface generate its own random MAC address but need to determine what it is.

### Bridge interface

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.
Expand Down Expand Up @@ -385,6 +422,24 @@ const listener = await stack.listenTcp({

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).

Just like a `TapInterface`, specifying `mac` and `ip` addresses are optional for a `BridgeInterface`. If you don't provide a MAC address, a random one will be generated. If you don't provide an IP address, the bridge itself 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 bridge as a pure switch and don't need to communicate with virtual devices from JavaScript.

The interface's MAC address, IP address, and subnet mask can be retrieved using the `mac`, `ip` and `netmask` properties:

```ts
const bridgeInterface = await stack.createBridgeInterface({
ports: [port1, port2],
mac: '02:00:00:00:00:01',
ip: '192.168.1.1/24',
});

console.log(bridgeInterface.mac); // 02:00:00:00:00:01
console.log(bridgeInterface.ip); // 192.168.1.1
console.log(bridgeInterface.netmask); // 255.255.255.0
```

This is particularly useful when you let the bridge generate its own random MAC address but need to determine what it is.

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.

### Other interfaces
Expand Down
49 changes: 43 additions & 6 deletions packages/tcpip/src/bindings/base.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,47 @@
import type { SysExports, WasiExports } from '../types.js';
import type { Pointer, SysExports, WasiExports } from '../types.js';
import { UniquePointer } from '../util.js';

export type CommonExports = {
get_interface_mac_address(handle: Pointer): Pointer;
get_interface_ip4_address(handle: Pointer): Pointer;
get_interface_ip4_netmask(handle: Pointer): Pointer;
};

export abstract class Bindings<Imports, Exports> {
#exports?: Exports & WasiExports & SysExports;
#exports?: Exports & CommonExports & WasiExports & SysExports;

abstract imports: Imports;
get exports(): Exports & WasiExports & SysExports {

get exports(): Exports & CommonExports & WasiExports & SysExports {
if (!this.#exports) {
throw new Error('exports were not registered');
}
return this.#exports;
}

register(exports: Exports & WasiExports & SysExports) {
/**
* Register the exports object from the wasm module.
*/
register(exports: Exports & CommonExports & WasiExports & SysExports) {
this.#exports = exports;
}

/**
* Allocates a region of wasm memory and returns a `UniquePointer` to the start.
*
* `UniquePointer` will automatically free the memory when it is disposed.
* It is intended to be used with the `using` statement which will automatically
* dispose of the pointer when the current scope ends.
*/
smartMalloc(size: number) {
return new UniquePointer(this.exports.malloc(size), this.exports.free);
}

/**
* Copies a Uint8Array to a newly allocated region of wasm memory.
*
* @returns A pointer to the start of the copied data.
*/
copyToMemory(data: ArrayBuffer) {
const bytes = new Uint8Array(data);
const length = bytes.length;
Expand All @@ -36,8 +58,23 @@ export abstract class Bindings<Imports, Exports> {
return pointer;
}

copyFromMemory(ptr: number, length: number): Uint8Array {
const buffer = this.exports.memory.buffer.slice(ptr, ptr + length);
/**
* Copies a region of wasm memory to a new Uint8Array.
*
* @returns A new Uint8Array containing the copied data.
*/
copyFromMemory(ptr: Pointer | number, length: number): Uint8Array {
const buffer = this.exports.memory.buffer.slice(
Number(ptr),
Number(ptr) + length
);
return new Uint8Array(buffer);
}

/**
* Creates a Uint8Array view over a region of wasm memory.
*/
viewFromMemory(ptr: Pointer | number, length: number): Uint8Array {
return new Uint8Array(this.exports.memory.buffer, Number(ptr), length);
}
}
63 changes: 62 additions & 1 deletion packages/tcpip/src/bindings/bridge-interface.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import {
parseIPv4Address,
parseMacAddress,
serializeIPv4Cidr,
serializeMacAddress,
type IPv4Address,
type IPv4Cidr,
type MacAddress,
} from '@tcpip/wire';
import type { Pointer } from '../types.js';
import { generateMacAddress } from '../util.js';
import { generateMacAddress, Hooks } from '../util.js';
import { Bindings } from './base.js';
import { tapInterfaceHooks, type TapInterface } from './tap-interface.js';

type BridgeInterfaceHandle = Pointer;

type BridgeInterfaceOuterHooks = {
handle: BridgeInterfaceHandle;
getMacAddress(): MacAddress;
getIPv4Address(): IPv4Address | undefined;
getIPv4Netmask(): IPv4Address | undefined;
};

type BridgeInterfaceInnerHooks = {};

export const bridgeInterfaceHooks = new Hooks<
BridgeInterface,
BridgeInterfaceOuterHooks,
BridgeInterfaceInnerHooks
>();

export type BridgeImports = {};

export type BridgeExports = {
Expand Down Expand Up @@ -58,6 +76,37 @@ export class BridgeBindings extends Bindings<BridgeImports, BridgeExports> {
);

const bridgeInterface = new VirtualBridgeInterface();

bridgeInterfaceHooks.setOuter(bridgeInterface, {
handle,
getMacAddress: () => {
const macPtr = this.exports.get_interface_mac_address(handle);

const macBytes = this.viewFromMemory(macPtr, 6);
return parseMacAddress(macBytes);
},
getIPv4Address: () => {
const ipPtr = this.exports.get_interface_ip4_address(handle);

if (ipPtr === 0) {
return;
}

const ipBytes = this.viewFromMemory(ipPtr, 4);
return parseIPv4Address(ipBytes);
},
getIPv4Netmask: () => {
const netmaskPtr = this.exports.get_interface_ip4_netmask(handle);

if (netmaskPtr === 0) {
return;
}

const netmaskBytes = this.viewFromMemory(netmaskPtr, 4);
return parseIPv4Address(netmaskBytes);
},
});

this.interfaces.set(handle, bridgeInterface);

return bridgeInterface;
Expand All @@ -82,8 +131,20 @@ export type BridgeInterfaceOptions = {

export type BridgeInterface = {
readonly type: 'bridge';
readonly mac: MacAddress;
readonly ip?: IPv4Address;
readonly netmask?: IPv4Address;
};

export class VirtualBridgeInterface implements BridgeInterface {
readonly type = 'bridge';
get mac(): MacAddress {
return bridgeInterfaceHooks.getOuter(this).getMacAddress();
}
get ip(): IPv4Address | undefined {
return bridgeInterfaceHooks.getOuter(this).getIPv4Address();
}
get netmask(): IPv4Address | undefined {
return bridgeInterfaceHooks.getOuter(this).getIPv4Netmask();
}
}
55 changes: 54 additions & 1 deletion packages/tcpip/src/bindings/loopback-interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { serializeIPv4Cidr, type IPv4Cidr } from '@tcpip/wire';
import {
parseIPv4Address,
serializeIPv4Cidr,
type IPv4Address,
type IPv4Cidr,
} from '@tcpip/wire';
import type { Pointer } from '../types.js';
import { Hooks } from '../util.js';
import { Bindings } from './base.js';

type LoopbackInterfaceHandle = Pointer;

type LoopbackInterfaceOuterHooks = {
handle: LoopbackInterfaceHandle;
getIPv4Address(): IPv4Address | undefined;
getIPv4Netmask(): IPv4Address | undefined;
};

type LoopbackInterfaceInnerHooks = {};

export const loopbackInterfaceHooks = new Hooks<
LoopbackInterface,
LoopbackInterfaceOuterHooks,
LoopbackInterfaceInnerHooks
>();

export type LoopbackImports = {
register_loopback_interface(handle: LoopbackInterfaceHandle): void;
};
Expand All @@ -25,6 +45,31 @@ export class LoopbackBindings extends Bindings<
imports = {
register_loopback_interface: (handle: LoopbackInterfaceHandle) => {
const loopbackInterface = new VirtualLoopbackInterface();

loopbackInterfaceHooks.setOuter(loopbackInterface, {
handle,
getIPv4Address: () => {
const ipPtr = this.exports.get_interface_ip4_address(handle);

if (ipPtr === 0) {
return;
}

const ipBytes = this.viewFromMemory(ipPtr, 4);
return parseIPv4Address(ipBytes);
},
getIPv4Netmask: () => {
const netmaskPtr = this.exports.get_interface_ip4_netmask(handle);

if (netmaskPtr === 0) {
return;
}

const netmaskBytes = this.viewFromMemory(netmaskPtr, 4);
return parseIPv4Address(netmaskBytes);
},
});

this.interfaces.set(handle, loopbackInterface);
},
};
Expand Down Expand Up @@ -68,8 +113,16 @@ export type LoopbackInterfaceOptions = {

export type LoopbackInterface = {
readonly type: 'loopback';
readonly ip?: IPv4Address;
readonly netmask?: IPv4Address;
};

export class VirtualLoopbackInterface implements LoopbackInterface {
readonly type = 'loopback';
get ip(): IPv4Address | undefined {
return loopbackInterfaceHooks.getOuter(this).getIPv4Address();
}
get netmask(): IPv4Address | undefined {
return loopbackInterfaceHooks.getOuter(this).getIPv4Netmask();
}
}
Loading