Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion host/src/qemu/network-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ function seqLe(a: number, b: number): boolean {
return seqDistance(a, b) <= 0;
}

/** Receive window advertised on every segment in `bytes` (no window scaling) */
const TCP_RECEIVE_WINDOW = 65535;

const HTTP_METHODS = [
"GET",
"POST",
Expand Down Expand Up @@ -840,6 +843,23 @@ export class NetworkStack extends EventEmitter {
this.drainOutboundTcp(key, session);
}

if (payload.length === 0 && !SYN && !FIN) {
// Keepalive probes (seq = RCV.NXT - 1) need an ACK (RFC 9293 搂3.10.7.4).
const offset = seqDistance(seq, session.myAck);
if (offset < 0 || offset >= TCP_RECEIVE_WINDOW) {
this.sendTCP(
session.srcIP,
session.srcPort,
session.dstIP,
session.dstPort,
session.mySeq,
session.myAck,
0x10,
);
return;
}
}

if (payload.length > 0) {
// Basic TCP reassembly / retransmit handling:
// - only accept in-order bytes starting at `session.myAck`
Expand Down Expand Up @@ -1035,7 +1055,7 @@ export class NetworkStack extends EventEmitter {
header.writeUInt32BE(wrapSeq(ack), 8);
header[12] = 0x50;
header[13] = flags;
header.writeUInt16BE(65535, 14);
header.writeUInt16BE(TCP_RECEIVE_WINDOW, 14);
header.writeUInt16BE(0, 16);
header.writeUInt16BE(0, 18);

Expand Down
84 changes: 84 additions & 0 deletions host/test/network-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1917,3 +1917,87 @@ test("network-stack: outbound TCP keeps flowing when mySeq wraps past 2^32", ()
stack.handleTcpData({ key, data: Buffer.alloc(4 * 1024, 0x43) });
assert.equal(countTcpPayloadBytes(drainAllQemuTx(stack)), 4 * 1024);
});

test("network-stack: zero-length segment outside the receive window is answered with an ACK", () => {
const gatewayMac = mac([0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xdd]);
const vmMac = mac([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);

let key = "";
const stack = new NetworkStack({
gatewayMac,
vmMac,
dnsServers: ["8.8.8.8"],
allowTcpFlow: () => true,
callbacks: {
onUdpSend: () => {},
onTcpConnect: (m) => {
key = m.key;
return { allowRawTcp: true };
},
onTcpSend: () => {},
onTcpClose: () => {},
onTcpPause: () => {},
onTcpResume: () => {},
},
});

const srcIP = ip([192, 168, 127, 3]);
const dstIP = ip([198, 19, 0, 10]);
const srcPort = 40020;
const dstPort = 7000;

stack.handleTCP(
buildTcpSegment({ srcPort, dstPort, seq: 100, ack: 0, flags: 0x02 }),
srcIP,
dstIP,
);
stack.handleTcpConnected({ key });
drainAllQemuTx(stack);
const hostSeq = (stack as any).natTable.get(key).mySeq as number;

stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 101,
ack: hostSeq,
flags: 0x18,
payload: Buffer.from("hello"),
}),
srcIP,
dstIP,
);
drainAllQemuTx(stack);

const tcpOut = () =>
decodeFramesFromQemuData(drainAllQemuTx(stack))
.map(parseEthernet)
.filter((eth) => eth.etherType === 0x0800)
.map((eth) => parseIPv4(eth.payload))
.filter((ipOut) => ipOut.protocol === 6)
.map((ipOut) => ipOut.payload);

// An acceptable pure ACK gets no reply, or two stacks would ACK forever.
stack.handleTCP(
buildTcpSegment({ srcPort, dstPort, seq: 106, ack: hostSeq, flags: 0x10 }),
srcIP,
dstIP,
);
assert.equal(tcpOut().length, 0);

// Keepalive probe
stack.handleTCP(
buildTcpSegment({ srcPort, dstPort, seq: 105, ack: hostSeq, flags: 0x10 }),
srcIP,
dstIP,
);
const replies = tcpOut();
assert.equal(replies.length, 1, "expected exactly one ACK for the probe");
assert.equal(replies[0][13], 0x10, "reply must be a bare ACK");
assert.equal(
replies[0].readUInt32BE(4),
hostSeq,
"reply seq must be SND.NXT",
);
assert.equal(replies[0].readUInt32BE(8), 106, "reply ack must be RCV.NXT");
});