import { LOG_FETCH } from "../const.js"; import { h } from "../lib.js"; import { dbg_assert, dbg_log } from "../log.js"; // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml const ETHERTYPE_IPV4 = 0x0800; const ETHERTYPE_ARP = 0x0806; const ETHERTYPE_IPV6 = 0x86DD; const IPV4_PROTO_ICMP = 1; const IPV4_PROTO_TCP = 6; const IPV4_PROTO_UDP = 17; const UNIX_EPOCH = new Date("1970-01-01T00:00:00Z").getTime(); const NTP_EPOCH = new Date("1900-01-01T00:00:00Z").getTime(); const NTP_EPOC_DIFF = UNIX_EPOCH - NTP_EPOCH; const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; /* For the complete TCP state diagram see: * * https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg * * State TIME_WAIT is not needed, we can skip it and transition directly to CLOSED instead. */ export const TCP_STATE_CLOSED = "closed"; export const TCP_STATE_SYN_RECEIVED = "syn-received"; export const TCP_STATE_SYN_SENT = "syn-sent"; export const TCP_STATE_SYN_PROBE = "syn-probe"; //const TCP_STATE_LISTEN = "listen"; export const TCP_STATE_ESTABLISHED = "established"; export const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; export const TCP_STATE_CLOSE_WAIT = "close-wait"; export const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; export const TCP_STATE_LAST_ACK = "last-ack"; export const TCP_STATE_CLOSING = "closing"; //const TCP_STATE_TIME_WAIT = "time-wait"; // source: RFC6335, 6. Port Number Ranges const TCP_DYNAMIC_PORT_START = 49152; const TCP_DYNAMIC_PORT_END = 65535; const TCP_DYNAMIC_PORT_RANGE = TCP_DYNAMIC_PORT_END - TCP_DYNAMIC_PORT_START; const ETH_HEADER_SIZE = 14; const ETH_PAYLOAD_OFFSET = ETH_HEADER_SIZE; const ETH_PAYLOAD_SIZE = 1500; const ETH_TRAILER_SIZE = 4; const ETH_FRAME_SIZE = ETH_HEADER_SIZE + ETH_PAYLOAD_SIZE + ETH_TRAILER_SIZE; const IPV4_HEADER_SIZE = 20; const IPV4_PAYLOAD_OFFSET = ETH_PAYLOAD_OFFSET + IPV4_HEADER_SIZE; const IPV4_PAYLOAD_SIZE = ETH_PAYLOAD_SIZE - IPV4_HEADER_SIZE; const UDP_HEADER_SIZE = 8; const UDP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_SIZE; const UDP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - UDP_HEADER_SIZE; const TCP_HEADER_SIZE = 20; const TCP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + TCP_HEADER_SIZE; const TCP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - TCP_HEADER_SIZE; const ICMP_HEADER_SIZE = 4; const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } class GrowableRingbuffer { /** * @param {number} initial_capacity * @param {number} maximum_capacity */ constructor(initial_capacity, maximum_capacity) { initial_capacity = Math.min(initial_capacity, 16); this.maximum_capacity = maximum_capacity ? Math.max(maximum_capacity, initial_capacity) : 0; this.tail = 0; this.head = 0; this.length = 0; this.buffer = new Uint8Array(initial_capacity); } /** * @param {Uint8Array} src_array */ write(src_array) { const src_length = src_array.length; const total_length = this.length + src_length; let capacity = this.buffer.length; if(capacity < total_length) { dbg_assert(capacity > 0); while(capacity < total_length) { capacity *= 2; } if(this.maximum_capacity && capacity > this.maximum_capacity) { throw new Error("stream capacity overflow in GrowableRingbuffer.write(), package dropped"); } const new_buffer = new Uint8Array(capacity); this.peek(new_buffer); this.tail = 0; this.head = this.length; this.buffer = new_buffer; } const buffer = this.buffer; const new_head = this.head + src_length; if(new_head > capacity) { const i_split = capacity - this.head; buffer.set(src_array.subarray(0, i_split), this.head); buffer.set(src_array.subarray(i_split)); } else { buffer.set(src_array, this.head); } this.head = new_head % capacity; this.length += src_length; } /** * @param {Uint8Array} dst_array */ peek(dst_array) { const length = Math.min(this.length, dst_array.length); if(length) { const buffer = this.buffer; const capacity = buffer.length; const new_tail = this.tail + length; if(new_tail > capacity) { const buf_len_left = new_tail % capacity; const buf_len_right = capacity - this.tail; dst_array.set(buffer.subarray(this.tail)); dst_array.set(buffer.subarray(0, buf_len_left), buf_len_right); } else { dst_array.set(buffer.subarray(this.tail, new_tail)); } } return length; } /** * @param {number} length */ remove(length) { if(length > this.length) { length = this.length; } if(length) { this.tail = (this.tail + length) % this.buffer.length; this.length -= length; } return length; } } export function create_eth_encoder_buf() { const eth_frame = new Uint8Array(ETH_FRAME_SIZE); const buffer = eth_frame.buffer; const offset = eth_frame.byteOffset; return { eth_frame: eth_frame, eth_frame_view: new DataView(buffer), eth_payload_view: new DataView(buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE), ipv4_payload_view: new DataView(buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE), udp_payload_view: new DataView(buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE), text_encoder: new TextEncoder() }; } /** * Copy given data array into view starting at offset, return number of bytes written. * * @param {number} offset * @param {ArrayBuffer|ArrayBufferView} data * @param {DataView} view * @param {Object} out */ function view_set_array(offset, data, view, out) { out.eth_frame.set(data, view.byteOffset + offset); return data.length; } /** * UTF8-encode given string into view starting at offset, return number of bytes written. * * @param {number} offset * @param {string} str * @param {DataView} view * @param {Object} out */ function view_set_string(offset, str, view, out) { return out.text_encoder.encodeInto(str, out.eth_frame.subarray(view.byteOffset + offset)).written; } /** * Calculate internet checksum for view[0 : length] and return the 16-bit result. * Source: RFC768 and RFC1071 (chapter 4.1). * * @param {number} length * @param {number} checksum * @param {DataView} view * @param {Object} out */ function calc_inet_checksum(length, checksum, view, out) { const uint16_end = view.byteOffset + (length & ~1); const eth_frame = out.eth_frame; for(let i = view.byteOffset; i < uint16_end; i += 2) { checksum += eth_frame[i] << 8 | eth_frame[i+1]; } if(length & 1) { checksum += eth_frame[uint16_end] << 8; } while(checksum >>> 16) { checksum = (checksum & 0xffff) + (checksum >>> 16); } return ~checksum & 0xffff; } /** * @param {Object} out * @param {Object} spec */ function make_packet(out, spec) { dbg_assert(spec.eth); out.eth_frame.fill(0); return out.eth_frame.subarray(0, write_eth(spec, out)); } function handle_fake_tcp(packet, adapter) { const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { dbg_log("SYN to already opened port", LOG_FETCH); } if(adapter.on_tcp_connection(packet, tuple)) { return; } } if(!adapter.tcp_conn[tuple]) { dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; reply.ipv4 = { proto: IPV4_PROTO_TCP, src: packet.ipv4.dest, dest: packet.ipv4.src }; 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 }; adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } adapter.tcp_conn[tuple].process(packet); } function handle_fake_dns_static(packet, adapter) { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; reply.ipv4 = { proto: IPV4_PROTO_UDP, src: adapter.router_ip, dest: packet.ipv4.src, }; reply.udp = { sport: 53, dport: packet.udp.sport }; let answers = []; let flags = 0x8000; //Response, flags |= 0x0180; // Recursion // flags |= 0x0400; Authoritative for(let i = 0; i < packet.dns.questions.length; ++i) { let q = packet.dns.questions[i]; switch(q.type){ case 1: // A record answers.push({ name: q.name, type: q.type, class: q.class, ttl: 600, data: [192, 168, 87, 1] }); break; default: } } reply.dns = { id: packet.dns.id, flags: flags, questions: packet.dns.questions, answers: answers }; adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } function handle_fake_dns_doh(packet, adapter) { const fetch_url = `https://${adapter.doh_server || DEFAULT_DOH_SERVER}/dns-query`; const fetch_opts = { method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data }; fetch(fetch_url, fetch_opts).then(async (resp) => { const reply = { eth: { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }, ipv4: { proto: IPV4_PROTO_UDP, src: adapter.router_ip, dest: packet.ipv4.src }, udp: { sport: 53, dport: packet.udp.sport, data: new Uint8Array(await resp.arrayBuffer()) } }; adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); }); return true; } function handle_fake_dns(packet, adapter) { if(adapter.dns_method === "static") { return handle_fake_dns_static(packet, adapter); } else { return handle_fake_dns_doh(packet, adapter); } } function handle_fake_ntp(packet, adapter) { let now = Date.now(); // - 1000 * 60 * 60 * 24 * 7; let now_n = now + NTP_EPOC_DIFF; let now_n_f = TWO_TO_32 * ((now_n % 1000) / 1000); 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: 123, dport: packet.udp.sport }; let flags = (0 << 6) | (4 << 3) | 4; reply.ntp = Object.assign({}, packet.ntp); reply.ntp.flags = flags; reply.ntp.poll = 10; reply.ntp.ori_ts_i = packet.ntp.trans_ts_i; reply.ntp.ori_ts_f = packet.ntp.trans_ts_f; reply.ntp.rec_ts_i = now_n / 1000; reply.ntp.rec_ts_f = now_n_f; reply.ntp.trans_ts_i = now_n / 1000; reply.ntp.trans_ts_f = now_n_f; reply.ntp.stratum = 2; adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } function handle_fake_dhcp(packet, adapter) { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; reply.ipv4 = { proto: IPV4_PROTO_UDP, src: adapter.router_ip, dest: adapter.vm_ip, }; reply.udp = { sport: 67, dport: 68, }; reply.dhcp = { htype: 1, hlen: 6, hops: 0, xid: packet.dhcp.xid, secs: 0, flags: 0, ciaddr: 0, yiaddr: iptolong(adapter.vm_ip), siaddr: iptolong(adapter.router_ip), giaddr: iptolong(adapter.router_ip), chaddr: packet.dhcp.chaddr, }; let options = []; // idk, it seems like op should be 3, but udhcpc sends 1 let fix = packet.dhcp.options.find(function(x) { return x[0] === 53; }); if( fix && fix[2] === 3 ) packet.dhcp.op = 3; if(packet.dhcp.op === 1) { reply.dhcp.op = 2; options.push(new Uint8Array([53, 1, 2])); } if(packet.dhcp.op === 3) { reply.dhcp.op = 2; options.push(new Uint8Array([53, 1, 5])); options.push(new Uint8Array([51, 4, 8, 0, 0, 0])); // Lease Time } let router_ip = [adapter.router_ip[0], adapter.router_ip[1], adapter.router_ip[2], adapter.router_ip[3]]; options.push(new Uint8Array([1, 4, 255, 255, 255, 0])); // Netmask if(adapter.masquerade) { options.push(new Uint8Array([3, 4].concat(router_ip))); // Router options.push(new Uint8Array([6, 4].concat(router_ip))); // DNS } options.push(new Uint8Array([54, 4].concat(router_ip))); // DHCP Server options.push(new Uint8Array([60, 3].concat(V86_ASCII))); // Vendor options.push(new Uint8Array([255, 0])); reply.dhcp.options = options; adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } export function handle_fake_networking(data, adapter) { let packet = {}; parse_eth(data, packet); if(packet.ipv4) { if(packet.tcp) { handle_fake_tcp(packet, adapter); } else if(packet.udp) { if(packet.dns) { handle_fake_dns(packet, adapter); } else if(packet.dhcp) { handle_fake_dhcp(packet, adapter); } else if(packet.ntp) { handle_fake_ntp(packet, adapter); } else if(packet.udp.dport === 8) { handle_udp_echo(packet, adapter); } } else if(packet.icmp && packet.icmp.type === 8) { handle_fake_ping(packet, adapter); } } else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { arp_whohas(packet, adapter); } } function parse_eth(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let ethertype = view.getUint16(12); let eth = { ethertype: ethertype, dest: data.subarray(0, 6), dest_s: a2ethaddr(data.subarray(0, 6)), src: data.subarray(6, 12), src_s: a2ethaddr(data.subarray(6, 12)), }; o.eth = eth; // TODO: Remove CRC from the end of the packet maybe? let payload = data.subarray(ETH_HEADER_SIZE, data.length); if(ethertype === ETHERTYPE_IPV4) { parse_ipv4(payload, o); } else if(ethertype === ETHERTYPE_ARP) { parse_arp(payload, o); } else if(ethertype === ETHERTYPE_IPV6) { dbg_log("Unimplemented: ipv6"); } else { dbg_log("Unknown ethertype: " + h(ethertype), LOG_FETCH); } } function write_eth(spec, out) { const view = out.eth_frame_view; view_set_array(0, spec.eth.dest, view, out); view_set_array(6, spec.eth.src, view, out); view.setUint16(12, spec.eth.ethertype); let len = ETH_HEADER_SIZE; if(spec.arp) { len += write_arp(spec, out); } else if(spec.ipv4) { len += write_ipv4(spec, out); } return len; } function parse_arp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let hlen = data[4]; let plen = data[5]; let arp = { htype: view.getUint16(0), ptype: view.getUint16(2), oper: view.getUint16(6), sha: data.subarray(8, 14), spa: data.subarray(14, 18), tha: data.subarray(18, 24), tpa: data.subarray(24, 28), }; o.arp = arp; } function write_arp(spec, out) { const view = out.eth_payload_view; view.setUint16(0, spec.arp.htype); view.setUint16(2, spec.arp.ptype); view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); view_set_array(8, spec.arp.sha, view, out); view_set_array(14, spec.arp.spa, view, out); view_set_array(18, spec.arp.tha, view, out); view_set_array(24, spec.arp.tpa, view, out); return 28; } function parse_ipv4(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let version = (data[0] >> 4) & 0x0F; let ihl = data[0] & 0x0F; let tos = view.getUint8(1); let len = view.getUint16(2); let ttl = view.getUint8(8); let proto = view.getUint8(9); let ip_checksum = view.getUint16(10); let ipv4 = { version, ihl, tos, len, ttl, proto, ip_checksum, src: data.subarray(12, 12+4), dest: data.subarray(16, 16+4), }; // Ethernet minmum packet size. if(Math.max(len, 46) !== data.length) { dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); } o.ipv4 = ipv4; let ipdata = data.subarray(ihl * 4, len); if(proto === IPV4_PROTO_ICMP) { parse_icmp(ipdata, o); } else if(proto === IPV4_PROTO_TCP) { parse_tcp(ipdata, o); } else if(proto === IPV4_PROTO_UDP) { parse_udp(ipdata, o); } } function write_ipv4(spec, out) { const view = out.eth_payload_view; const ihl = IPV4_HEADER_SIZE >> 2; // header length in 32-bit words const version = 4; let len = IPV4_HEADER_SIZE; if(spec.icmp) { len += write_icmp(spec, out); } else if(spec.udp) { len += write_udp(spec, out); } else if(spec.tcp) { len += write_tcp(spec, out); } view.setUint8(0, version << 4 | (ihl & 0x0F)); view.setUint8(1, spec.ipv4.tos || 0); view.setUint16(2, len); view.setUint16(4, spec.ipv4.id || 0); view.setUint8(6, 2 << 5); // DF Flag view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); view.setUint16(10, 0); // checksum initially zero before calculation view_set_array(12, spec.ipv4.src, view, out); view_set_array(16, spec.ipv4.dest, view, out); view.setUint16(10, calc_inet_checksum(IPV4_HEADER_SIZE, 0, view, out)); return len; } function parse_icmp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let icmp = { type: view.getUint8(0), code: view.getUint8(1), checksum: view.getUint16(2), data: data.subarray(4) }; o.icmp = icmp; } function write_icmp(spec, out) { const view = out.ipv4_payload_view; view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); view.setUint16(2, 0); // checksum initially zero before calculation const data_length = view_set_array(ICMP_HEADER_SIZE, spec.icmp.data, view, out); const total_length = ICMP_HEADER_SIZE + data_length; view.setUint16(2, calc_inet_checksum(total_length, 0, view, out)); return total_length; } function parse_udp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let udp = { sport: view.getUint16(0), dport: view.getUint16(2), len: view.getUint16(4), checksum: view.getUint16(6), data: data.subarray(8), data_s: new TextDecoder().decode(data.subarray(8)) }; //dbg_assert(udp.data.length + 8 == udp.len); if(udp.dport === 67 || udp.sport === 67) { //DHCP parse_dhcp(data.subarray(8), o); } else if(udp.dport === 53 || udp.sport === 53) { parse_dns(data.subarray(8), o); } else if(udp.dport === 123) { parse_ntp(data.subarray(8), o); } o.udp = udp; } function write_udp(spec, out) { const view = out.ipv4_payload_view; let total_length = UDP_HEADER_SIZE; if(spec.dhcp) { total_length += write_dhcp(spec, out); } else if(spec.dns) { total_length += write_dns(spec, out); } else if(spec.ntp) { total_length += write_ntp(spec, out); } else { total_length += view_set_array(0, spec.udp.data, out.udp_payload_view, out); } view.setUint16(0, spec.udp.sport); view.setUint16(2, spec.udp.dport); view.setUint16(4, total_length); view.setUint16(6, 0); // checksum initially zero before calculation const pseudo_header = (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_UDP + total_length; view.setUint16(6, calc_inet_checksum(total_length, pseudo_header, view, out)); return total_length; } function parse_dns(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let dns = { id: view.getUint16(0), flags: view.getUint16(2), questions: [], answers: [] }; let qdcount = view.getUint16(4); let ancount = view.getUint16(6); let nscount = view.getUint16(8); let arcount = view.getUint16(10); let offset = 12; function read_dstr() { let o = []; let len; do { len = view.getUint8(offset); o.push(new TextDecoder().decode(data.subarray(offset+1, offset+1+len))); offset += len + 1; } while(len > 0); return o; } for(let i = 0; i < qdcount; i++) { dns.questions.push({ name: read_dstr(), type: view.getInt16(offset), class: view.getInt16(offset + 2) }); offset += 4; } for(let i = 0; i < ancount; i++) { let ans = { name: read_dstr(), type: view.getInt16(offset), class: view.getUint16(offset + 2), ttl: view.getUint32(offset + 4) }; offset += 8; let rdlen = view.getUint16(offset); offset += 2; ans.data = data.subarray(offset, offset+rdlen); offset += rdlen; dns.answers.push(ans); } o.dns = dns; } function write_dns(spec, out) { const view = out.udp_payload_view; view.setUint16(0, spec.dns.id); view.setUint16(2, spec.dns.flags); view.setUint16(4, spec.dns.questions.length); view.setUint16(6, spec.dns.answers.length); let offset = 12; for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { const n_written = view_set_string(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } view.setUint16(offset, q.type); offset += 2; view.setUint16(offset, q.class); offset += 2; } function write_reply(a) { for(let s of a.name) { const n_written = view_set_string(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } view.setUint16(offset, a.type); offset += 2; view.setUint16(offset, a.class); offset += 2; view.setUint32(offset, a.ttl); offset += 4; view.setUint16(offset, a.data.length); offset += 2; offset += view_set_array(offset, a.data, view, out); } for(let i = 0; i < spec.dns.answers.length; ++i) { let a = spec.dns.answers[i]; write_reply(a); } return offset; } function parse_dhcp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let bootpo = data.subarray(44,44+192); let dhcp = { op: view.getUint8(0), htype: view.getUint8(1), hlen: view.getUint8(2), hops: view.getUint8(3), xid: view.getUint32(4), secs: view.getUint16(8), flags: view.getUint16(10), ciaddr: view.getUint32(12), yiaddr: view.getUint32(16), siaddr: view.getUint32(20), giaddr: view.getUint32(24), chaddr: data.subarray(28,28+16), magic: view.getUint32(236), options: [], }; let options = data.subarray(240); for(let i = 0; i < options.length; ++i) { let start = i; let op = options[i]; if(op === 0) continue; ++i; let len = options[i]; i += len; dhcp.options.push(options.subarray(start, start + len + 2)); } o.dhcp = dhcp; o.dhcp_options = dhcp.options; } function write_dhcp(spec, out) { const view = out.udp_payload_view; view.setUint8(0, spec.dhcp.op); view.setUint8(1, spec.dhcp.htype); view.setUint8(2, spec.dhcp.hlen); view.setUint8(3, spec.dhcp.hops); view.setUint32(4, spec.dhcp.xid); view.setUint16(8, spec.dhcp.secs); view.setUint16(10, spec.dhcp.flags); view.setUint32(12, spec.dhcp.ciaddr); view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); view_set_array(28, spec.dhcp.chaddr, view, out); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { offset += view_set_array(offset, o, view, out); } return offset; } function parse_ntp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); o.ntp = { flags: view.getUint8(0), stratum: view.getUint8(1), poll: view.getUint8(2), precision: view.getUint8(3), root_delay: view.getUint32(4), root_disp: view.getUint32(8), ref_id: view.getUint32(12), ref_ts_i: view.getUint32(16), ref_ts_f: view.getUint32(20), ori_ts_i: view.getUint32(24), ori_ts_f: view.getUint32(28), rec_ts_i: view.getUint32(32), rec_ts_f: view.getUint32(36), trans_ts_i: view.getUint32(40), trans_ts_f: view.getUint32(44), }; } function write_ntp(spec, out) { const view = out.udp_payload_view; view.setUint8(0, spec.ntp.flags); view.setUint8(1, spec.ntp.stratum); view.setUint8(2, spec.ntp.poll); view.setUint8(3, spec.ntp.precision); view.setUint32(4, spec.ntp.root_delay); view.setUint32(8, spec.ntp.root_disp); view.setUint32(12, spec.ntp.ref_id); view.setUint32(16, spec.ntp.ref_ts_i); view.setUint32(20, spec.ntp.ref_ts_f); view.setUint32(24, spec.ntp.ori_ts_i); view.setUint32(28, spec.ntp.ori_ts_f); view.setUint32(32, spec.ntp.rec_ts_i); view.setUint32(36, spec.ntp.rec_ts_f); view.setUint32(40, spec.ntp.trans_ts_i); view.setUint32(44, spec.ntp.trans_ts_f); return 48; } function parse_tcp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); let tcp = { sport: view.getUint16(0), dport: view.getUint16(2), seq: view.getUint32(4), ackn: view.getUint32(8), doff: view.getUint8(12) >> 4, winsize: view.getUint16(14), checksum: view.getUint16(16), urgent: view.getUint16(18), }; let flags = view.getUint8(13); tcp.fin = !!(flags & 0x01); tcp.syn = !!(flags & 0x02); tcp.rst = !!(flags & 0x04); tcp.psh = !!(flags & 0x08); tcp.ack = !!(flags & 0x10); tcp.urg = !!(flags & 0x20); tcp.ece = !!(flags & 0x40); tcp.cwr = !!(flags & 0x80); o.tcp = tcp; let offset = tcp.doff * 4; o.tcp_data = data.subarray(offset); } function write_tcp(spec, out) { const view = out.ipv4_payload_view; let flags = 0; let tcp = spec.tcp; if(tcp.fin) flags |= 0x01; if(tcp.syn) flags |= 0x02; if(tcp.rst) flags |= 0x04; if(tcp.psh) flags |= 0x08; if(tcp.ack) flags |= 0x10; if(tcp.urg) flags |= 0x20; if(tcp.ece) flags |= 0x40; if(tcp.cwr) flags |= 0x80; const doff = TCP_HEADER_SIZE >> 2; // header length in 32-bit words view.setUint16(0, tcp.sport); view.setUint16(2, tcp.dport); view.setUint32(4, tcp.seq); view.setUint32(8, tcp.ackn); view.setUint8(12, doff << 4); view.setUint8(13, flags); view.setUint16(14, tcp.winsize); view.setUint16(16, 0); // checksum initially zero before calculation view.setUint16(18, tcp.urgent || 0); let total_length = TCP_HEADER_SIZE; if(spec.tcp_data) { total_length += view_set_array(TCP_HEADER_SIZE, spec.tcp_data, view, out); } const pseudo_header = (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_TCP + total_length; view.setUint16(16, calc_inet_checksum(total_length, pseudo_header, view, out)); return total_length; } export function fake_tcp_connect(dport, adapter) { const vm_ip_str = adapter.vm_ip.join("."); const router_ip_str = adapter.router_ip.join("."); const sport_0 = (Math.random() * TCP_DYNAMIC_PORT_RANGE) | 0; let sport, tuple, sport_i = 0; do { sport = TCP_DYNAMIC_PORT_START + ((sport_0 + sport_i) % TCP_DYNAMIC_PORT_RANGE); tuple = `${vm_ip_str}:${dport}:${router_ip_str}:${sport}`; } while(++sport_i < TCP_DYNAMIC_PORT_RANGE && adapter.tcp_conn[tuple]); if(adapter.tcp_conn[tuple]) { throw new Error("pool of dynamic TCP port numbers exhausted, connection aborted"); } let conn = new TCPConnection(); conn.tuple = tuple; conn.hsrc = adapter.router_mac; conn.psrc = adapter.router_ip; conn.sport = sport; conn.hdest = adapter.vm_mac; conn.dport = dport; conn.pdest = adapter.vm_ip; conn.net = adapter; adapter.tcp_conn[tuple] = conn; conn.connect(); return conn; } export function fake_tcp_probe(dport, adapter) { return new Promise((res, rej) => { let handle = fake_tcp_connect(dport, adapter); handle.state = TCP_STATE_SYN_PROBE; handle.on("probe", res); }); } /** * @constructor */ export function TCPConnection() { this.state = TCP_STATE_CLOSED; this.net = null; // The adapter is stored here this.send_buffer = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); this.in_active_close = false; this.delayed_send_fin = false; this.delayed_state = undefined; this.events_handlers = {}; } TCPConnection.prototype.on = function(event, handler) { this.events_handlers[event] = handler; }; TCPConnection.prototype.emit = function(event, ...args) { if(!this.events_handlers[event]) return; this.events_handlers[event].apply(this, args); }; TCPConnection.prototype.ipv4_reply = function() { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.hsrc, dest: this.hdest }; reply.ipv4 = { proto: IPV4_PROTO_TCP, src: this.psrc, dest: this.pdest }; reply.tcp = { sport: this.sport, dport: this.dport, winsize: this.winsize, ackn: this.ack, seq: this.seq, ack: true }; return reply; }; TCPConnection.prototype.packet_reply = function(packet, tcp_options) { const reply_tcp = { sport: packet.tcp.dport, dport: packet.tcp.sport, winsize: packet.tcp.winsize, ackn: this.ack, seq: this.seq }; if(tcp_options) { for(const opt in tcp_options) { reply_tcp[opt] = tcp_options[opt]; } } const reply = this.ipv4_reply(); reply.tcp = reply_tcp; return reply; }; TCPConnection.prototype.connect = function() { // dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.seq = 1338; this.ack = 1; this.start_seq = 0; this.winsize = 64240; this.state = TCP_STATE_SYN_SENT; let reply = this.ipv4_reply(); reply.ipv4.id = 2345; reply.tcp = { sport: this.sport, dport: this.dport, seq: 1337, ackn: 0, winsize: 0, syn: true, }; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.accept = function(packet) { this.seq = 1338; this.ack = packet.tcp.seq + 1; this.start_seq = packet.tcp.seq; this.hsrc = this.net.router_mac; this.psrc = packet.ipv4.dest; this.sport = packet.tcp.dport; this.hdest = packet.eth.src; this.dport = packet.tcp.sport; this.pdest = packet.ipv4.src; this.winsize = packet.tcp.winsize; let reply = this.ipv4_reply(); reply.tcp = { sport: this.sport, dport: this.dport, seq: 1337, ackn: this.ack, winsize: packet.tcp.winsize, syn: true, ack: true }; // dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); this.state = TCP_STATE_ESTABLISHED; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.process = function(packet) { if(this.state === TCP_STATE_CLOSED) { // dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } else if(packet.tcp.rst) { if(this.state === TCP_STATE_SYN_PROBE) { this.emit("probe", false); this.release(); return; } // dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); this.on_close(); this.release(); return; } else if(packet.tcp.syn) { if(this.state === TCP_STATE_SYN_SENT && packet.tcp.ack) { this.ack = packet.tcp.seq + 1; this.start_seq = packet.tcp.seq; this.last_received_ackn = packet.tcp.ackn; const reply = this.ipv4_reply(); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); // dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); this.state = TCP_STATE_ESTABLISHED; this.emit("connect"); } else if(this.state === TCP_STATE_SYN_PROBE && packet.tcp.ack) { this.emit("probe", true); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.release(); } else { dbg_log(`TCP[${this.tuple}]: WARNING: unexpected SYN packet dropped`, LOG_FETCH); } if(packet.tcp_data.length) { dbg_log(`TCP[${this.tuple}]: WARNING: ${packet.tcp_data.length} bytes of unexpected SYN packet payload dropped`, LOG_FETCH); } return; } if(packet.tcp.ack) { if(this.state === TCP_STATE_SYN_RECEIVED) { // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); this.state = TCP_STATE_ESTABLISHED; } else if(this.state === TCP_STATE_FIN_WAIT_1) { if(!packet.tcp.fin) { // handle FIN+ACK in FIN_WAIT_1 separately further down below // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); this.state = TCP_STATE_FIN_WAIT_2; } } else if(this.state === TCP_STATE_CLOSING || this.state === TCP_STATE_LAST_ACK) { // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); this.release(); return; } } if(this.last_received_ackn === undefined) { this.last_received_ackn = packet.tcp.ackn; } else { const n_ack = packet.tcp.ackn - this.last_received_ackn; //console.log("Read ", n_ack, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) if(n_ack > 0) { this.last_received_ackn = packet.tcp.ackn; this.send_buffer.remove(n_ack); this.seq += n_ack; this.pending = false; if(this.delayed_send_fin && !this.send_buffer.length) { // dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${this.delayed_state}"`, LOG_FETCH); this.delayed_send_fin = false; this.state = this.delayed_state; const reply = this.ipv4_reply(); reply.tcp.fin = true; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } } else if(n_ack < 0) { // TODO: could this just be a 32-bit sequence number overflow? dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.on_close(); this.release(); return; } } if(packet.tcp.fin) { if(this.ack !== packet.tcp.seq) { dbg_log(`TCP[${this.tuple}]: WARNING: closing connection in state "${this.state}" with invalid seq (${this.ack} != ${packet.tcp.seq})`, LOG_FETCH); } ++this.ack; // FIN increases seqnr const reply = this.packet_reply(packet, {}); if(this.state === TCP_STATE_ESTABLISHED) { // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); reply.tcp.ack = true; this.state = TCP_STATE_CLOSE_WAIT; this.on_shutdown(); } else if(this.state === TCP_STATE_FIN_WAIT_1) { if(packet.tcp.ack) { // dbg_log(`TCP[${this.tuple}]: received ACK+FIN in state "${this.state}"`, LOG_FETCH); this.release(); } else { // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_CLOSING}"`, LOG_FETCH); this.state = TCP_STATE_CLOSING; } reply.tcp.ack = true; } else if(this.state === TCP_STATE_FIN_WAIT_2) { // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}"`, LOG_FETCH); this.release(); reply.tcp.ack = true; } else { // dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); this.release(); this.on_close(); reply.tcp.rst = true; } this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } else if(this.ack !== packet.tcp.seq) { // Handle TCP Keep-Alives silently. // Excerpt from RFC 9293, 3.8.4. TCP Keep-Alives: // To confirm that an idle connection is still active, these // implementations send a probe segment designed to elicit a response // from the TCP peer. Such a segment generally contains SEG.SEQ = // SND.NXT-1 and may or may not contain one garbage octet of data. if(this.ack !== packet.tcp.seq + 1) { dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} ` + `pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} ` + `(${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); } const reply = this.packet_reply(packet, {ack: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } else if(packet.tcp.ack && packet.tcp_data.length > 0) { this.ack += packet.tcp_data.length; const reply = this.ipv4_reply(); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.emit("data", packet.tcp_data); } this.pump(); }; /** * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { if(!this.in_active_close) { this.send_buffer.write(data); } this.pump(); }; /** * @param {!Array} data_array */ TCPConnection.prototype.writev = function(data_array) { if(!this.in_active_close) { for(const data of data_array) { this.send_buffer.write(data); } } this.pump(); }; TCPConnection.prototype.close = function() { if(!this.in_active_close) { this.in_active_close = true; let next_state; if(this.state === TCP_STATE_ESTABLISHED || this.state === TCP_STATE_SYN_RECEIVED) { next_state = TCP_STATE_FIN_WAIT_1; } else if(this.state === TCP_STATE_CLOSE_WAIT) { next_state = TCP_STATE_LAST_ACK; } else { if(this.state !== TCP_STATE_SYN_SENT) { dbg_log(`TCP[${this.tuple}]: active close in unexpected state "${this.state}"`, LOG_FETCH); } this.release(); return; } if(this.send_buffer.length || this.pending) { // dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}", delayed next "${next_state}"`, LOG_FETCH); this.delayed_send_fin = true; this.delayed_state = next_state; } else { // dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${next_state}"`, LOG_FETCH); this.state = next_state; const reply = this.ipv4_reply(); reply.tcp.fin = true; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } } this.pump(); }; TCPConnection.prototype.on_shutdown = function() { this.emit("shutdown"); // forward FIN event from guest device to network adapter }; TCPConnection.prototype.on_close = function() { this.emit("close"); // forward RST event from guest device to network adapter }; TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); this.state = TCP_STATE_CLOSED; delete this.net.tcp_conn[this.tuple]; } }; TCPConnection.prototype.pump = function() { if(this.send_buffer.length && !this.pending) { const data = this.send_chunk_buf; const n_ready = this.send_buffer.peek(data); const reply = this.ipv4_reply(); reply.tcp.psh = true; reply.tcp_data = data.subarray(0, n_ready); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.pending = true; } }; 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(adapter.eth_encoder_buf, 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: packet.ipv4.dest, dest: packet.ipv4.src, }; reply.icmp = { type: 0, code: packet.icmp.code, data: packet.icmp.data }; adapter.receive(make_packet(adapter.eth_encoder_buf, 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(adapter.eth_encoder_buf, reply)); }