import { LOG_NET } from '../const.js' import { dbg_log } from '../log.js' import { create_eth_encoder_buf, handle_fake_networking, TCPConnection, } from './fake_network.js' import type { EthEncoderBuf, NetworkAdapterLike, PacketSpec, } from './fake_network.js' import { BusConnector } from '../bus.js' interface WispNetworkConfig { id?: number router_mac?: string router_ip?: string vm_ip?: string masquerade?: boolean dns_method?: string doh_server?: string mtu?: number } interface WispConnection { data_callback: (data: Uint8Array) => void close_callback: (reason: number) => void congestion: number congested?: boolean } interface WispFrameConnect { type: 'CONNECT' stream_id: number hostname: string port: number data_callback: (data: Uint8Array) => void close_callback: (reason: number) => void } interface WispFrameData { type: 'DATA' stream_id: number data: Uint8Array } interface WispFrameClose { type: 'CLOSE' stream_id: number reason: number } type WispFrame = WispFrameConnect | WispFrameData | WispFrameClose interface CongestedPacket { data: Uint8Array type: string } export class WispNetworkAdapter implements NetworkAdapterLike { wispws: WebSocket | null last_stream: number connections: Record congested_buffer: CongestedPacket[] bus: BusConnector id: number router_mac: Uint8Array router_ip: Uint8Array vm_ip: Uint8Array masquerade: boolean vm_mac: Uint8Array dns_method: string doh_server: string | undefined tcp_conn: Record mtu: number | undefined eth_encoder_buf: EthEncoderBuf constructor( wisp_url: string, bus: BusConnector, config?: WispNetworkConfig, ) { this.wispws = null this.register_ws(wisp_url) this.last_stream = 1 this.connections = { 0: { congestion: 0, data_callback: () => {}, close_callback: () => {}, }, } 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.dns_method = config.dns_method || 'doh' this.doh_server = config.doh_server this.tcp_conn = {} this.mtu = config.mtu this.eth_encoder_buf = create_eth_encoder_buf(this.mtu) this.bus.register( 'net' + this.id + '-mac', function (this: WispNetworkAdapter, mac: string) { this.vm_mac = new Uint8Array( mac.split(':').map(function (x) { return parseInt(x, 16) }), ) }, this, ) this.bus.register( 'net' + this.id + '-send', function (this: WispNetworkAdapter, data: Uint8Array) { this.send(data) }, this, ) } register_ws(wisp_url: string): void { this.wispws = new WebSocket( wisp_url.replace('wisp://', 'ws://').replace('wisps://', 'wss://'), ) this.wispws.binaryType = 'arraybuffer' this.wispws.onmessage = (event: MessageEvent) => { this.process_incoming_wisp_frame(new Uint8Array(event.data)) } this.wispws.onclose = () => { setTimeout(() => { this.register_ws(wisp_url) }, 10000) // wait 10s before reconnecting } } send_packet(data: Uint8Array, type: string, stream_id: number): void { if (this.connections[stream_id]) { if (this.connections[stream_id].congestion > 0) { if (type === 'DATA') { this.connections[stream_id].congestion-- } this.wispws!.send(new Uint8Array(data)) } else { this.connections[stream_id].congested = true this.congested_buffer.push({ data: data, type: type }) } } } process_incoming_wisp_frame(frame: Uint8Array): void { 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) { const buffer = this.congested_buffer.slice(0) this.congested_buffer.length = 0 this.connections[stream_id].congested = false for (const packet of buffer) { this.send_packet(packet.data, packet.type, stream_id) } } 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, ) } } send_wisp_frame(frame_obj: WispFrame): void { let full_packet: Uint8Array let view: DataView 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 as WispFrame).type, LOG_NET, ) return } this.send_packet(full_packet, frame_obj.type, frame_obj.stream_id) } destroy(): void { if (this.wispws) { this.wispws.onmessage = null this.wispws.onclose = null this.wispws.close() this.wispws = null } } on_tcp_connection(conn: TCPConnection, packet: PacketSpec): boolean { conn.stream_id = this.last_stream++ conn.on('data', (data: Uint8Array) => { if (data.length !== 0) { this.send_wisp_frame({ type: 'DATA', stream_id: conn.stream_id, data: data, }) } }) conn.on_close = () => { this.send_wisp_frame({ type: 'CLOSE', stream_id: conn.stream_id, reason: 0x02, // 0x02: Voluntary stream closure }) } // WISP doesn't implement shutdown, use close as workaround conn.on_shutdown = conn.on_close this.send_wisp_frame({ type: 'CONNECT', stream_id: conn.stream_id, hostname: packet.ipv4!.dest.join('.'), port: conn.sport, data_callback: (data: Uint8Array) => { conn.write(data) }, close_callback: (_data: number) => { conn.close() }, }) conn.accept() return true } send(data: Uint8Array): void { // TODO: forward UDP traffic to WISP server once this WISP client supports UDP handle_fake_networking(data, this) } receive(data: Uint8Array): void { this.bus.send('net' + this.id + '-receive', new Uint8Array(data)) } }