v86 / src /browser /fake_network.js
peterpeter8585's picture
Upload 553 files
8df6da4 verified
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<Uint8Array>} 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));
}