Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quic"
version = "1.3.12"
version = "1.3.13"
authors = ["Roger Qiu <[email protected]>"]
license-file = "LICENSE"
edition = "2021"
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matrixai/quic",
"version": "1.3.12",
"version": "1.3.13",
"author": "Matrix AI",
"contributors": [
{
Expand Down Expand Up @@ -48,11 +48,11 @@
"ip-num": "^1.5.0"
},
"optionalDependencies": {
"@matrixai/quic-darwin-arm64": "1.3.12",
"@matrixai/quic-darwin-universal": "1.3.12",
"@matrixai/quic-darwin-x64": "1.3.12",
"@matrixai/quic-linux-x64": "1.3.12",
"@matrixai/quic-win32-x64": "1.3.12"
"@matrixai/quic-darwin-arm64": "1.3.13",
"@matrixai/quic-darwin-universal": "1.3.13",
"@matrixai/quic-darwin-x64": "1.3.13",
"@matrixai/quic-linux-x64": "1.3.13",
"@matrixai/quic-win32-x64": "1.3.13"
},
"devDependencies": {
"@fast-check/jest": "^1.1.0",
Expand Down
100 changes: 84 additions & 16 deletions src/QUICConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,29 @@ class QUICConnection {
/**
* Tracks the highest StreamId that has a created QUICStream for clientBidi
*/
protected streamIdUsedClientBidi = -1 as StreamId;
protected streamIdUsedClientBidi = (0b00 - 4) as StreamId;

/**
* Tracks the highest StreamId that has a created QUICStream for serverBidi
*/
protected streamIdUsedServerBidi = -1 as StreamId;
protected streamIdUsedServerBidi = (0b01 - 4) as StreamId;

/**
* Tracks the highest StreamId that has a created QUICStream for clientUni
*/
protected streamIdUsedClientUni = -1 as StreamId;
protected streamIdUsedClientUni = (0b10 - 4) as StreamId;

/**
* Tracks the highest StreamId that has a created QUICStream for clientUni
*/
protected streamIdUsedServerUni = -1 as StreamId;
protected streamIdUsedServerUni = (0b11 - 4) as StreamId;

/**
* Tracks used ids that have skipped the expected next id for the streamIdUsed counters.
* If the next id in the streamIdUsed sequence is used then we remove IDs from the Set
* up to the next ID gap.
*/
protected streamIdUsedSet: Set<number> = new Set();

/**
* Quiche connection timer. This performs time delayed state transitions.
Expand Down Expand Up @@ -988,24 +995,85 @@ class QUICConnection {
}

protected isStreamUsed(streamId: StreamId): boolean {
let nextId: number;
const type = 0b11 & streamId;
switch (type) {
case 0b00:
if (streamId <= this.streamIdUsedClientBidi) return true;
this.streamIdUsedClientBidi = streamId;
return false;
nextId = this.streamIdUsedClientBidi + 4;
if (
streamId <= this.streamIdUsedClientBidi ||
this.streamIdUsedSet.has(streamId)
) {
return true;
} else if (streamId === nextId) {
// Increase counter and check set in loop.
do {
this.streamIdUsedClientBidi = nextId as StreamId;
this.streamIdUsedSet.delete(nextId);
nextId += 4;
} while (this.streamIdUsedSet.has(nextId));
return false;
} else {
this.streamIdUsedSet.add(streamId);
return false;
}
case 0b01:
if (streamId <= this.streamIdUsedServerBidi) return true;
this.streamIdUsedServerBidi = streamId;
return false;
nextId = this.streamIdUsedServerBidi + 4;
if (
streamId <= this.streamIdUsedServerBidi ||
this.streamIdUsedSet.has(streamId)
) {
return true;
} else if (streamId === nextId) {
// Increase counter and check set in loop.
do {
this.streamIdUsedServerBidi = nextId as StreamId;
this.streamIdUsedSet.delete(nextId);
nextId += 4;
} while (this.streamIdUsedSet.has(nextId));
return false;
} else {
this.streamIdUsedSet.add(streamId);
return false;
}
case 0b10:
if (streamId <= this.streamIdUsedClientUni) return true;
this.streamIdUsedClientUni = streamId;
return false;
nextId = this.streamIdUsedClientUni + 4;
if (
streamId <= this.streamIdUsedClientUni ||
this.streamIdUsedSet.has(streamId)
) {
return true;
} else if (streamId === nextId) {
// Increase counter and check set in loop.
do {
this.streamIdUsedClientUni = nextId as StreamId;
this.streamIdUsedSet.delete(nextId);
nextId += 4;
} while (this.streamIdUsedSet.has(nextId));
return false;
} else {
this.streamIdUsedSet.add(streamId);
return false;
}
case 0b11:
if (streamId <= this.streamIdUsedServerUni) return true;
this.streamIdUsedServerUni = streamId;
return false;
nextId = this.streamIdUsedServerUni + 4;
if (
streamId <= this.streamIdUsedServerUni ||
this.streamIdUsedSet.has(streamId)
) {
return true;
} else if (streamId === nextId) {
// Increase counter and check set in loop.
do {
this.streamIdUsedServerUni = nextId as StreamId;
this.streamIdUsedSet.delete(nextId);
nextId += 4;
} while (this.streamIdUsedSet.has(nextId));
return false;
} else {
this.streamIdUsedSet.add(streamId);
return false;
}
default:
utils.never('got an unexpected ID type');
}
Expand Down
129 changes: 128 additions & 1 deletion tests/QUICStream.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { ClientCryptoOps, QUICConnection, ServerCryptoOps } from '@';
import type {
ClientCryptoOps,
QUICConnection,
ServerCryptoOps,
StreamId,
} from '@';
import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger';
import { destroyed } from '@matrixai/async-init';
import { test, fc } from '@fast-check/jest';
import * as events from '@/events';
import * as errors from '@/errors';
import * as utils from '@/utils';
Expand Down Expand Up @@ -2246,4 +2252,125 @@ describe(QUICStream.name, () => {
await client.destroy({ force: true });
await server.stop({ force: true });
});
test.prop(
[
fc
.array(fc.integer({ min: 1 }), { minLength: 1000, maxLength: 2000 })
.noShrink(),
],
{ numRuns: 1 },
)('out of order Ids are handled properly', async (arr) => {
const size = arr.length;
const used: Set<number> = new Set();
const ids: Array<number> = [];
for (let num of arr) {
do {
num = (num + 1) % size;
} while (used.has(num));
ids.push(num);
used.add(num);
}

const connectionEventProm =
utils.promise<events.EventQUICServerConnection>();
const tlsConfig = await generateTLSConfig(defaultType);
const server = new QUICServer({
crypto: {
key,
ops: serverCrypto,
},
logger: logger.getChild(QUICServer.name),
config: {
key: tlsConfig.leafKeyPairPEM.privateKey,
cert: tlsConfig.leafCertPEM,
verifyPeer: false,
},
});
socketCleanMethods.extractSocket(server);
server.addEventListener(
events.EventQUICServerConnection.name,
(e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e),
);
await server.start({
host: localhost,
});
const client = await QUICClient.createQUICClient({
host: localhost,
port: server.port,
localHost: localhost,
crypto: {
ops: clientCrypto,
},
logger: logger.getChild(QUICClient.name),
config: {
verifyPeer: false,
},
});
socketCleanMethods.extractSocket(client);
await connectionEventProm.p;

const checkId = (id: StreamId): boolean => {
// @ts-ignore: Using protected method
return client.connection.isStreamUsed(id);
};

for (const id of ids) {
expect(checkId(id as StreamId)).toBeFalse();
}
// @ts-ignore: using protected property
const usedIdSet = client.connection.streamIdUsedSet;
expect(usedIdSet.size).toBe(0);
});
test('out of order Ids are handled properly', async () => {
const connectionEventProm =
utils.promise<events.EventQUICServerConnection>();
const tlsConfig = await generateTLSConfig(defaultType);
const server = new QUICServer({
crypto: {
key,
ops: serverCrypto,
},
logger: logger.getChild(QUICServer.name),
config: {
key: tlsConfig.leafKeyPairPEM.privateKey,
cert: tlsConfig.leafCertPEM,
verifyPeer: false,
},
});
socketCleanMethods.extractSocket(server);
server.addEventListener(
events.EventQUICServerConnection.name,
(e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e),
);
await server.start({
host: localhost,
});
const client = await QUICClient.createQUICClient({
host: localhost,
port: server.port,
localHost: localhost,
crypto: {
ops: clientCrypto,
},
logger: logger.getChild(QUICClient.name),
config: {
verifyPeer: false,
},
});
socketCleanMethods.extractSocket(client);
await connectionEventProm.p;

const checkId = (id: StreamId): boolean => {
// @ts-ignore: Using protected method
return client.connection.isStreamUsed(id);
};

expect(checkId(0 as StreamId)).toBeFalse();
expect(checkId(4 as StreamId)).toBeFalse();
expect(checkId(8 as StreamId)).toBeFalse();
expect(checkId(4 as StreamId)).toBeTrue();
expect(checkId(16 as StreamId)).toBeFalse();
expect(checkId(0 as StreamId)).toBeTrue();
expect(checkId(0 as StreamId)).toBeTrue();
});
});
Loading