wisp_network adapter (#1097)

This commit is contained in:
Neal Shah 2024-08-27 15:41:59 -04:00 committed by GitHub
parent 23d279c26d
commit 67bfed335d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 687 additions and 56 deletions

View file

@ -87,7 +87,7 @@ CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js flopp
LIB_FILES=9p.js filesystem.js jor1k.js marshall.js utf8.js LIB_FILES=9p.js filesystem.js jor1k.js marshall.js utf8.js
BROWSER_FILES=screen.js keyboard.js mouse.js speaker.js serial.js \ BROWSER_FILES=screen.js keyboard.js mouse.js speaker.js serial.js \
network.js starter.js worker_bus.js dummy_screen.js \ network.js starter.js worker_bus.js dummy_screen.js \
fake_network.js fetch_network.js print_stats.js filestorage.js fake_network.js wisp_network.js fetch_network.js print_stats.js filestorage.js
RUST_FILES=$(shell find src/rust/ -name '*.rs') \ RUST_FILES=$(shell find src/rust/ -name '*.rs') \
src/rust/gen/interpreter.rs src/rust/gen/interpreter0f.rs \ src/rust/gen/interpreter.rs src/rust/gen/interpreter0f.rs \
@ -306,6 +306,7 @@ devices-test: all-debug
./tests/devices/virtio_9p.js ./tests/devices/virtio_9p.js
./tests/devices/virtio_console.js ./tests/devices/virtio_console.js
./tests/devices/fetch_network.js ./tests/devices/fetch_network.js
./tests/devices/wisp_network.js
rust-test: $(RUST_FILES) rust-test: $(RUST_FILES)
env RUSTFLAGS="-D warnings" RUST_BACKTRACE=full RUST_TEST_THREADS=1 cargo test -- --nocapture env RUSTFLAGS="-D warnings" RUST_BACKTRACE=full RUST_TEST_THREADS=1 cargo test -- --nocapture

View file

@ -225,33 +225,7 @@ function handle_fake_networking(data, adapter) {
} }
if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) {
let packet_subnet = iptolong(packet.arp.tpa) & 0xFFFFFF00; arp_whohas(packet, adapter);
let router_subnet = iptolong(adapter.router_ip) & 0xFFFFFF00;
if(!adapter.masquerade) {
if(packet_subnet !== router_subnet) {
return;
}
}
if(packet_subnet === router_subnet) {
// Ignore the DHCP client area
if(packet.arp.tpa[3] > 99) return;
}
// Reply to ARP Whohas
let reply = {};
reply.eth = { ethertype: ETHERTYPE_ARP, src: adapter.router_mac, dest: packet.eth.src };
reply.arp = {
htype: 1,
ptype: ETHERTYPE_IPV4,
oper: 2,
sha: adapter.router_mac,
spa: packet.arp.tpa,
tha: packet.eth.src,
tpa: packet.arp.spa
};
adapter.receive(make_packet(reply));
} }
if(packet.dns) { if(packet.dns) {
@ -264,20 +238,7 @@ function handle_fake_networking(data, adapter) {
// ICMP Ping // ICMP Ping
if(packet.icmp && packet.icmp.type === 8) { if(packet.icmp && packet.icmp.type === 8) {
let reply = {}; handle_fake_ping(packet, adapter);
reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_ICMP,
src: adapter.router_ip,
dest: packet.ipv4.src,
};
reply.icmp = {
type: 0,
code: packet.icmp.code,
data: packet.icmp.data
};
adapter.receive(make_packet(reply));
return;
} }
if(packet.dhcp) { if(packet.dhcp) {
@ -285,20 +246,7 @@ function handle_fake_networking(data, adapter) {
} }
if(packet.udp && packet.udp.dport === 8) { if(packet.udp && packet.udp.dport === 8) {
// UDP Echo Server handle_udp_echo(packet, adapter);
let reply = {};
reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_UDP,
src: packet.ipv4.dest,
dest: packet.ipv4.src,
};
reply.udp = {
sport: packet.udp.dport,
dport: packet.udp.sport,
data: new TextEncoder().encode(packet.udp.data_s)
};
adapter.receive(make_packet(reply));
} }
} }
@ -528,6 +476,7 @@ function parse_udp(data, o) {
dport: view.getUint16(2), dport: view.getUint16(2),
len: view.getUint16(4), len: view.getUint16(4),
checksum: view.getUint16(6), checksum: view.getUint16(6),
data: data.subarray(8),
data_s: new TextDecoder().decode(data.subarray(8)) data_s: new TextDecoder().decode(data.subarray(8))
}; };
@ -1133,3 +1082,67 @@ TCPConnection.prototype.pump = function() {
this.net.receive(make_packet(reply)); this.net.receive(make_packet(reply));
} }
}; };
function arp_whohas(packet, adapter) {
let packet_subnet = iptolong(packet.arp.tpa) & 0xFFFFFF00;
let router_subnet = iptolong(adapter.router_ip) & 0xFFFFFF00;
if(!adapter.masquerade) {
if(packet_subnet !== router_subnet) {
return;
}
}
if(packet_subnet === router_subnet) {
// Ignore the DHCP client area
if(packet.arp.tpa[3] > 99) return;
}
// Reply to ARP Whohas
let reply = {};
reply.eth = { ethertype: ETHERTYPE_ARP, src: adapter.router_mac, dest: packet.eth.src };
reply.arp = {
htype: 1,
ptype: ETHERTYPE_IPV4,
oper: 2,
sha: adapter.router_mac,
spa: packet.arp.tpa,
tha: packet.eth.src,
tpa: packet.arp.spa
};
adapter.receive(make_packet(reply));
}
function handle_fake_ping(packet, adapter) {
let reply = {};
reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_ICMP,
src: adapter.router_ip,
dest: packet.ipv4.src,
};
reply.icmp = {
type: 0,
code: packet.icmp.code,
data: packet.icmp.data
};
adapter.receive(make_packet(reply));
}
function handle_udp_echo(packet, adapter) {
// UDP Echo Server
let reply = {};
reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_UDP,
src: packet.ipv4.dest,
dest: packet.ipv4.src,
};
reply.udp = {
sport: packet.udp.dport,
dport: packet.udp.sport,
data: new TextEncoder().encode(packet.udp.data_s)
};
adapter.receive(make_packet(reply));
}

View file

@ -297,6 +297,9 @@ V86.prototype.continue_init = async function(emulator, options)
{ {
this.network_adapter = new FetchNetworkAdapter(this.bus); this.network_adapter = new FetchNetworkAdapter(this.bus);
} }
else if(options.network_relay_url.startsWith("wisp://") || options.network_relay_url.startsWith("wisps://")) {
this.network_adapter = new WispNetworkAdapter(options.network_relay_url, this.bus, options);
}
else else
{ {
this.network_adapter = new NetworkAdapter(options.network_relay_url, this.bus); this.network_adapter = new NetworkAdapter(options.network_relay_url, this.bus);

302
src/browser/wisp_network.js Normal file
View file

@ -0,0 +1,302 @@
"use strict";
const DEFAULT_DOH_SERVER = "cloudflare-dns.com";
/**
* @constructor
*
* @param {BusConnector} bus
* @param {*=} config
*/
function WispNetworkAdapter(wisp_url, bus, config)
{
this.register_ws(wisp_url);
this.last_stream = 1;
this.connections = {0: {congestion: 0}};
this.congested_buffer = [];
config = config || {};
this.bus = bus;
this.id = config.id || 0;
this.router_mac = new Uint8Array((config.router_mac || "52:54:0:1:2:3").split(":").map(function(x) { return parseInt(x, 16); }));
this.router_ip = new Uint8Array((config.router_ip || "192.168.86.1").split(".").map(function(x) { return parseInt(x, 10); }));
this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); }));
this.masquerade = config.masquerade === undefined || !!config.masquerade;
this.vm_mac = new Uint8Array(6);
this.doh_server = config.doh_server || DEFAULT_DOH_SERVER;
this.tcp_conn = {};
this.bus.register("net" + this.id + "-mac", function(mac) {
this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); }));
}, this);
this.bus.register("net" + this.id + "-send", function(data)
{
this.send(data);
}, this);
}
WispNetworkAdapter.prototype.register_ws = function(wisp_url) {
this.wispws = new WebSocket(wisp_url.replace("wisp://", "ws://").replace("wisps://", "wss://"));
this.wispws.binaryType = "arraybuffer";
this.wispws.onmessage = (event) => {
this.process_incoming_wisp_frame(new Uint8Array(event.data));
};
this.wispws.onclose = () => {
setTimeout(() => {
this.register_ws(wisp_url);
}, 10000); // wait 10s before reconnecting
};
};
WispNetworkAdapter.prototype.send_packet = function(data, type, stream_id) {
if(this.connections[stream_id].congestion > 0) {
if(type === "DATA") {
this.connections[stream_id].congestion--;
}
this.wispws.send(data);
} else {
this.connections[stream_id].congested = true;
this.congested_buffer.push({data: data, type: type});
}
};
WispNetworkAdapter.prototype.process_incoming_wisp_frame = function(frame) {
const view = new DataView(frame.buffer);
const stream_id = view.getUint32(1, true);
switch(frame[0]) {
case 1: // CONNECT
// The server should never send this actually
dbg_log("Server sent client-only packet CONNECT", LOG_NET);
break;
case 2: // DATA
if(this.connections[stream_id])
this.connections[stream_id].data_callback(frame.slice(5));
else
throw new Error("Got a DATA packet but stream not registered. ID: " + stream_id);
break;
case 3: // CONTINUE
if(this.connections[stream_id]) {
this.connections[stream_id].congestion = view.getUint32(5, true);
}
if(this.connections[stream_id].congested) {
for(const packet of this.congested_buffer) {
this.send_packet(packet.data, packet.type, stream_id);
}
this.connections[stream_id].congested = false;
}
break;
case 4: // CLOSE
if(this.connections[stream_id])
this.connections[stream_id].close_callback(view.getUint8(5));
delete this.connections[stream_id];
break;
case 5: // PROTOEXT
dbg_log("got a wisp V2 upgrade request, ignoring", LOG_NET);
// Not responding, this is wisp v1 client not wisp v2;
break;
default:
dbg_log("Wisp server returned unknown packet: " + frame[0], LOG_NET);
}
};
// FrameObj will be the following
// FrameObj.stream_id (number)
//
// FrameObj.type -- CONNECT
// FrameObj.hostname (string)
// FrameObj.port (number)
// FrameObj.data_callback (function (Uint8Array))
// FrameObj.close_callback (function (number)) OPTIONAL
//
//
// FrameObj.type -- DATA
// FrameObj.data (Uint8Array)
//
// FrameObj.type -- CLOSE
// FrameObj.reason (number)
//
//
WispNetworkAdapter.prototype.send_wisp_frame = function(frame_obj) {
let full_packet;
let view;
switch(frame_obj.type) {
case "CONNECT":
const hostname_buffer = new TextEncoder().encode(frame_obj.hostname);
full_packet = new Uint8Array(5 + 1 + 2 + hostname_buffer.length);
view = new DataView(full_packet.buffer);
view.setUint8(0, 0x01); // TYPE
view.setUint32(1, frame_obj.stream_id, true); // Stream ID
view.setUint8(5, 0x01); // TCP
view.setUint16(6, frame_obj.port, true); // PORT
full_packet.set(hostname_buffer, 8); // hostname
// Setting callbacks
this.connections[frame_obj.stream_id] = {
data_callback: frame_obj.data_callback,
close_callback: frame_obj.close_callback,
congestion: this.connections[0].congestion
};
break;
case "DATA":
full_packet = new Uint8Array(5 + frame_obj.data.length);
view = new DataView(full_packet.buffer);
view.setUint8(0, 0x02); // TYPE
view.setUint32(1, frame_obj.stream_id, true); // Stream ID
full_packet.set(frame_obj.data, 5); // Actual data
break;
case "CLOSE":
full_packet = new Uint8Array(5 + 1);
view = new DataView(full_packet.buffer);
view.setUint8(0, 0x04); // TYPE
view.setUint32(1, frame_obj.stream_id, true); // Stream ID
view.setUint8(5, frame_obj.reason); // Packet size
break;
default:
dbg_log("Client tried to send unknown packet: " + frame_obj.type, LOG_NET);
}
this.send_packet(full_packet, frame_obj.type, frame_obj.stream_id);
};
WispNetworkAdapter.prototype.destroy = function()
{
if(this.wispws) {
this.wispws.onmessage = null;
this.wispws.onclose = null;
this.wispws.close();
this.wispws = null;
}
};
/**
* @param {Uint8Array} data
*/
WispNetworkAdapter.prototype.send = function(data)
{
let packet = {};
parse_eth(data, packet);
if(packet.tcp) {
let reply = {};
reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_TCP,
src: packet.ipv4.dest,
dest: packet.ipv4.src
};
let tuple = [
packet.ipv4.src.join("."),
packet.tcp.sport,
packet.ipv4.dest.join("."),
packet.tcp.dport
].join(":");
if(packet.tcp.syn) {
if(this.tcp_conn[tuple]) {
dbg_log("SYN to already opened port", LOG_FETCH);
}
const tcp_conn = new TCPConnection();
tcp_conn.state = TCP_STATE_SYN_RECEIVED;
tcp_conn.net = this;
tcp_conn.tuple = tuple;
tcp_conn.stream_id = this.last_stream++;
this.tcp_conn[tuple] = tcp_conn;
tcp_conn.on_data = (data) => {
if(data.length !== 0) {
this.send_wisp_frame({
type: "DATA",
stream_id: tcp_conn.stream_id,
data: data
});
}
};
this.send_wisp_frame({
type: "CONNECT",
stream_id: tcp_conn.stream_id,
hostname: packet.ipv4.dest.join("."),
port: packet.tcp.dport,
data_callback: (data) => {
tcp_conn.write(data);
},
close_callback: (data) => {
tcp_conn.close();
}
});
tcp_conn.accept(packet);
return;
}
if(!this.tcp_conn[tuple]) {
dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH);
let bop = packet.tcp.ackn;
if(packet.tcp.fin || packet.tcp.syn) bop += 1;
reply.tcp = {
sport: packet.tcp.dport,
dport: packet.tcp.sport,
seq: bop,
ackn: packet.tcp.seq + (packet.tcp.syn ? 1: 0),
winsize: packet.tcp.winsize,
rst: true,
ack: packet.tcp.syn
};
this.receive(make_packet(reply));
return;
}
this.tcp_conn[tuple].process(packet);
}
if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) {
arp_whohas(packet, this);
}
if(packet.dns) {
// TODO: remove when this wisp client supports udp
(async () => {
let reply = {};
reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src };
reply.ipv4 = {
proto: IPV4_PROTO_UDP,
src: this.router_ip,
dest: packet.ipv4.src,
};
reply.udp = { sport: 53, dport: packet.udp.sport };
const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer());
reply.udp.data = new Uint8Array(result);
this.receive(make_packet(reply));
})();
}
if(packet.ntp) {
// TODO: remove when this wisp client supports udp
handle_fake_ntp(packet, this);
return;
}
if(packet.dhcp) {
handle_fake_dhcp(packet, this);
return;
}
if(packet.udp && packet.udp.dport === 8) {
// TODO: remove when this wisp client supports udp
handle_udp_echo(packet, this);
}
};
/**
* @param {Uint8Array} data
*/
WispNetworkAdapter.prototype.receive = function(data)
{
this.bus.send("net" + this.id + "-receive", new Uint8Array(data));
};

312
tests/devices/wisp_network.js Executable file
View file

@ -0,0 +1,312 @@
#!/usr/bin/env -S node --experimental-websocket
"use strict";
process.on("unhandledRejection", exn => { throw exn; });
const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;
const V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86;
const assert = require("assert").strict;
const SHOW_LOGS = false;
const STOP_ON_FIRST_FAILURE = false;
function log_pass(msg, ...args)
{
console.log(`\x1b[92m[+] ${msg}\x1b[0m`, ...args);
}
function log_warn(msg, ...args)
{
console.error(`\x1b[93m[!] ${msg}\x1b[0m`, ...args);
}
function log_fail(msg, ...args)
{
console.error(`\x1b[91m[-] ${msg}\x1b[0m`, ...args);
}
const tests =
[
{
name: "DHCP",
timeout: 60,
start: () =>
{
emulator.serial0_send("udhcpc\n");
emulator.serial0_send("echo -e done\\\\tudhcpc\n");
},
end_trigger: "done\tudhcpc",
end: (capture) =>
{
assert(/lease of 192.168.86.100 obtained/.test(capture), "lease of 192.168.86.100 obtained");
},
},
{
name: "ifconfig",
timeout: 60,
start: () =>
{
emulator.serial0_send("ifconfig\n");
emulator.serial0_send("echo -e done\\\\tifconfig\n");
},
end_trigger: "done\tifconfig",
end: (capture) =>
{
assert(/192.168.86.100/.test(capture), "192.168.86.100");
},
},
{
name: "route",
timeout: 60,
start: () =>
{
emulator.serial0_send("ip route\n");
emulator.serial0_send("echo -e done\\\\troute\n");
},
end_trigger: "done\troute",
end: (capture) =>
{
assert(/192.168.86.1/.test(capture), "192.168.86.100");
},
},
//{
// name: "arp -a",
// timeout: 60,
// start: () =>
// {
// emulator.serial0_send("arp -a\n");
// emulator.serial0_send("echo -e done\\\\tarp\n");
// },
// end_trigger: "done\tarp",
// end: (capture) =>
// {
// assert(/.192.168.86.1. at 52:54:00:01:02:03 \[ether\] {2}on eth0/.test(capture), "(192.168.86.1) at 52:54:00:01:02:03 [ether] on eth0");
// },
//},
{
name: "Curl example.org",
timeout: 60,
allow_failure: true,
start: () =>
{
emulator.serial0_send("wget -T 10 -O - example.org\n");
emulator.serial0_send("echo -e done\\\\texample.org\n");
},
end_trigger: "done\texample.org",
end: (capture) =>
{
assert(/This domain is for use in illustrative examples in documents/.test(capture), "got example.org text");
},
},
];
let test_num = 0;
let test_timeout = 0;
const failed_tests = [];
const emulator = new V86({
bios: { url: __dirname + "/../../bios/seabios.bin" },
vga_bios: { url: __dirname + "/../../bios/vgabios.bin" },
cdrom: { url: __dirname + "/../../images/linux4.iso" },
autostart: true,
memory_size: 64 * 1024 * 1024,
disable_jit: +process.env.DISABLE_JIT,
network_relay_url: "wisps://wisp.mercurywork.shop/",
log_level: SHOW_LOGS ? 0x400000 : 0,
});
emulator.add_listener("emulator-ready", function () {
});
let ran_command = false;
let line = "";
let capturing = false;
let capture = "";
let next_trigger;
let next_trigger_handler;
function start_timeout()
{
if(tests[test_num].timeout)
{
test_timeout = setTimeout(() =>
{
log_fail("Test #%d (%s) took longer than %s sec. Timing out and terminating.", test_num, tests[test_num].name, tests[test_num].timeout);
process.exit(1);
}, tests[test_num].timeout * 1000);
}
}
function begin()
{
start_timeout();
console.log("\nPreparing test #%d: %s", test_num, tests[test_num].name);
start_test();
}
function start_test()
{
console.log("Starting test #%d: %s", test_num, tests[test_num].name);
capture = "";
tests[test_num].start();
if(tests[test_num].capture_trigger)
{
next_trigger = tests[test_num].capture_trigger;
next_trigger_handler = start_capture;
}
else
{
next_trigger = tests[test_num].end_trigger;
next_trigger_handler = end_test;
}
start_capture();
}
function start_capture()
{
console.log("Capturing...");
capture = "";
capturing = true;
next_trigger = tests[test_num].end_trigger;
next_trigger_handler = end_test;
}
function end_test()
{
capturing = false;
if(tests[test_num].timeout)
{
clearTimeout(test_timeout);
}
let test_has_failed = false;
try {
tests[test_num].end(capture);
} catch(e) {
console.log(e);
test_has_failed = true;
}
if(!test_has_failed)
{
log_pass("Test #%d passed: %s", test_num, tests[test_num].name);
}
else
{
if(tests[test_num].allow_failure)
{
log_warn("Test #%d failed: %s (failure allowed)", test_num, tests[test_num].name);
}
else
{
log_fail("Test #%d failed: %s", test_num, tests[test_num].name);
if(STOP_ON_FIRST_FAILURE)
{
finish_tests();
}
}
test_has_failed = false;
}
test_num++;
if(test_num < tests.length)
{
begin();
}
else
{
finish_tests();
}
}
function finish_tests()
{
emulator.stop();
emulator.destroy();
console.log("\nTests finished.");
if(failed_tests.length === 0)
{
console.log("All tests passed");
}
else
{
let unallowed_failure = false;
console.error("Failed %d out of %d tests:", failed_tests.length, tests.length);
for(const num of failed_tests)
{
if(tests[num].allow_failure)
{
log_warn("#%d %s (failure allowed)", num, tests[num].name);
}
else
{
unallowed_failure = true;
log_fail("#%d %s", num, tests[num].name);
}
}
if(unallowed_failure)
{
process.exit(1);
}
}
}
emulator.bus.register("emulator-started", function()
{
console.error("Booting now, please stand by");
});
emulator.add_listener("serial0-output-byte", function(byte)
{
const chr = String.fromCharCode(byte);
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
{
return;
}
let new_line = "";
let is_new_line = false;
if(chr === "\n")
{
is_new_line = true;
new_line = line;
line = "";
}
else
{
line += chr;
}
if(!ran_command && line.endsWith("~% "))
{
ran_command = true;
begin();
}
else if(new_line === next_trigger)
{
next_trigger_handler();
}
else if(is_new_line && capturing)
{
capture += new_line + "\n";
console.log(" Captured: %s", new_line);
}
else if(is_new_line)
{
console.log(" Serial: %s", new_line);
}
});