"use strict";

// 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.
 */
const TCP_STATE_CLOSED = "closed";
const TCP_STATE_SYN_RECEIVED = "syn-received";
const TCP_STATE_SYN_SENT = "syn-sent";
//const TCP_STATE_LISTEN = "listen";
const TCP_STATE_ESTABLISHED = "established";
const TCP_STATE_FIN_WAIT_1 = "fin-wait-1";
const TCP_STATE_CLOSE_WAIT = "close-wait";
const TCP_STATE_FIN_WAIT_2 = "fin-wait-2";
const TCP_STATE_LAST_ACK = "last-ack";
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) {
            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;
    }
}

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));
}

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;
}

/**
 * @constructor
 */
function TCPConnection()
{
    this.state = TCP_STATE_CLOSED;
    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;
}

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;
};

/*
// TODO: Is this method used anywhere anymore? It used to be called from fake_tcp_connect() which was removed.
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) {
        // 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;
            if(this.on_connect) {
                this.on_connect.call(this);
            }
        }
        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.on_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() {
    // forward FIN event from guest device to network adapter
};

TCPConnection.prototype.on_close = function() {
    // 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: adapter.router_ip,
        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));
}
