import { EventEmitter } from "events";
import { performance } from "perf_hooks";
import { normalizeIpv4Servers } from "../utils/dns.js";
// Protocol Constants
const ETH_P_IP = 0x0800;
const ETH_P_ARP = 0x0806;
const IP_PROTO_TCP = 6;
const IP_PROTO_UDP = 17;
const IP_PROTO_ICMP = 1;
function makeTcpNatKey(srcIP, srcPort, dstIP, dstPort) {
    return `TCP:${srcIP}:${srcPort}:${dstIP}:${dstPort}`;
}
const HTTP_METHODS = [
    "GET",
    "POST",
    "PUT",
    "PATCH",
    "DELETE",
    "HEAD",
    "OPTIONS",
    "CONNECT",
    "TRACE",
];
// DHCP Constants
const DHCP_SERVER_PORT = 67;
const DHCP_CLIENT_PORT = 68;
const DHCP_MAGIC_COOKIE = 0x63825363;
// DHCP Message Types
const DHCP_DISCOVER = 1;
const DHCP_OFFER = 2;
const DHCP_REQUEST = 3;
const DHCP_ACK = 5;
// DHCP Options
const DHCP_OPT_SUBNET_MASK = 1;
const DHCP_OPT_ROUTER = 3;
const DHCP_OPT_DNS = 6;
const DHCP_OPT_LEASE_TIME = 51;
const DHCP_OPT_MSG_TYPE = 53;
const DHCP_OPT_SERVER_ID = 54;
const DHCP_OPT_END = 255;
function normalizeDnsServers(servers) {
    const candidates = normalizeIpv4Servers(servers)
        // Guest resolvers must be reachable over the virtual NIC; loopback resolvers are not.
        .filter((server) => !server.startsWith("127."))
        .filter((server) => server !== "0.0.0.0" && server !== "255.255.255.255");
    return candidates.length > 0 ? candidates : ["8.8.8.8"];
}
export class NetworkStack extends EventEmitter {
    gatewayIP;
    vmIP;
    gatewayMac;
    vmMac;
    dnsServers;
    callbacks;
    allowTcpFlow;
    sshPorts;
    natTable = new Map();
    MAX_FLOW_SNIFF = 8 * 1024;
    rxBuffer = Buffer.alloc(0);
    txQueueHigh = [];
    txQueueLow = [];
    txQueueSize = 0;
    txQueueHighSize = 0;
    TX_QUEUE_MAX_BYTES;
    TX_BUFFER_HIGH_WATER = 512 * 1024;
    TX_BUFFER_LOW_WATER = 128 * 1024;
    TCP_MAX_IN_FLIGHT_BYTES = 48 * 1024;
    TCP_MAX_BURST_BYTES = 16 * 1024;
    txQueuePaused = new Set();
    txFlowPaused = new Set();
    constructor(options) {
        super();
        this.gatewayIP = options.gatewayIP ?? "192.168.127.1";
        this.vmIP = options.vmIP ?? "192.168.127.3";
        this.gatewayMac =
            options.gatewayMac ?? Buffer.from([0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xdd]);
        this.vmMac =
            options.vmMac ?? Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
        this.dnsServers = normalizeDnsServers(options.dnsServers);
        this.callbacks = options.callbacks;
        this.allowTcpFlow = options.allowTcpFlow ?? (() => true);
        this.sshPorts = new Set((options.sshPorts && options.sshPorts.length > 0
            ? options.sshPorts
            : [22]).filter((p) => Number.isInteger(p) && p > 0 && p <= 65535));
        this.TX_QUEUE_MAX_BYTES = options.txQueueMaxBytes ?? 8 * 1024 * 1024;
    }
    reset() {
        this.natTable.clear();
        this.rxBuffer = Buffer.alloc(0);
        this.txQueueHigh = [];
        this.txQueueLow = [];
        this.txQueueSize = 0;
        this.txQueueHighSize = 0;
        this.txQueuePaused.clear();
        this.txFlowPaused.clear();
    }
    hasPendingData() {
        return this.txQueueSize > 0;
    }
    // Called when QEMU writes data to the network interface
    writeToNetwork(data) {
        this.rxBuffer = Buffer.concat([this.rxBuffer, data]);
        while (this.rxBuffer.length >= 4) {
            const frameLen = this.rxBuffer.readUInt32BE(0);
            if (this.rxBuffer.length < 4 + frameLen)
                break;
            const frame = this.rxBuffer.subarray(4, 4 + frameLen);
            this.receive(frame);
            this.rxBuffer = this.rxBuffer.subarray(4 + frameLen);
        }
    }
    // Called when QEMU wants to read data from the network interface
    readFromNetwork(maxLen) {
        if (this.txQueueSize === 0)
            return null;
        let remaining = maxLen;
        let total = 0;
        const chunks = [];
        while (remaining > 0 && this.txQueueSize > 0) {
            const useHigh = this.txQueueHigh.length > 0;
            const queue = useHigh ? this.txQueueHigh : this.txQueueLow;
            const head = queue[0];
            const consumed = Math.min(head.length, remaining);
            chunks.push(consumed === head.length ? head : head.subarray(0, consumed));
            if (consumed === head.length) {
                queue.shift();
            }
            else {
                queue[0] = head.subarray(consumed);
            }
            remaining -= consumed;
            total += consumed;
            this.txQueueSize -= consumed;
            if (useHigh) {
                this.txQueueHighSize -= consumed;
            }
        }
        if (this.txQueueHighSize < this.TX_BUFFER_LOW_WATER &&
            this.txQueuePaused.size > 0) {
            for (const key of this.txQueuePaused) {
                if (!this.txFlowPaused.has(key)) {
                    this.callbacks.onTcpResume({ key });
                }
            }
            this.txQueuePaused.clear();
        }
        if (chunks.length === 1 && chunks[0].length === total) {
            return chunks[0];
        }
        return Buffer.concat(chunks, total);
    }
    send(payload, proto) {
        if (!this.vmMac)
            return;
        const frame = Buffer.alloc(14 + payload.length);
        this.vmMac.copy(frame, 0);
        this.gatewayMac.copy(frame, 6);
        frame.writeUInt16BE(proto, 12);
        payload.copy(frame, 14);
        const packet = Buffer.alloc(4 + frame.length);
        packet.writeUInt32BE(frame.length, 0);
        frame.copy(packet, 4);
        const queued = this.enqueueTx(packet, this.classifyTxPriority(proto, payload));
        if (!queued && proto === ETH_P_IP) {
            this.teardownDroppedTcpSession(payload);
        }
    }
    sendBroadcast(payload, proto) {
        const broadcastMac = Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
        const frame = Buffer.alloc(14 + payload.length);
        broadcastMac.copy(frame, 0);
        this.gatewayMac.copy(frame, 6);
        frame.writeUInt16BE(proto, 12);
        payload.copy(frame, 14);
        const packet = Buffer.alloc(4 + frame.length);
        packet.writeUInt32BE(frame.length, 0);
        frame.copy(packet, 4);
        this.enqueueTx(packet, this.classifyTxPriority(proto, payload));
    }
    classifyTxPriority(etherType, payload) {
        // ARP and DHCP are required for basic networking to work; keep them high priority.
        if (etherType === ETH_P_ARP)
            return "high";
        if (etherType !== ETH_P_IP)
            return "low";
        // IPv4 header is at least 20 bytes.
        if (payload.length < 20)
            return "low";
        const version = payload[0] >> 4;
        if (version !== 4)
            return "low";
        const ipProto = payload[9];
        if (ipProto === IP_PROTO_TCP)
            return "high";
        return "low";
    }
    /**
     * Enqueues a QEMU-framed ethernet packet for host->guest delivery.
     *
     * Emits:
     * - `"network-activity"` when something is queued
     * - `"tx-drop"` with {@link TxDropInfo} when a packet is dropped/evicted due to queue limits
     */
    enqueueTx(packet, priority) {
        if (packet.length === 0)
            return true;
        if (packet.length > this.TX_QUEUE_MAX_BYTES) {
            const info = {
                priority,
                bytes: packet.length,
                reason: "packet-too-large",
            };
            this.emit("tx-drop", info);
            return false;
        }
        // Enforce a hard cap to avoid unbounded memory growth if QEMU stops draining.
        if (this.txQueueSize + packet.length > this.TX_QUEUE_MAX_BYTES) {
            if (priority === "low") {
                const info = {
                    priority,
                    bytes: packet.length,
                    reason: "queue-full",
                };
                this.emit("tx-drop", info);
                return false;
            }
            // For high-priority packets (TCP/ARP), evict low-priority packets first.
            let evictedBytes = 0;
            while (this.txQueueLow.length > 0 &&
                this.txQueueSize + packet.length > this.TX_QUEUE_MAX_BYTES) {
                const evicted = this.txQueueLow.shift();
                evictedBytes += evicted.length;
                this.txQueueSize -= evicted.length;
            }
            if (evictedBytes > 0) {
                const info = {
                    priority: "low",
                    bytes: evictedBytes,
                    reason: "evicted",
                };
                this.emit("tx-drop", info);
            }
            if (this.txQueueSize + packet.length > this.TX_QUEUE_MAX_BYTES) {
                const info = {
                    priority,
                    bytes: packet.length,
                    reason: "queue-full",
                    evictedBytes,
                };
                this.emit("tx-drop", info);
                return false;
            }
        }
        if (priority === "high") {
            this.txQueueHigh.push(packet);
            this.txQueueHighSize += packet.length;
        }
        else {
            this.txQueueLow.push(packet);
        }
        this.txQueueSize += packet.length;
        this.emit("network-activity");
        return true;
    }
    receive(frame) {
        try {
            if (frame.length < 14)
                return;
            const etherType = frame.readUInt16BE(12);
            const payload = frame.subarray(14);
            const srcMac = frame.subarray(6, 12);
            if (!this.vmMac) {
                this.vmMac = Buffer.from(srcMac);
            }
            if (etherType === ETH_P_ARP) {
                this.handleARP(payload);
            }
            else if (etherType === ETH_P_IP) {
                this.handleIP(payload);
            }
        }
        catch (err) {
            this.emit("error", err);
        }
    }
    handleARP(packet) {
        const op = packet.readUInt16BE(6);
        if (op === 1) {
            const targetIP = packet.subarray(24, 28);
            const targetIPStr = targetIP.join(".");
            if (targetIPStr === this.gatewayIP) {
                const reply = Buffer.alloc(28);
                packet.copy(reply, 0, 0, 8);
                reply.writeUInt16BE(2, 6);
                this.gatewayMac.copy(reply, 8);
                targetIP.copy(reply, 14);
                packet.subarray(8, 14).copy(reply, 18);
                packet.subarray(14, 18).copy(reply, 24);
                this.send(reply, ETH_P_ARP);
            }
        }
    }
    handleIP(packet) {
        const version = packet[0] >> 4;
        if (version !== 4)
            return;
        const headerLen = (packet[0] & 0x0f) * 4;
        const totalLen = packet.readUInt16BE(2);
        const protocol = packet[9];
        const srcIP = packet.subarray(12, 16);
        const dstIP = packet.subarray(16, 20);
        const data = packet.subarray(headerLen, totalLen);
        if (protocol === IP_PROTO_ICMP) {
            this.handleICMP(data, srcIP, dstIP);
        }
        else if (protocol === IP_PROTO_TCP) {
            this.handleTCP(data, srcIP, dstIP);
        }
        else if (protocol === IP_PROTO_UDP) {
            this.handleUDP(data, srcIP, dstIP);
        }
    }
    calculateChecksum(buf) {
        let sum = 0;
        for (let i = 0; i < buf.length - 1; i += 2) {
            sum += buf.readUInt16BE(i);
        }
        if (buf.length % 2 === 1) {
            sum += buf[buf.length - 1] << 8;
        }
        while (sum >> 16) {
            sum = (sum & 0xffff) + (sum >> 16);
        }
        return ~sum & 0xffff;
    }
    calculateUdpChecksum(payload, srcIP, dstIP) {
        const pseudo = Buffer.alloc(12);
        srcIP.copy(pseudo, 0);
        dstIP.copy(pseudo, 4);
        pseudo[8] = 0;
        pseudo[9] = IP_PROTO_UDP;
        pseudo.writeUInt16BE(payload.length, 10);
        const checksumData = Buffer.concat([pseudo, payload]);
        const checksum = this.calculateChecksum(checksumData);
        return checksum === 0 ? 0xffff : checksum;
    }
    handleICMP(data, srcIP, dstIP) {
        const type = data[0];
        if (type !== 8)
            return;
        const wantsDebug = this.listenerCount("icmp") > 0;
        const canParse = data.length >= 8;
        const rxTime = wantsDebug ? performance.now() : 0;
        const id = wantsDebug && canParse ? data.readUInt16BE(4) : 0;
        const seq = wantsDebug && canParse ? data.readUInt16BE(6) : 0;
        const reply = Buffer.alloc(data.length);
        data.copy(reply);
        reply[0] = 0;
        reply[2] = 0;
        reply[3] = 0;
        const ck = this.calculateChecksum(reply);
        reply.writeUInt16BE(ck, 2);
        if (wantsDebug && canParse) {
            const replyTime = performance.now();
            this.emit("icmp", {
                srcIP: srcIP.join("."),
                dstIP: dstIP.join("."),
                id,
                seq,
                rxTime,
                replyTime,
                size: data.length,
            });
        }
        this.sendIP(reply, IP_PROTO_ICMP, dstIP, srcIP);
    }
    sendIP(payload, protocol, srcIP, dstIP) {
        const header = Buffer.alloc(20);
        header[0] = 0x45;
        header[1] = 0;
        header.writeUInt16BE(20 + payload.length, 2);
        header.writeUInt16BE(0, 4);
        header.writeUInt16BE(0, 6);
        header[8] = 64;
        header[9] = protocol;
        srcIP.copy(header, 12);
        dstIP.copy(header, 16);
        header.writeUInt16BE(this.calculateChecksum(header), 10);
        const packet = Buffer.concat([header, payload]);
        if (dstIP[0] === 255 &&
            dstIP[1] === 255 &&
            dstIP[2] === 255 &&
            dstIP[3] === 255) {
            this.sendBroadcast(packet, ETH_P_IP);
        }
        else {
            this.send(packet, ETH_P_IP);
        }
    }
    looksLikeTlsClientHello(data) {
        if (data.length < 5)
            return false;
        if (data[0] !== 0x16)
            return false; // handshake
        if (data[1] !== 0x03)
            return false; // TLS major version
        return data[2] >= 0x00 && data[2] <= 0x03;
    }
    matchHttpMethodPrefix(data) {
        const snippet = data.toString("ascii", 0, Math.min(data.length, 16));
        for (const method of HTTP_METHODS) {
            if (snippet.startsWith(`${method} `)) {
                return { status: "match", method };
            }
            if (method.startsWith(snippet)) {
                return { status: "partial" };
            }
        }
        return { status: "none" };
    }
    parseHttpRequestLine(data) {
        const lineEnd = data.indexOf("\r\n");
        if (lineEnd === -1)
            return null;
        const line = data.subarray(0, lineEnd).toString("ascii");
        const [method, target, version] = line.split(" ");
        if (!method || !target || !version)
            return null;
        if (!version.startsWith("HTTP/"))
            return null;
        if (!HTTP_METHODS.includes(method))
            return null;
        return { method, target, version };
    }
    classifyTcpFlow(data, dstPort) {
        if (data.length === 0) {
            return { status: "need-more" };
        }
        if (this.looksLikeTlsClientHello(data)) {
            return { status: "tls" };
        }
        const prefix = this.matchHttpMethodPrefix(data);
        if (prefix.status === "match") {
            const requestLine = this.parseHttpRequestLine(data);
            if (!requestLine) {
                return { status: "need-more" };
            }
            return {
                status: "http",
                method: requestLine.method,
                isConnect: requestLine.method === "CONNECT",
            };
        }
        if (prefix.status === "partial") {
            return { status: "need-more" };
        }
        if (this.sshPorts.has(dstPort)) {
            const snippet = data.toString("ascii", 0, Math.min(data.length, 4));
            if (snippet === "SSH-") {
                return { status: "ssh" };
            }
            if ("SSH-".startsWith(snippet)) {
                return { status: "need-more" };
            }
        }
        if (data.length < 4) {
            return { status: "need-more" };
        }
        return { status: "deny", reason: "unknown-protocol" };
    }
    rejectTcpFlow(session, key, ack, reason) {
        this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, ack, 0x14);
        this.destroyTcpSession(key);
        this.emit("tcp-deny", { key, reason });
    }
    handleTCP(segment, srcIP, dstIP) {
        const srcPort = segment.readUInt16BE(0);
        const dstPort = segment.readUInt16BE(2);
        const seq = segment.readUInt32BE(4);
        const ack = segment.readUInt32BE(8);
        const offset = (segment[12] >> 4) * 4;
        const flags = segment[13];
        const window = segment.readUInt16BE(14);
        const payload = segment.subarray(offset);
        const SYN = (flags & 0x02) !== 0;
        const FIN = (flags & 0x01) !== 0;
        const RST = (flags & 0x04) !== 0;
        const key = makeTcpNatKey(srcIP.join("."), srcPort, dstIP.join("."), dstPort);
        let session = this.natTable.get(key);
        if (RST) {
            if (session) {
                this.destroyTcpSession(key);
            }
            return;
        }
        if (SYN && !session) {
            session = {
                state: "SYN_SENT",
                srcIP: Buffer.from(srcIP),
                srcPort,
                dstIP: Buffer.from(dstIP),
                dstPort,
                vmSeq: seq,
                vmAck: ack,
                mySeq: Math.floor(Math.random() * 0x0fffffff),
                myAck: seq + 1,
                peerWindow: window,
                pendingOutbound: Buffer.alloc(0),
                endPending: false,
                flowProtocol: null,
                allowRawTcp: false,
                pendingData: Buffer.alloc(0),
                drainScheduled: false,
            };
            this.natTable.set(key, session);
            // Keep callback typing backward-compatible (`void`) while allowing callers
            // to optionally return a connect decision.
            const connectDecision = this.callbacks.onTcpConnect({
                key,
                dstIP: dstIP.join("."),
                dstPort,
                srcIP: srcIP.join("."),
                srcPort,
            });
            session.allowRawTcp = Boolean(connectDecision?.allowRawTcp);
            return;
        }
        if (!session) {
            if (!SYN) {
                this.sendTCP(srcIP, srcPort, dstIP, dstPort, 0, seq + (payload.length || 1), 0x04);
            }
            return;
        }
        const prevPeerWindow = session.peerWindow;
        session.peerWindow = window;
        let shouldDrainOutbound = false;
        if (ack > session.vmAck && ack <= session.mySeq) {
            session.vmAck = ack;
            shouldDrainOutbound = true;
        }
        if (session.pendingOutbound.length > 0 && window > prevPeerWindow) {
            shouldDrainOutbound = true;
        }
        if (shouldDrainOutbound) {
            this.drainOutboundTcp(key, session);
        }
        if (payload.length > 0) {
            // Basic TCP reassembly / retransmit handling:
            // - only accept in-order bytes starting at `session.myAck`
            // - ignore pure retransmits (seq+len <= myAck)
            // - ignore out-of-order segments (seq > myAck)
            //
            // This matters for SSH in particular: duplicating bytes can desync the
            // protocol and trigger errors like "Bad packet length".
            const expectedSeq = session.myAck;
            if (seq > expectedSeq) {
                // Out-of-order: re-ACK what we've already seen.
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
                return;
            }
            const skip = Math.max(0, expectedSeq - seq);
            if (skip >= payload.length) {
                // Pure retransmit (or segment contains only already-acked bytes)
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
                if (!FIN) {
                    return;
                }
            }
            const newPayload = payload.subarray(skip);
            let sendBuffer = null;
            const nextAck = expectedSeq + newPayload.length;
            if (!session.flowProtocol) {
                session.pendingData = Buffer.concat([session.pendingData, newPayload]);
                if (session.allowRawTcp) {
                    session.flowProtocol = "tcp";
                }
                else {
                    const classification = this.classifyTcpFlow(session.pendingData, session.dstPort);
                    if (classification.status === "need-more") {
                        if (session.pendingData.length >= this.MAX_FLOW_SNIFF) {
                            this.rejectTcpFlow(session, key, nextAck, "sniff-limit-exceeded");
                            return;
                        }
                    }
                    else if (classification.status === "deny") {
                        this.rejectTcpFlow(session, key, nextAck, classification.reason);
                        return;
                    }
                    else if (classification.status === "http") {
                        if (classification.isConnect) {
                            this.rejectTcpFlow(session, key, nextAck, "connect-not-allowed");
                            return;
                        }
                        session.flowProtocol = "http";
                        session.httpMethod = classification.method;
                    }
                    else if (classification.status === "tls") {
                        session.flowProtocol = "tls";
                    }
                    else if (classification.status === "ssh") {
                        session.flowProtocol = "ssh";
                    }
                }
                if (session.flowProtocol) {
                    const allowed = this.allowTcpFlow({
                        key,
                        srcIP: session.srcIP.join("."),
                        srcPort: session.srcPort,
                        dstIP: session.dstIP.join("."),
                        dstPort: session.dstPort,
                        protocol: session.flowProtocol,
                        httpMethod: session.httpMethod,
                    });
                    if (!allowed) {
                        this.rejectTcpFlow(session, key, nextAck, "policy-deny");
                        return;
                    }
                    sendBuffer = session.pendingData;
                    session.pendingData = Buffer.alloc(0);
                }
            }
            else {
                sendBuffer = newPayload;
            }
            session.vmSeq = nextAck;
            session.myAck = nextAck;
            if (sendBuffer && sendBuffer.length > 0) {
                this.callbacks.onTcpSend({ key, data: Buffer.from(sendBuffer) });
            }
            // ACK payload immediately unless FIN is also set (FIN handling below will ACK).
            if (!FIN) {
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
            }
        }
        if (FIN) {
            // FIN consumes one sequence number, so only accept it once the sequence
            // space up to the FIN is fully received.
            const finSeq = seq + payload.length;
            if (finSeq > session.myAck) {
                // Out-of-order FIN: keep ACKing the last in-order byte.
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
                return;
            }
            if (finSeq < session.myAck) {
                // Duplicate FIN (already acked)
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
                return;
            }
            // finSeq === session.myAck
            this.callbacks.onTcpClose({ key, destroy: false });
            session.myAck++;
            this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x10);
            if (session.state === "CLOSED_BY_REMOTE" ||
                session.state === "FIN_WAIT") {
                this.clearPauseState(key);
                this.natTable.delete(key);
            }
            else {
                session.state = "FIN_SENT";
            }
        }
    }
    sendTCP(dstIP, dstPort, srcIP, srcPort, seq, ack, flags, payload = Buffer.alloc(0)) {
        const header = Buffer.alloc(20);
        header.writeUInt16BE(srcPort, 0);
        header.writeUInt16BE(dstPort, 2);
        header.writeUInt32BE(seq, 4);
        header.writeUInt32BE(ack, 8);
        header[12] = 0x50;
        header[13] = flags;
        header.writeUInt16BE(65535, 14);
        header.writeUInt16BE(0, 16);
        header.writeUInt16BE(0, 18);
        const pseudo = Buffer.alloc(12);
        srcIP.copy(pseudo, 0);
        dstIP.copy(pseudo, 4);
        pseudo[8] = 0;
        pseudo[9] = IP_PROTO_TCP;
        pseudo.writeUInt16BE(20 + payload.length, 10);
        const ckData = Buffer.concat([pseudo, header, payload]);
        const ck = this.calculateChecksum(ckData);
        header.writeUInt16BE(ck, 16);
        this.sendIP(Buffer.concat([header, payload]), IP_PROTO_TCP, srcIP, dstIP);
    }
    handleUDP(segment, srcIP, dstIP) {
        const srcPort = segment.readUInt16BE(0);
        const dstPort = segment.readUInt16BE(2);
        const payload = segment.subarray(8);
        if (srcPort === DHCP_CLIENT_PORT && dstPort === DHCP_SERVER_PORT) {
            this.handleDHCP(payload);
            return;
        }
        const key = `UDP:${srcIP.join(".")}:${srcPort}:${dstIP.join(".")}:${dstPort}`;
        this.callbacks.onUdpSend({
            key,
            dstIP: dstIP.join("."),
            dstPort,
            srcIP: srcIP.join("."),
            srcPort,
            payload: Buffer.from(payload),
        });
    }
    handleDHCP(data) {
        if (data.length < 240)
            return;
        const op = data[0];
        if (op !== 1)
            return;
        const xid = data.readUInt32BE(4);
        const flags = data.readUInt16BE(10);
        const chaddr = data.subarray(28, 28 + 16);
        const magic = data.readUInt32BE(236);
        if (magic !== DHCP_MAGIC_COOKIE)
            return;
        let msgType = 0;
        let i = 240;
        while (i < data.length) {
            const opt = data[i];
            if (opt === DHCP_OPT_END)
                break;
            if (opt === 0) {
                i += 1;
                continue;
            }
            const len = data[i + 1];
            const optData = data.subarray(i + 2, i + 2 + len);
            if (opt === DHCP_OPT_MSG_TYPE && len >= 1) {
                msgType = optData[0];
            }
            i += 2 + len;
        }
        if (msgType === DHCP_DISCOVER) {
            this.sendDHCPReply(DHCP_OFFER, xid, chaddr, flags);
        }
        else if (msgType === DHCP_REQUEST) {
            this.sendDHCPReply(DHCP_ACK, xid, chaddr, flags);
        }
    }
    sendDHCPReply(msgType, xid, chaddr, flags) {
        const reply = Buffer.alloc(300);
        reply[0] = 2;
        reply[1] = 1;
        reply[2] = 6;
        reply[3] = 0;
        reply.writeUInt32BE(xid, 4);
        reply.writeUInt16BE(0, 8);
        reply.writeUInt16BE(flags, 10);
        const vmIPParts = this.vmIP.split(".").map(Number);
        reply[16] = vmIPParts[0];
        reply[17] = vmIPParts[1];
        reply[18] = vmIPParts[2];
        reply[19] = vmIPParts[3];
        const gwIPParts = this.gatewayIP.split(".").map(Number);
        reply[20] = gwIPParts[0];
        reply[21] = gwIPParts[1];
        reply[22] = gwIPParts[2];
        reply[23] = gwIPParts[3];
        chaddr.copy(reply, 28);
        reply.writeUInt32BE(DHCP_MAGIC_COOKIE, 236);
        let optOffset = 240;
        reply[optOffset++] = DHCP_OPT_MSG_TYPE;
        reply[optOffset++] = 1;
        reply[optOffset++] = msgType;
        reply[optOffset++] = DHCP_OPT_SERVER_ID;
        reply[optOffset++] = 4;
        reply[optOffset++] = gwIPParts[0];
        reply[optOffset++] = gwIPParts[1];
        reply[optOffset++] = gwIPParts[2];
        reply[optOffset++] = gwIPParts[3];
        reply[optOffset++] = DHCP_OPT_LEASE_TIME;
        reply[optOffset++] = 4;
        reply.writeUInt32BE(86400, optOffset);
        optOffset += 4;
        reply[optOffset++] = DHCP_OPT_SUBNET_MASK;
        reply[optOffset++] = 4;
        reply[optOffset++] = 255;
        reply[optOffset++] = 255;
        reply[optOffset++] = 255;
        reply[optOffset++] = 0;
        reply[optOffset++] = DHCP_OPT_ROUTER;
        reply[optOffset++] = 4;
        reply[optOffset++] = gwIPParts[0];
        reply[optOffset++] = gwIPParts[1];
        reply[optOffset++] = gwIPParts[2];
        reply[optOffset++] = gwIPParts[3];
        const dnsServers = this.dnsServers.length > 0 ? this.dnsServers : ["8.8.8.8"];
        const dnsEntries = dnsServers
            .map((server) => server.split(".").map(Number))
            .filter((parts) => parts.length === 4 &&
            parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255));
        if (dnsEntries.length === 0) {
            dnsEntries.push([8, 8, 8, 8]);
        }
        reply[optOffset++] = DHCP_OPT_DNS;
        reply[optOffset++] = dnsEntries.length * 4;
        for (const parts of dnsEntries) {
            reply[optOffset++] = parts[0];
            reply[optOffset++] = parts[1];
            reply[optOffset++] = parts[2];
            reply[optOffset++] = parts[3];
        }
        reply[optOffset++] = 28; // DHCP_OPT_BROADCAST
        reply[optOffset++] = 4;
        reply[optOffset++] = vmIPParts[0];
        reply[optOffset++] = vmIPParts[1];
        reply[optOffset++] = vmIPParts[2];
        reply[optOffset++] = 255;
        reply[optOffset++] = DHCP_OPT_END;
        const dhcpLen = 300;
        const udpLen = 8 + dhcpLen;
        const udpHeader = Buffer.alloc(8);
        udpHeader.writeUInt16BE(DHCP_SERVER_PORT, 0);
        udpHeader.writeUInt16BE(DHCP_CLIENT_PORT, 2);
        udpHeader.writeUInt16BE(udpLen, 4);
        udpHeader.writeUInt16BE(0, 6);
        const udpPayload = Buffer.concat([udpHeader, reply]);
        const srcIP = Buffer.from(gwIPParts);
        const dstIP = Buffer.from([255, 255, 255, 255]);
        const ipHeader = Buffer.alloc(20);
        ipHeader[0] = 0x45;
        ipHeader[1] = 0;
        ipHeader.writeUInt16BE(20 + udpPayload.length, 2);
        ipHeader.writeUInt16BE(0, 4);
        ipHeader.writeUInt16BE(0, 6);
        ipHeader[8] = 64;
        ipHeader[9] = IP_PROTO_UDP;
        srcIP.copy(ipHeader, 12);
        dstIP.copy(ipHeader, 16);
        ipHeader.writeUInt16BE(this.calculateChecksum(ipHeader), 10);
        const ipPacket = Buffer.concat([ipHeader, udpPayload]);
        const dstMac = flags & 0x8000
            ? Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
            : chaddr.subarray(0, 6);
        const frame = Buffer.alloc(14 + ipPacket.length);
        dstMac.copy(frame, 0);
        this.gatewayMac.copy(frame, 6);
        frame.writeUInt16BE(ETH_P_IP, 12);
        ipPacket.copy(frame, 14);
        const packet = Buffer.alloc(4 + frame.length);
        packet.writeUInt32BE(frame.length, 0);
        frame.copy(packet, 4);
        // DHCP is required for connectivity; treat it as high priority even though it's UDP.
        this.enqueueTx(packet, "high");
        this.emit("dhcp", msgType === DHCP_OFFER ? "OFFER" : "ACK", this.vmIP);
    }
    handleUdpResponse(message) {
        const { data, srcIP, srcPort, dstIP, dstPort } = message;
        const udpHeader = Buffer.alloc(8);
        udpHeader.writeUInt16BE(dstPort, 0);
        udpHeader.writeUInt16BE(srcPort, 2);
        udpHeader.writeUInt16BE(8 + data.length, 4);
        udpHeader.writeUInt16BE(0, 6);
        const dstIPBuf = Buffer.from(dstIP.split(".").map(Number));
        const srcIPBuf = Buffer.from(srcIP.split(".").map(Number));
        const payload = Buffer.concat([udpHeader, Buffer.from(data)]);
        const checksum = this.calculateUdpChecksum(payload, dstIPBuf, srcIPBuf);
        udpHeader.writeUInt16BE(checksum, 6);
        const withChecksum = Buffer.concat([udpHeader, Buffer.from(data)]);
        this.sendIP(withChecksum, IP_PROTO_UDP, dstIPBuf, srcIPBuf);
    }
    clearPauseState(key) {
        this.txQueuePaused.delete(key);
        this.txFlowPaused.delete(key);
    }
    destroyTcpSession(key) {
        this.callbacks.onTcpClose({ key, destroy: true });
        this.clearPauseState(key);
        this.natTable.delete(key);
    }
    /**
     * Tears down the NAT flow when a host->guest TCP packet with payload is dropped
     *
     * We cannot retransmit dropped payload from this userspace stack, so keeping
     * the session open can deadlock on in-flight accounting. Pure control packets
     * (ACK/FIN/RST without payload) are left to normal TCP recovery.
     */
    teardownDroppedTcpSession(ipPayload) {
        if (ipPayload.length < 20)
            return;
        const version = ipPayload[0] >> 4;
        const ipProto = ipPayload[9];
        const headerLen = (ipPayload[0] & 0x0f) * 4;
        const totalLen = ipPayload.readUInt16BE(2);
        if (version !== 4 ||
            ipProto !== IP_PROTO_TCP ||
            headerLen < 20 ||
            totalLen < headerLen + 20 ||
            ipPayload.length < totalLen) {
            return;
        }
        const tcpStart = headerLen;
        const tcpHeaderLen = ((ipPayload[tcpStart + 12] >> 4) & 0x0f) * 4;
        if (tcpHeaderLen < 20) {
            return;
        }
        const tcpPayloadLen = totalLen - headerLen - tcpHeaderLen;
        if (tcpPayloadLen <= 0) {
            return;
        }
        const guestIP = ipPayload.subarray(16, 20).join(".");
        const guestPort = ipPayload.readUInt16BE(tcpStart + 2);
        const upstreamIP = ipPayload.subarray(12, 16).join(".");
        const upstreamPort = ipPayload.readUInt16BE(tcpStart);
        const key = makeTcpNatKey(guestIP, guestPort, upstreamIP, upstreamPort);
        if (this.natTable.has(key)) {
            this.destroyTcpSession(key);
        }
    }
    pauseFlow(key) {
        if (this.txFlowPaused.has(key)) {
            return;
        }
        this.txFlowPaused.add(key);
        this.callbacks.onTcpPause({ key });
    }
    maybeResumeFlow(key) {
        if (!this.txFlowPaused.has(key)) {
            return;
        }
        const session = this.natTable.get(key);
        if (!session || session.pendingOutbound.length === 0) {
            this.txFlowPaused.delete(key);
            if (!this.txQueuePaused.has(key)) {
                this.callbacks.onTcpResume({ key });
            }
            return;
        }
        const inFlight = Math.max(0, session.mySeq - session.vmAck);
        const maxInFlight = Math.max(0, Math.min(session.peerWindow, this.TCP_MAX_IN_FLIGHT_BYTES));
        if (inFlight < maxInFlight) {
            this.txFlowPaused.delete(key);
            if (!this.txQueuePaused.has(key)) {
                this.callbacks.onTcpResume({ key });
            }
        }
    }
    drainOutboundTcp(key, session) {
        const isLiveSession = () => this.natTable.get(key) === session;
        if (!isLiveSession()) {
            return;
        }
        if (session.pendingOutbound.length === 0 && !session.endPending) {
            this.maybeResumeFlow(key);
            return;
        }
        const MSS = 1460;
        let bytesBurstThisTick = 0;
        let inFlight = Math.max(0, session.mySeq - session.vmAck);
        const maxInFlight = Math.max(0, Math.min(session.peerWindow, this.TCP_MAX_IN_FLIGHT_BYTES));
        while (session.pendingOutbound.length > 0 &&
            inFlight < maxInFlight &&
            bytesBurstThisTick < this.TCP_MAX_BURST_BYTES) {
            const allowance = maxInFlight - inFlight;
            const burstAllowance = this.TCP_MAX_BURST_BYTES - bytesBurstThisTick;
            const chunkSize = Math.min(MSS, session.pendingOutbound.length, allowance, burstAllowance);
            if (chunkSize <= 0) {
                break;
            }
            const chunk = session.pendingOutbound.subarray(0, chunkSize);
            session.pendingOutbound = session.pendingOutbound.subarray(chunkSize);
            const flags = session.pendingOutbound.length === 0 ? 0x18 : 0x10;
            this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, flags, chunk);
            if (!isLiveSession()) {
                return;
            }
            session.mySeq += chunk.length;
            inFlight += chunk.length;
            bytesBurstThisTick += chunk.length;
        }
        if (!isLiveSession()) {
            return;
        }
        if (session.pendingOutbound.length > 0 &&
            inFlight < maxInFlight &&
            bytesBurstThisTick >= this.TCP_MAX_BURST_BYTES) {
            if (!session.drainScheduled) {
                session.drainScheduled = true;
                setImmediate(() => {
                    session.drainScheduled = false;
                    if (this.natTable.get(key) === session) {
                        this.drainOutboundTcp(key, session);
                    }
                });
            }
            return;
        }
        if (session.pendingOutbound.length === 0 && session.endPending) {
            if (inFlight < maxInFlight) {
                this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x11);
                if (!isLiveSession()) {
                    return;
                }
                session.mySeq++;
                session.state = "FIN_WAIT";
                session.endPending = false;
                inFlight += 1;
            }
        }
        if (!isLiveSession()) {
            return;
        }
        if (session.pendingOutbound.length > 0 || session.endPending) {
            this.pauseFlow(key);
        }
        else {
            this.maybeResumeFlow(key);
        }
    }
    handleTcpConnected(message) {
        const session = this.natTable.get(message.key);
        if (!session)
            return;
        session.state = "ESTABLISHED";
        this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x12);
        session.mySeq++;
    }
    handleTcpData(message) {
        const session = this.natTable.get(message.key);
        if (!session)
            return;
        const payload = Buffer.from(message.data);
        if (payload.length > 0) {
            session.pendingOutbound =
                session.pendingOutbound.length === 0
                    ? payload
                    : Buffer.concat([session.pendingOutbound, payload]);
            this.drainOutboundTcp(message.key, session);
        }
        if (this.txQueueHighSize > this.TX_BUFFER_HIGH_WATER &&
            !this.txQueuePaused.has(message.key)) {
            this.txQueuePaused.add(message.key);
            this.callbacks.onTcpPause({ key: message.key });
        }
    }
    handleTcpEnd(message) {
        const session = this.natTable.get(message.key);
        if (!session)
            return;
        session.endPending = true;
        this.drainOutboundTcp(message.key, session);
    }
    handleTcpError(message) {
        const session = this.natTable.get(message.key);
        if (!session)
            return;
        this.sendTCP(session.srcIP, session.srcPort, session.dstIP, session.dstPort, session.mySeq, session.myAck, 0x04);
        this.clearPauseState(message.key);
        this.natTable.delete(message.key);
    }
    handleTcpClosed(message) {
        const session = this.natTable.get(message.key);
        if (session) {
            session.state = "CLOSED_BY_REMOTE";
        }
    }
}
//# sourceMappingURL=network-stack.js.map