tcpip.js networking examples / docs
Hey 👋 amazing tool.
So far the networking examples focus on connecting to external networks or proxies (fetch proxy, WISP). I'm interested in local networking - communicating directly between JS and v86. This would open the door to use cases like running a server-side program (like Postgres, nginx, etc) inside v86 and communicate with it from JS.
I built tcpip.js to solve this. It's a virtual network stack that can run in the browser. It's built on top of lwIP compiled to WASM with bindings to hook into each layer of the network stack (L2, L3, L4).
Example use case
Set up plumbing that pipes ethernet packets between v86 and the virtual network stack.
import { createStack } from 'tcpip';
import { createV86NetworkStream } from '@tcpip/v86';
const stack = await createStack();
// Tap interfaces hook into ethernet (L2) frames
const tapInterface = await stack.createTapInterface({
mac: '01:23:45:67:89:ab',
ip: '192.168.1.1/24',
});
const emulator = new V86();
// Adds net0 listener and exposes as readable/writable streams
const vmNic = createV86NetworkStream(emulator);
// Forward frames between the tap interface and the VM's NIC
tapInterface.readable.pipeTo(vmNic.writable);
vmNic.readable.pipeTo(tapInterface.writable);
Now we can establish a TCP connection to the emulator from JS (assuming VM has IP 192.168.1.2 and a server listening on port 80):
const connection = await stack.connectTcp({
host: '192.168.1.2',
port: 80,
});
// connection.writable is a standard WritableStream
const writer = connection.writable.getWriter();
// Send data
await writer.write(new TextEncoder().encode('Hello, world!'));
await writer.close();
// Listen for incoming data
for await (const chunk of connection) {
console.log(new TextDecoder().decode(chunk));
}
Or we can create a TCP server and listen for inbound connections:
const listener = await stack.listenTcp({
port: 80,
});
// TcpListener is an async iterable that yields TcpConnections
for await (const connection of listener) {
const writer = connection.writable.getWriter();
// Send data
await writer.write(new TextEncoder().encode('Hello, world!'));
await writer.close();
// Listen for incoming data
for await (const chunk of connection) {
console.log(new TextDecoder().decode(chunk));
}
}
Other use cases
I'm planning to add bridge interface support soon, which would allow you to use tcpip.js as a switch/router. This can be useful for connecting more than 2 VMs together in a LAN.
I was wondering if you are interested in adding docs / examples for this use case? If so happy to put a PR together that adds some demos and networking docs.
Nice work! Would you like to share it in this discussion: https://github.com/copy/v86/discussions/1048?
Super cool stuff. Im sure this is way more comprehensive then what we've done in 1300 lines of javascript code. I also thought about maybe using the gvisor stuff similar to container2wasm, but it looked too heavy weight. lwip looks like it could be pretty nice, especially as it also seems to have a http server and TLS. Its a bit tough to make a sweet example without DHCP, DNS, and maybe even HTTP working in tcpip.js, as thats what most people are actually going to need to make their VMs work.
@basicer Good timing. I'm just finishing up UDP, DHCP, and DNS support for tcpip.js (along with adapters for container2wasm to do the same thing there). I'll post an update when it's ready. HTTP is partway done - I'll get back to that after the above is finished.
PS. fun fact tcpip.js used to be built on Go + gvisor but I rewrote on lwIP for the same reason - wasm output was ~5MB vs under 100KB now.
@gregnr Any updates?
@basicer UDP is fully supported and documented here: https://github.com/chipmk/tcpip.js#udp-api
DNS is also supported now via @tcpip/dns and documented here: https://github.com/chipmk/tcpip.js#dns
tcpip.js also includes an embedded DNS resolver now that works with the TCP / UDP APIs:
import { createStack } from 'tcpip';
const stack = await createStack();
// TCP
const connection = await stack.connectTcp({
host: 'mydomain.internal',
port: 80,
});
// UDP
const udpSocket = await stack.openUdp();
const writer = udpSocket.writable.getWriter();
await writer.write({
host: 'mydomain.internal',
port: 1234,
data: new TextEncoder().encode('Hello, world!'),
});
Then you can run your own DNS server like this:
import { createDns } from '@tcpip/dns';
const { lookup, serve } = await createDns(stack);
await serve({
request: async ({ name, type }) => {
if (name === 'mydomain.internal' && type === 'A') {
return {
type,
ip: '192.168.1.2',
ttl: 300,
};
}
},
});
Since most people run tcpip.js as an isolated network (in-browser, not connected to the internet), lookup() and the embedded resolver point to 127.0.0.1:53 on the stack by default and you are expected to run your own DNS server there. But nothing stops you from pointing to a DNS server in a VM instead, or proxying DNS requests to the internet via a websocket or DNS-over-HTTP proxy.
I've also added a BridgeInterface that allows you to use tcpip.js as a router/switch. So you can now run multiple v86 VMs and connect them together on a shared LAN via this bridge interface, using the tcpip.js stack as the router. Technically v86 already supports communication between multiple VMs today, but this uses BroadcastChannel to send all L2 packets to all VMs vs. using forwarding tables to send traffic to only the appropriate VM. The bridge also allows you to communicate with all VMs using JavaScript via TCP/UDP APIs.
A POC DHCP server is available in @tcpip/dhcp but it's not documented yet. Once I test this some more I'll add documentation.
A common use case would be to integrate the DHCP server that auto-assign IPs to VMs on a bridge with the DNS server that gives them a hostname for service discovery. I'm working on a higher level library on top of v86+tcpip+BridgeInterface+@tcpip/dns+@tcpip/dhcp that implements the above in a docker compose-style API.