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
29 changes: 25 additions & 4 deletions host/src/qemu/network-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ type TcpSession = {
pendingOutbound: Buffer;
/** FIN pending until outbound payload is drained */
endPending: boolean;
/** whether the guest's FIN has been accepted */
guestFinReceived: boolean;
/** whether our FIN has been sent (it is the last sequence number) */
finSent: boolean;
flowProtocol: TcpFlowProtocol | null;
/** whether this flow can bypass protocol sniffing and run as raw tcp */
allowRawTcp: boolean;
Expand Down Expand Up @@ -790,6 +794,8 @@ export class NetworkStack extends EventEmitter {
peerWindow: window,
pendingOutbound: Buffer.alloc(0),
endPending: false,
guestFinReceived: false,
finSent: false,
flowProtocol: null,
allowRawTcp: false,
pendingData: Buffer.alloc(0),
Expand Down Expand Up @@ -832,6 +838,16 @@ export class NetworkStack extends EventEmitter {
if (seqGt(ack, session.vmAck) && seqLe(ack, session.mySeq)) {
session.vmAck = ack;
shouldDrainOutbound = true;
// LAST-ACK: tracked apart from `state`, which handleTcpClosed overwrites
if (
session.guestFinReceived &&
session.finSent &&
session.vmAck === session.mySeq
) {
this.clearPauseState(key);
this.natTable.delete(key);
return;
}
}
if (session.pendingOutbound.length > 0 && window > prevPeerWindow) {
shouldDrainOutbound = true;
Expand Down Expand Up @@ -995,6 +1011,7 @@ export class NetworkStack extends EventEmitter {
// finSeq === session.myAck
this.callbacks.onTcpClose({ key, destroy: false });
session.myAck = wrapSeq(session.myAck + 1);
session.guestFinReceived = true;

this.sendTCP(
session.srcIP,
Expand All @@ -1005,12 +1022,15 @@ export class NetworkStack extends EventEmitter {
session.myAck,
0x10,
);
if (
session.state === "CLOSED_BY_REMOTE" ||
session.state === "FIN_WAIT"
) {
if (session.state === "CLOSED_BY_REMOTE") {
this.clearPauseState(key);
this.natTable.delete(key);
} else if (session.state === "FIN_WAIT") {
// CLOSING: while our FIN is unacked, the ACK handler drops the session
if (session.finSent && session.vmAck === session.mySeq) {
this.clearPauseState(key);
this.natTable.delete(key);
}
} else {
session.state = "FIN_SENT";
}
Expand Down Expand Up @@ -1468,6 +1488,7 @@ export class NetworkStack extends EventEmitter {

session.mySeq = wrapSeq(session.mySeq + 1);
session.state = "FIN_WAIT";
session.finSent = true;
session.endPending = false;
inFlight += 1;
}
Expand Down
259 changes: 259 additions & 0 deletions host/test/network-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1917,3 +1917,262 @@ 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);
});

for (const remoteClosed of [false, true]) {
test(`network-stack: session is removed once the guest ACKs our FIN after closing first${remoteClosed ? " (host socket closed)" : ""}`, () => {
// A leaked session makes a later SYN on the same 4-tuple be ignored.
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"],
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 = 40030;
const dstPort = 7000;

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

stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 101,
ack: hostSeq,
flags: 0x11,
}),
srcIP,
dstIP,
);
// Upstream still replies after the guest's half-close
stack.handleTcpData({ key, data: Buffer.from("bye") });
stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 102,
ack: hostSeq + 3,
flags: 0x10,
}),
srcIP,
dstIP,
);
assert.ok((stack as any).natTable.has(key));

stack.handleTcpData({ key, data: Buffer.from("!") });
stack.handleTcpEnd({ key });
if (remoteClosed) {
stack.handleTcpClosed({ key });
}
const fin = decodeFramesFromQemuData(drainAllQemuTx(stack))
.map(parseEthernet)
.filter((eth) => eth.etherType === 0x0800)
.map((eth) => parseIPv4(eth.payload))
.filter((ipOut) => ipOut.protocol === 6)
.map((ipOut) => ipOut.payload)
.find((tcp) => (tcp[13] & 0x01) !== 0);
assert.ok(fin, "expected an outbound FIN segment");
assert.equal(fin.readUInt32BE(4), hostSeq + 4);

// Data is acknowledged, our FIN is not yet
stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 102,
ack: hostSeq + 4,
flags: 0x10,
}),
srcIP,
dstIP,
);
assert.ok((stack as any).natTable.has(key));

stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 102,
ack: hostSeq + 5,
flags: 0x10,
}),
srcIP,
dstIP,
);
assert.equal((stack as any).natTable.has(key), false);
});
}

test("network-stack: session closed by upstream first stays until the guest's FIN", () => {
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"],
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 = 40031;
const dstPort = 7000;

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

stack.handleTcpEnd({ key });
stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 101,
ack: hostSeq + 1,
flags: 0x10,
}),
srcIP,
dstIP,
);
assert.ok((stack as any).natTable.has(key));

stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 101,
ack: hostSeq + 1,
flags: 0x11,
}),
srcIP,
dstIP,
);
assert.equal((stack as any).natTable.has(key), false);
});

for (const finInOnTcpClose of [false, true]) {
test(`network-stack: simultaneous close keeps the session until the guest ACKs our FIN${finInOnTcpClose ? " (FIN sent from onTcpClose)" : ""}`, () => {
const gatewayMac = mac([0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xdd]);
const vmMac = mac([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);

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

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

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

if (!finInOnTcpClose) {
stack.handleTcpEnd({ key });
}
// The guest's FIN crosses ours and does not ACK it
stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 101,
ack: hostSeq,
flags: 0x11,
}),
srcIP,
dstIP,
);
assert.equal((stack as any).natTable.get(key)?.mySeq, hostSeq + 1);

stack.handleTCP(
buildTcpSegment({
srcPort,
dstPort,
seq: 102,
ack: hostSeq + 1,
flags: 0x10,
}),
srcIP,
dstIP,
);
assert.equal((stack as any).natTable.has(key), false);
});
}