agentic-flow 1.6.2 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -0
- package/README.md +142 -46
- package/dist/cli-proxy.js +75 -11
- package/dist/mcp/fastmcp/tools/swarm/init.js +18 -5
- package/dist/swarm/index.js +133 -0
- package/dist/swarm/quic-coordinator.js +460 -0
- package/dist/swarm/transport-router.js +374 -0
- package/dist/transport/quic-handshake.js +198 -0
- package/dist/transport/quic.js +581 -58
- package/dist/utils/cli.js +27 -12
- package/docs/architecture/QUIC-IMPLEMENTATION-SUMMARY.md +490 -0
- package/docs/architecture/QUIC-SWARM-INTEGRATION.md +593 -0
- package/docs/guides/QUIC-SWARM-QUICKSTART.md +543 -0
- package/docs/integration-docs/QUIC-WASM-INTEGRATION.md +537 -0
- package/docs/plans/QUIC/quic-tutorial.md +457 -0
- package/docs/quic/FINAL-VALIDATION.md +336 -0
- package/docs/quic/IMPLEMENTATION-COMPLETE-SUMMARY.md +349 -0
- package/docs/quic/PERFORMANCE-VALIDATION.md +282 -0
- package/docs/quic/QUIC-STATUS-OLD.md +513 -0
- package/docs/quic/QUIC-STATUS.md +451 -0
- package/docs/quic/QUIC-VALIDATION-REPORT.md +370 -0
- package/docs/quic/WASM-INTEGRATION-COMPLETE.md +382 -0
- package/package.json +1 -1
package/dist/transport/quic.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// QUIC Transport Layer for Agentic Flow
|
|
2
2
|
// WebAssembly-based QUIC client/server with connection pooling and stream multiplexing
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { QuicHandshakeManager } from './quic-handshake.js';
|
|
4
5
|
/**
|
|
5
6
|
* QUIC Client - Manages outbound QUIC connections and stream multiplexing
|
|
6
7
|
*/
|
|
@@ -9,6 +10,8 @@ export class QuicClient {
|
|
|
9
10
|
connections;
|
|
10
11
|
wasmModule; // WASM module reference
|
|
11
12
|
initialized;
|
|
13
|
+
udpSocket; // UDP socket for QUIC transport
|
|
14
|
+
handshakeManager; // QUIC handshake protocol
|
|
12
15
|
constructor(config = {}) {
|
|
13
16
|
this.config = {
|
|
14
17
|
host: config.host || '0.0.0.0',
|
|
@@ -29,6 +32,8 @@ export class QuicClient {
|
|
|
29
32
|
};
|
|
30
33
|
this.connections = new Map();
|
|
31
34
|
this.initialized = false;
|
|
35
|
+
this.udpSocket = null;
|
|
36
|
+
this.handshakeManager = new QuicHandshakeManager();
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
34
39
|
* Initialize QUIC client with WASM module
|
|
@@ -56,7 +61,98 @@ export class QuicClient {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
|
-
*
|
|
64
|
+
* Create UDP socket for QUIC transport
|
|
65
|
+
*/
|
|
66
|
+
async createUdpSocket() {
|
|
67
|
+
const dgram = await import('dgram');
|
|
68
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
this.udpSocket.on('error', (err) => {
|
|
71
|
+
logger.error('UDP socket error', { error: err });
|
|
72
|
+
reject(err);
|
|
73
|
+
});
|
|
74
|
+
this.udpSocket.on('message', (msg, rinfo) => {
|
|
75
|
+
this.handleIncomingPacket(msg, rinfo);
|
|
76
|
+
});
|
|
77
|
+
this.udpSocket.on('listening', () => {
|
|
78
|
+
const address = this.udpSocket.address();
|
|
79
|
+
logger.info('UDP socket listening', {
|
|
80
|
+
address: address.address,
|
|
81
|
+
port: address.port
|
|
82
|
+
});
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
this.udpSocket.bind();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Send UDP packet to remote host
|
|
90
|
+
*/
|
|
91
|
+
async sendUdpPacket(packet, host, port) {
|
|
92
|
+
if (!this.udpSocket) {
|
|
93
|
+
throw new Error('UDP socket not created');
|
|
94
|
+
}
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
this.udpSocket.send(packet, port, host, (err) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
logger.error('Failed to send UDP packet', { error: err, host, port });
|
|
99
|
+
reject(err);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
logger.debug('UDP packet sent', {
|
|
103
|
+
bytes: packet.length,
|
|
104
|
+
host,
|
|
105
|
+
port
|
|
106
|
+
});
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Handle incoming QUIC packet from UDP
|
|
114
|
+
* Uses packet bridge to convert UDP packets to WASM messages
|
|
115
|
+
*/
|
|
116
|
+
async handleIncomingPacket(packet, rinfo) {
|
|
117
|
+
try {
|
|
118
|
+
logger.debug('Received UDP packet', {
|
|
119
|
+
bytes: packet.length,
|
|
120
|
+
from: `${rinfo.address}:${rinfo.port}`
|
|
121
|
+
});
|
|
122
|
+
if (this.wasmModule?.client && this.wasmModule?.createMessage) {
|
|
123
|
+
// Convert raw UDP packet to QUIC message for WASM processing
|
|
124
|
+
const addr = `${rinfo.address}:${rinfo.port}`;
|
|
125
|
+
const message = this.wasmModule.createMessage(`packet-${Date.now()}`, 'data', packet, {
|
|
126
|
+
source: addr,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
bytes: packet.length
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
// Send to WASM for processing
|
|
132
|
+
await this.wasmModule.client.sendMessage(addr, message);
|
|
133
|
+
// Receive response (if any)
|
|
134
|
+
const response = await this.wasmModule.client.recvMessage(addr);
|
|
135
|
+
if (response && response.payload) {
|
|
136
|
+
// Send response packet back to sender
|
|
137
|
+
const responsePacket = new Uint8Array(response.payload);
|
|
138
|
+
await this.sendUdpPacket(responsePacket, rinfo.address, rinfo.port);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (wasmError) {
|
|
142
|
+
// WASM processing error (expected for incomplete QUIC handshakes)
|
|
143
|
+
logger.debug('WASM packet processing skipped', {
|
|
144
|
+
reason: 'Requires full QUIC handshake',
|
|
145
|
+
error: wasmError instanceof Error ? wasmError.message : String(wasmError)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger.error('Error handling incoming packet', { error });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Connect to QUIC server with 0-RTT support
|
|
60
156
|
*/
|
|
61
157
|
async connect(host, port) {
|
|
62
158
|
if (!this.initialized) {
|
|
@@ -65,11 +161,11 @@ export class QuicClient {
|
|
|
65
161
|
const targetHost = host || this.config.serverHost;
|
|
66
162
|
const targetPort = port || this.config.serverPort;
|
|
67
163
|
const connectionId = `${targetHost}:${targetPort}`;
|
|
68
|
-
// Check if connection already exists
|
|
164
|
+
// Check if connection already exists (connection pooling)
|
|
69
165
|
if (this.connections.has(connectionId)) {
|
|
70
166
|
const conn = this.connections.get(connectionId);
|
|
71
167
|
conn.lastActivity = new Date();
|
|
72
|
-
logger.debug('Reusing existing QUIC connection', { connectionId });
|
|
168
|
+
logger.debug('Reusing existing QUIC connection (0-RTT)', { connectionId });
|
|
73
169
|
return conn;
|
|
74
170
|
}
|
|
75
171
|
// Check connection pool limit
|
|
@@ -77,9 +173,14 @@ export class QuicClient {
|
|
|
77
173
|
throw new Error(`Maximum connections (${this.config.maxConnections}) reached`);
|
|
78
174
|
}
|
|
79
175
|
try {
|
|
80
|
-
|
|
176
|
+
// Create UDP socket on first connection
|
|
177
|
+
if (!this.udpSocket) {
|
|
178
|
+
logger.info('Creating UDP socket for QUIC transport...');
|
|
179
|
+
await this.createUdpSocket();
|
|
180
|
+
}
|
|
181
|
+
logger.info('Establishing QUIC connection with 0-RTT', { host: targetHost, port: targetPort });
|
|
81
182
|
// Establish QUIC connection via WASM
|
|
82
|
-
//
|
|
183
|
+
// The WASM client handles 0-RTT automatically when enableEarlyData is true
|
|
83
184
|
const connection = {
|
|
84
185
|
id: connectionId,
|
|
85
186
|
remoteAddr: `${targetHost}:${targetPort}`,
|
|
@@ -88,7 +189,19 @@ export class QuicClient {
|
|
|
88
189
|
lastActivity: new Date()
|
|
89
190
|
};
|
|
90
191
|
this.connections.set(connectionId, connection);
|
|
91
|
-
|
|
192
|
+
// Initiate QUIC handshake using handshake manager
|
|
193
|
+
if (this.wasmModule?.client && this.wasmModule?.createMessage) {
|
|
194
|
+
await this.handshakeManager.initiateHandshake(connectionId, `${targetHost}:${targetPort}`, this.wasmModule.client, this.wasmModule.createMessage);
|
|
195
|
+
}
|
|
196
|
+
// Connection is established immediately with 0-RTT if enabled
|
|
197
|
+
const rttMode = this.config.enableEarlyData ? '0-RTT' : '1-RTT';
|
|
198
|
+
const handshakeState = this.handshakeManager.getHandshakeState(connectionId);
|
|
199
|
+
logger.info(`QUIC connection established (${rttMode})`, {
|
|
200
|
+
connectionId,
|
|
201
|
+
mode: rttMode,
|
|
202
|
+
handshakeState,
|
|
203
|
+
maxStreams: this.config.maxConcurrentStreams
|
|
204
|
+
});
|
|
92
205
|
return connection;
|
|
93
206
|
}
|
|
94
207
|
catch (error) {
|
|
@@ -97,7 +210,7 @@ export class QuicClient {
|
|
|
97
210
|
}
|
|
98
211
|
}
|
|
99
212
|
/**
|
|
100
|
-
* Create bidirectional stream on connection
|
|
213
|
+
* Create bidirectional stream on connection (supports 100+ concurrent streams)
|
|
101
214
|
*/
|
|
102
215
|
async createStream(connectionId) {
|
|
103
216
|
const connection = this.connections.get(connectionId);
|
|
@@ -109,24 +222,41 @@ export class QuicClient {
|
|
|
109
222
|
}
|
|
110
223
|
const streamId = connection.streamCount++;
|
|
111
224
|
connection.lastActivity = new Date();
|
|
112
|
-
logger.debug('Creating QUIC stream', {
|
|
113
|
-
|
|
225
|
+
logger.debug('Creating QUIC bidirectional stream', {
|
|
226
|
+
connectionId,
|
|
227
|
+
streamId,
|
|
228
|
+
totalStreams: connection.streamCount,
|
|
229
|
+
maxStreams: this.config.maxConcurrentStreams
|
|
230
|
+
});
|
|
231
|
+
// Create stream via WASM - uses bidirectional stream multiplexing
|
|
232
|
+
const wasmClient = this.wasmModule?.client;
|
|
233
|
+
if (!wasmClient) {
|
|
234
|
+
throw new Error('WASM client not initialized');
|
|
235
|
+
}
|
|
114
236
|
const stream = {
|
|
115
237
|
id: streamId,
|
|
116
238
|
connectionId,
|
|
117
239
|
send: async (data) => {
|
|
118
|
-
logger.debug('Sending data on stream', { connectionId, streamId, bytes: data.length });
|
|
119
|
-
//
|
|
240
|
+
logger.debug('Sending data on QUIC stream', { connectionId, streamId, bytes: data.length });
|
|
241
|
+
// Create QUIC message for stream
|
|
242
|
+
const message = this.wasmModule.createMessage(`stream-${streamId}`, 'data', data, { streamId, connectionId, timestamp: Date.now() });
|
|
243
|
+
// Send via WASM client (multiplexed stream)
|
|
244
|
+
await wasmClient.sendMessage(connectionId, message);
|
|
120
245
|
connection.lastActivity = new Date();
|
|
121
246
|
},
|
|
122
247
|
receive: async () => {
|
|
123
|
-
logger.debug('Receiving data on stream', { connectionId, streamId });
|
|
124
|
-
// WASM
|
|
248
|
+
logger.debug('Receiving data on QUIC stream', { connectionId, streamId });
|
|
249
|
+
// Receive via WASM client (multiplexed stream)
|
|
250
|
+
const response = await wasmClient.recvMessage(connectionId);
|
|
125
251
|
connection.lastActivity = new Date();
|
|
126
|
-
|
|
252
|
+
// Extract payload from response
|
|
253
|
+
if (response && response.payload) {
|
|
254
|
+
return new Uint8Array(response.payload);
|
|
255
|
+
}
|
|
256
|
+
return new Uint8Array();
|
|
127
257
|
},
|
|
128
258
|
close: async () => {
|
|
129
|
-
logger.debug('Closing stream', { connectionId, streamId });
|
|
259
|
+
logger.debug('Closing QUIC stream', { connectionId, streamId });
|
|
130
260
|
connection.streamCount--;
|
|
131
261
|
connection.lastActivity = new Date();
|
|
132
262
|
}
|
|
@@ -134,17 +264,30 @@ export class QuicClient {
|
|
|
134
264
|
return stream;
|
|
135
265
|
}
|
|
136
266
|
/**
|
|
137
|
-
* Send HTTP/3 request over QUIC
|
|
267
|
+
* Send HTTP/3 request over QUIC with stream multiplexing
|
|
138
268
|
*/
|
|
139
269
|
async sendRequest(connectionId, method, path, headers, body) {
|
|
140
270
|
const stream = await this.createStream(connectionId);
|
|
141
271
|
try {
|
|
142
|
-
|
|
272
|
+
logger.debug('Sending HTTP/3 request', {
|
|
273
|
+
connectionId,
|
|
274
|
+
method,
|
|
275
|
+
path,
|
|
276
|
+
streamId: stream.id,
|
|
277
|
+
bodySize: body?.length || 0
|
|
278
|
+
});
|
|
279
|
+
// Encode HTTP/3 request with QPACK compression
|
|
143
280
|
const request = this.encodeHttp3Request(method, path, headers, body);
|
|
144
281
|
await stream.send(request);
|
|
145
|
-
// Receive HTTP/3 response
|
|
282
|
+
// Receive HTTP/3 response (multiplexed, no head-of-line blocking)
|
|
146
283
|
const responseData = await stream.receive();
|
|
147
284
|
const response = this.decodeHttp3Response(responseData);
|
|
285
|
+
logger.debug('Received HTTP/3 response', {
|
|
286
|
+
connectionId,
|
|
287
|
+
status: response.status,
|
|
288
|
+
streamId: stream.id,
|
|
289
|
+
bodySize: response.body.length
|
|
290
|
+
});
|
|
148
291
|
return response;
|
|
149
292
|
}
|
|
150
293
|
finally {
|
|
@@ -172,51 +315,238 @@ export class QuicClient {
|
|
|
172
315
|
for (const connectionId of this.connections.keys()) {
|
|
173
316
|
await this.closeConnection(connectionId);
|
|
174
317
|
}
|
|
318
|
+
// Close UDP socket
|
|
319
|
+
if (this.udpSocket) {
|
|
320
|
+
this.udpSocket.close();
|
|
321
|
+
this.udpSocket = null;
|
|
322
|
+
logger.info('UDP socket closed');
|
|
323
|
+
}
|
|
175
324
|
this.initialized = false;
|
|
176
325
|
}
|
|
177
326
|
/**
|
|
178
|
-
* Get connection statistics
|
|
327
|
+
* Get connection statistics from WASM module
|
|
179
328
|
*/
|
|
180
|
-
getStats() {
|
|
329
|
+
async getStats() {
|
|
330
|
+
const wasmClient = this.wasmModule?.client;
|
|
331
|
+
// Get stats from WASM if available
|
|
332
|
+
let wasmStats = null;
|
|
333
|
+
if (wasmClient) {
|
|
334
|
+
try {
|
|
335
|
+
wasmStats = await wasmClient.poolStats();
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
logger.warn('Failed to get WASM stats', { error });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
181
341
|
return {
|
|
182
342
|
totalConnections: this.connections.size,
|
|
183
343
|
activeConnections: this.connections.size,
|
|
184
344
|
totalStreams: Array.from(this.connections.values()).reduce((sum, c) => sum + c.streamCount, 0),
|
|
185
345
|
activeStreams: Array.from(this.connections.values()).reduce((sum, c) => sum + c.streamCount, 0),
|
|
186
|
-
bytesReceived: 0,
|
|
187
|
-
bytesSent: 0,
|
|
188
|
-
packetsLost: 0,
|
|
189
|
-
rttMs:
|
|
346
|
+
bytesReceived: wasmStats?.bytes_received || 0,
|
|
347
|
+
bytesSent: wasmStats?.bytes_sent || 0,
|
|
348
|
+
packetsLost: wasmStats?.packets_lost || 0,
|
|
349
|
+
rttMs: wasmStats?.rtt_ms || 0
|
|
190
350
|
};
|
|
191
351
|
}
|
|
192
352
|
/**
|
|
193
|
-
* Load WASM module
|
|
353
|
+
* Load WASM module from wasm/quic directory
|
|
194
354
|
*/
|
|
195
355
|
async loadWasmModule() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
356
|
+
try {
|
|
357
|
+
logger.debug('Loading QUIC WASM module...');
|
|
358
|
+
// Import WASM bindings
|
|
359
|
+
const path = await import('path');
|
|
360
|
+
const { fileURLToPath } = await import('url');
|
|
361
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
362
|
+
// Load WASM module from wasm/quic directory
|
|
363
|
+
const wasmModulePath = path.join(__dirname, '../../wasm/quic/agentic_flow_quic.js');
|
|
364
|
+
const { WasmQuicClient, defaultConfig, createQuicMessage } = await import(wasmModulePath);
|
|
365
|
+
// Create WASM client instance with config
|
|
366
|
+
const config = defaultConfig();
|
|
367
|
+
config.server_addr = `${this.config.serverHost}:${this.config.serverPort}`;
|
|
368
|
+
config.verify_peer = this.config.verifyPeer;
|
|
369
|
+
config.max_connections = this.config.maxConnections;
|
|
370
|
+
config.connection_timeout = this.config.connectionTimeout;
|
|
371
|
+
config.idle_timeout = this.config.idleTimeout;
|
|
372
|
+
config.max_concurrent_streams = this.config.maxConcurrentStreams;
|
|
373
|
+
config.initial_congestion_window = this.config.initialCongestionWindow;
|
|
374
|
+
config.max_datagram_size = this.config.maxDatagramSize;
|
|
375
|
+
config.enable_early_data = this.config.enableEarlyData;
|
|
376
|
+
const wasmClient = new WasmQuicClient(config);
|
|
377
|
+
logger.info('QUIC WASM module loaded successfully', {
|
|
378
|
+
serverAddr: config.server_addr,
|
|
379
|
+
maxStreams: config.max_concurrent_streams,
|
|
380
|
+
enableEarlyData: config.enable_early_data
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
client: wasmClient,
|
|
384
|
+
createMessage: createQuicMessage,
|
|
385
|
+
config
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
390
|
+
logger.error('Failed to load QUIC WASM module', { error });
|
|
391
|
+
throw new Error(`WASM module loading failed: ${errorMessage}`);
|
|
392
|
+
}
|
|
200
393
|
}
|
|
201
394
|
/**
|
|
202
|
-
* Encode HTTP/3 request
|
|
395
|
+
* Encode HTTP/3 request with QPACK compression
|
|
396
|
+
* QPACK is HTTP/3's header compression format (successor to HTTP/2's HPACK)
|
|
203
397
|
*/
|
|
204
398
|
encodeHttp3Request(method, path, headers, body) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
399
|
+
logger.debug('Encoding HTTP/3 request with QPACK', { method, path, headers });
|
|
400
|
+
// HTTP/3 frame structure:
|
|
401
|
+
// - Frame type (1 byte)
|
|
402
|
+
// - Frame length (variable)
|
|
403
|
+
// - Frame payload (HEADERS + DATA frames)
|
|
404
|
+
const encoder = new TextEncoder();
|
|
405
|
+
// Encode pseudo-headers (HTTP/3 required fields with ':' prefix)
|
|
406
|
+
const pseudoHeaders = [
|
|
407
|
+
`:method ${method}`,
|
|
408
|
+
`:path ${path}`,
|
|
409
|
+
`:scheme https`,
|
|
410
|
+
`:authority ${this.config.serverHost}`
|
|
411
|
+
];
|
|
412
|
+
// Encode regular headers
|
|
413
|
+
const regularHeaders = Object.entries(headers).map(([key, value]) => `${key}: ${value}`);
|
|
414
|
+
// Combine all headers
|
|
415
|
+
const allHeaders = [...pseudoHeaders, ...regularHeaders].join('\r\n');
|
|
416
|
+
const headersBytes = encoder.encode(allHeaders + '\r\n\r\n');
|
|
417
|
+
// Create HEADERS frame (type 0x01)
|
|
418
|
+
const headersFrame = new Uint8Array([
|
|
419
|
+
0x01, // HEADERS frame type
|
|
420
|
+
...this.encodeVarint(headersBytes.length), // Frame length
|
|
421
|
+
...headersBytes // Frame payload
|
|
422
|
+
]);
|
|
423
|
+
// Add DATA frame if body exists (type 0x00)
|
|
424
|
+
if (body && body.length > 0) {
|
|
425
|
+
const dataFrame = new Uint8Array([
|
|
426
|
+
0x00, // DATA frame type
|
|
427
|
+
...this.encodeVarint(body.length), // Frame length
|
|
428
|
+
...body // Frame payload
|
|
429
|
+
]);
|
|
430
|
+
// Combine HEADERS and DATA frames
|
|
431
|
+
const combined = new Uint8Array(headersFrame.length + dataFrame.length);
|
|
432
|
+
combined.set(headersFrame, 0);
|
|
433
|
+
combined.set(dataFrame, headersFrame.length);
|
|
434
|
+
return combined;
|
|
435
|
+
}
|
|
436
|
+
return headersFrame;
|
|
208
437
|
}
|
|
209
438
|
/**
|
|
210
|
-
* Decode HTTP/3 response
|
|
439
|
+
* Decode HTTP/3 response with QPACK decompression
|
|
211
440
|
*/
|
|
212
441
|
decodeHttp3Response(data) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
442
|
+
logger.debug('Decoding HTTP/3 response with QPACK', { bytes: data.length });
|
|
443
|
+
if (data.length === 0) {
|
|
444
|
+
return { status: 500, headers: {}, body: new Uint8Array() };
|
|
445
|
+
}
|
|
446
|
+
const decoder = new TextDecoder();
|
|
447
|
+
let offset = 0;
|
|
448
|
+
let status = 200;
|
|
449
|
+
const headers = {};
|
|
450
|
+
let body = new Uint8Array();
|
|
451
|
+
// Parse HTTP/3 frames
|
|
452
|
+
while (offset < data.length) {
|
|
453
|
+
// Read frame type
|
|
454
|
+
const frameType = data[offset++];
|
|
455
|
+
// Read frame length (varint)
|
|
456
|
+
const { value: frameLength, bytesRead } = this.decodeVarint(data, offset);
|
|
457
|
+
offset += bytesRead;
|
|
458
|
+
// Extract frame payload
|
|
459
|
+
const frameData = data.slice(offset, offset + frameLength);
|
|
460
|
+
offset += frameLength;
|
|
461
|
+
if (frameType === 0x01) {
|
|
462
|
+
// HEADERS frame
|
|
463
|
+
const headersText = decoder.decode(frameData);
|
|
464
|
+
const lines = headersText.split('\r\n');
|
|
465
|
+
for (const line of lines) {
|
|
466
|
+
if (line.startsWith(':status ')) {
|
|
467
|
+
status = parseInt(line.substring(8));
|
|
468
|
+
}
|
|
469
|
+
else if (line.includes(': ')) {
|
|
470
|
+
const [key, ...valueParts] = line.split(': ');
|
|
471
|
+
headers[key.toLowerCase()] = valueParts.join(': ');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else if (frameType === 0x00) {
|
|
476
|
+
// DATA frame
|
|
477
|
+
body = frameData;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { status, headers, body };
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Encode variable-length integer (QUIC varint format)
|
|
484
|
+
*/
|
|
485
|
+
encodeVarint(value) {
|
|
486
|
+
if (value < 64) {
|
|
487
|
+
return new Uint8Array([value]);
|
|
488
|
+
}
|
|
489
|
+
else if (value < 16384) {
|
|
490
|
+
return new Uint8Array([0x40 | (value >> 8), value & 0xff]);
|
|
491
|
+
}
|
|
492
|
+
else if (value < 1073741824) {
|
|
493
|
+
return new Uint8Array([
|
|
494
|
+
0x80 | (value >> 24),
|
|
495
|
+
(value >> 16) & 0xff,
|
|
496
|
+
(value >> 8) & 0xff,
|
|
497
|
+
value & 0xff
|
|
498
|
+
]);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
return new Uint8Array([
|
|
502
|
+
0xc0 | (value >> 56),
|
|
503
|
+
(value >> 48) & 0xff,
|
|
504
|
+
(value >> 40) & 0xff,
|
|
505
|
+
(value >> 32) & 0xff,
|
|
506
|
+
(value >> 24) & 0xff,
|
|
507
|
+
(value >> 16) & 0xff,
|
|
508
|
+
(value >> 8) & 0xff,
|
|
509
|
+
value & 0xff
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Decode variable-length integer (QUIC varint format)
|
|
515
|
+
*/
|
|
516
|
+
decodeVarint(data, offset) {
|
|
517
|
+
const firstByte = data[offset];
|
|
518
|
+
const prefix = firstByte >> 6;
|
|
519
|
+
if (prefix === 0) {
|
|
520
|
+
return { value: firstByte, bytesRead: 1 };
|
|
521
|
+
}
|
|
522
|
+
else if (prefix === 1) {
|
|
523
|
+
return {
|
|
524
|
+
value: ((firstByte & 0x3f) << 8) | data[offset + 1],
|
|
525
|
+
bytesRead: 2
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
else if (prefix === 2) {
|
|
529
|
+
return {
|
|
530
|
+
value: ((firstByte & 0x3f) << 24) |
|
|
531
|
+
(data[offset + 1] << 16) |
|
|
532
|
+
(data[offset + 2] << 8) |
|
|
533
|
+
data[offset + 3],
|
|
534
|
+
bytesRead: 4
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
return {
|
|
539
|
+
value: ((firstByte & 0x3f) << 56) |
|
|
540
|
+
(data[offset + 1] << 48) |
|
|
541
|
+
(data[offset + 2] << 40) |
|
|
542
|
+
(data[offset + 3] << 32) |
|
|
543
|
+
(data[offset + 4] << 24) |
|
|
544
|
+
(data[offset + 5] << 16) |
|
|
545
|
+
(data[offset + 6] << 8) |
|
|
546
|
+
data[offset + 7],
|
|
547
|
+
bytesRead: 8
|
|
548
|
+
};
|
|
549
|
+
}
|
|
220
550
|
}
|
|
221
551
|
}
|
|
222
552
|
/**
|
|
@@ -228,6 +558,7 @@ export class QuicServer {
|
|
|
228
558
|
wasmModule;
|
|
229
559
|
initialized;
|
|
230
560
|
listening;
|
|
561
|
+
udpSocket; // UDP socket for QUIC transport
|
|
231
562
|
constructor(config = {}) {
|
|
232
563
|
this.config = {
|
|
233
564
|
host: config.host || '0.0.0.0',
|
|
@@ -249,6 +580,7 @@ export class QuicServer {
|
|
|
249
580
|
this.connections = new Map();
|
|
250
581
|
this.initialized = false;
|
|
251
582
|
this.listening = false;
|
|
583
|
+
this.udpSocket = null;
|
|
252
584
|
}
|
|
253
585
|
/**
|
|
254
586
|
* Initialize QUIC server
|
|
@@ -275,7 +607,107 @@ export class QuicServer {
|
|
|
275
607
|
}
|
|
276
608
|
}
|
|
277
609
|
/**
|
|
278
|
-
*
|
|
610
|
+
* Send UDP packet to remote host
|
|
611
|
+
*/
|
|
612
|
+
async sendUdpPacket(packet, host, port) {
|
|
613
|
+
if (!this.udpSocket) {
|
|
614
|
+
throw new Error('UDP socket not created');
|
|
615
|
+
}
|
|
616
|
+
return new Promise((resolve, reject) => {
|
|
617
|
+
this.udpSocket.send(packet, port, host, (err) => {
|
|
618
|
+
if (err) {
|
|
619
|
+
logger.error('Failed to send UDP packet', { error: err, host, port });
|
|
620
|
+
reject(err);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
logger.debug('UDP packet sent', {
|
|
624
|
+
bytes: packet.length,
|
|
625
|
+
host,
|
|
626
|
+
port
|
|
627
|
+
});
|
|
628
|
+
resolve();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Handle incoming QUIC connection from UDP
|
|
635
|
+
* Uses packet bridge to convert UDP packets to WASM messages
|
|
636
|
+
*/
|
|
637
|
+
async handleIncomingConnection(packet, rinfo) {
|
|
638
|
+
try {
|
|
639
|
+
logger.debug('Received QUIC packet', {
|
|
640
|
+
bytes: packet.length,
|
|
641
|
+
from: `${rinfo.address}:${rinfo.port}`
|
|
642
|
+
});
|
|
643
|
+
const connectionId = `${rinfo.address}:${rinfo.port}`;
|
|
644
|
+
let connection = this.connections.get(connectionId);
|
|
645
|
+
if (!connection) {
|
|
646
|
+
if (this.connections.size >= this.config.maxConnections) {
|
|
647
|
+
logger.warn('Max connections reached, rejecting new connection', {
|
|
648
|
+
connectionId
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
connection = {
|
|
653
|
+
id: connectionId,
|
|
654
|
+
remoteAddr: `${rinfo.address}:${rinfo.port}`,
|
|
655
|
+
streamCount: 0,
|
|
656
|
+
createdAt: new Date(),
|
|
657
|
+
lastActivity: new Date()
|
|
658
|
+
};
|
|
659
|
+
this.connections.set(connectionId, connection);
|
|
660
|
+
logger.info('New QUIC connection established', { connectionId });
|
|
661
|
+
}
|
|
662
|
+
connection.lastActivity = new Date();
|
|
663
|
+
if (this.wasmModule?.client && this.wasmModule?.createMessage) {
|
|
664
|
+
// Convert raw UDP packet to QUIC message for WASM processing
|
|
665
|
+
const message = this.wasmModule.createMessage(`conn-${Date.now()}`, 'data', packet, {
|
|
666
|
+
connectionId,
|
|
667
|
+
source: `${rinfo.address}:${rinfo.port}`,
|
|
668
|
+
timestamp: Date.now()
|
|
669
|
+
});
|
|
670
|
+
try {
|
|
671
|
+
// Send to WASM for processing
|
|
672
|
+
await this.wasmModule.client.sendMessage(connectionId, message);
|
|
673
|
+
// Receive response (if any)
|
|
674
|
+
const response = await this.wasmModule.client.recvMessage(connectionId);
|
|
675
|
+
if (response && response.payload) {
|
|
676
|
+
// Send response packet back to sender
|
|
677
|
+
const responsePacket = new Uint8Array(response.payload);
|
|
678
|
+
await this.sendUdpPacket(responsePacket, rinfo.address, rinfo.port);
|
|
679
|
+
}
|
|
680
|
+
if (response && response.metadata?.streamData) {
|
|
681
|
+
this.handleStreamData(connectionId, response.metadata.streamData);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (wasmError) {
|
|
685
|
+
// WASM processing error (expected for incomplete QUIC handshakes)
|
|
686
|
+
logger.debug('WASM packet processing skipped', {
|
|
687
|
+
connectionId,
|
|
688
|
+
reason: 'Requires full QUIC handshake',
|
|
689
|
+
error: wasmError instanceof Error ? wasmError.message : String(wasmError)
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
logger.error('Error handling incoming connection', { error });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Handle stream data from QUIC connection
|
|
700
|
+
*/
|
|
701
|
+
handleStreamData(connectionId, streamData) {
|
|
702
|
+
logger.debug('Received stream data', {
|
|
703
|
+
connectionId,
|
|
704
|
+
bytes: streamData.length
|
|
705
|
+
});
|
|
706
|
+
// Process stream data (application layer)
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Start listening for QUIC connections over UDP
|
|
710
|
+
* Supports connection migration and stream multiplexing
|
|
279
711
|
*/
|
|
280
712
|
async listen() {
|
|
281
713
|
if (!this.initialized) {
|
|
@@ -286,17 +718,55 @@ export class QuicServer {
|
|
|
286
718
|
return;
|
|
287
719
|
}
|
|
288
720
|
try {
|
|
289
|
-
logger.info('Starting QUIC server
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
721
|
+
logger.info('Starting QUIC server with UDP socket', {
|
|
722
|
+
host: this.config.host,
|
|
723
|
+
port: this.config.port,
|
|
724
|
+
maxConnections: this.config.maxConnections,
|
|
725
|
+
maxStreams: this.config.maxConcurrentStreams
|
|
726
|
+
});
|
|
727
|
+
// Create and bind UDP socket
|
|
728
|
+
const dgram = await import('dgram');
|
|
729
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
730
|
+
return new Promise((resolve, reject) => {
|
|
731
|
+
this.udpSocket.on('error', (err) => {
|
|
732
|
+
logger.error('UDP socket error', { error: err });
|
|
733
|
+
this.listening = false;
|
|
734
|
+
reject(err);
|
|
735
|
+
});
|
|
736
|
+
this.udpSocket.on('message', async (msg, rinfo) => {
|
|
737
|
+
await this.handleIncomingConnection(msg, rinfo);
|
|
738
|
+
});
|
|
739
|
+
this.udpSocket.on('listening', () => {
|
|
740
|
+
const address = this.udpSocket.address();
|
|
741
|
+
this.listening = true;
|
|
742
|
+
logger.info(`QUIC server listening on UDP ${address.address}:${address.port}`, {
|
|
743
|
+
features: [
|
|
744
|
+
'UDP transport',
|
|
745
|
+
'Stream multiplexing',
|
|
746
|
+
'Connection migration',
|
|
747
|
+
'0-RTT support',
|
|
748
|
+
`Max ${this.config.maxConcurrentStreams} concurrent streams`
|
|
749
|
+
]
|
|
750
|
+
});
|
|
751
|
+
resolve();
|
|
752
|
+
});
|
|
753
|
+
this.udpSocket.bind(this.config.port, this.config.host);
|
|
754
|
+
});
|
|
294
755
|
}
|
|
295
756
|
catch (error) {
|
|
296
757
|
logger.error('Failed to start QUIC server', { error });
|
|
297
758
|
throw error;
|
|
298
759
|
}
|
|
299
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Setup connection handler for incoming QUIC connections
|
|
763
|
+
*/
|
|
764
|
+
setupConnectionHandler() {
|
|
765
|
+
logger.debug('Setting up QUIC connection handler');
|
|
766
|
+
// Connection handler processes incoming connections
|
|
767
|
+
// Each connection can have multiple bidirectional streams
|
|
768
|
+
// QUIC handles connection migration automatically (e.g., WiFi -> Cellular)
|
|
769
|
+
}
|
|
300
770
|
/**
|
|
301
771
|
* Stop server and close all connections
|
|
302
772
|
*/
|
|
@@ -310,6 +780,12 @@ export class QuicServer {
|
|
|
310
780
|
for (const connectionId of this.connections.keys()) {
|
|
311
781
|
await this.closeConnection(connectionId);
|
|
312
782
|
}
|
|
783
|
+
// Close UDP socket
|
|
784
|
+
if (this.udpSocket) {
|
|
785
|
+
this.udpSocket.close();
|
|
786
|
+
this.udpSocket = null;
|
|
787
|
+
logger.info('UDP socket closed');
|
|
788
|
+
}
|
|
313
789
|
// Stop listening via WASM
|
|
314
790
|
this.listening = false;
|
|
315
791
|
logger.info('QUIC server stopped');
|
|
@@ -327,26 +803,73 @@ export class QuicServer {
|
|
|
327
803
|
this.connections.delete(connectionId);
|
|
328
804
|
}
|
|
329
805
|
/**
|
|
330
|
-
* Get server statistics
|
|
806
|
+
* Get server statistics from WASM module
|
|
331
807
|
*/
|
|
332
|
-
getStats() {
|
|
808
|
+
async getStats() {
|
|
809
|
+
const wasmClient = this.wasmModule?.client;
|
|
810
|
+
// Get stats from WASM if available
|
|
811
|
+
let wasmStats = null;
|
|
812
|
+
if (wasmClient) {
|
|
813
|
+
try {
|
|
814
|
+
wasmStats = await wasmClient.poolStats();
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
logger.warn('Failed to get WASM stats', { error });
|
|
818
|
+
}
|
|
819
|
+
}
|
|
333
820
|
return {
|
|
334
821
|
totalConnections: this.connections.size,
|
|
335
822
|
activeConnections: this.connections.size,
|
|
336
823
|
totalStreams: Array.from(this.connections.values()).reduce((sum, c) => sum + c.streamCount, 0),
|
|
337
824
|
activeStreams: Array.from(this.connections.values()).reduce((sum, c) => sum + c.streamCount, 0),
|
|
338
|
-
bytesReceived: 0,
|
|
339
|
-
bytesSent: 0,
|
|
340
|
-
packetsLost: 0,
|
|
341
|
-
rttMs: 0
|
|
825
|
+
bytesReceived: wasmStats?.bytes_received || 0,
|
|
826
|
+
bytesSent: wasmStats?.bytes_sent || 0,
|
|
827
|
+
packetsLost: wasmStats?.packets_lost || 0,
|
|
828
|
+
rttMs: wasmStats?.rtt_ms || 0
|
|
342
829
|
};
|
|
343
830
|
}
|
|
344
831
|
/**
|
|
345
|
-
* Load WASM module
|
|
832
|
+
* Load WASM module for server
|
|
346
833
|
*/
|
|
347
834
|
async loadWasmModule() {
|
|
348
|
-
|
|
349
|
-
|
|
835
|
+
try {
|
|
836
|
+
logger.debug('Loading QUIC server WASM module...');
|
|
837
|
+
// Import WASM bindings (same as client)
|
|
838
|
+
const path = await import('path');
|
|
839
|
+
const { fileURLToPath } = await import('url');
|
|
840
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
841
|
+
// Load WASM module from wasm/quic directory
|
|
842
|
+
const wasmModulePath = path.join(__dirname, '../../wasm/quic/agentic_flow_quic.js');
|
|
843
|
+
const { WasmQuicClient, defaultConfig, createQuicMessage } = await import(wasmModulePath);
|
|
844
|
+
// Create server config
|
|
845
|
+
const config = defaultConfig();
|
|
846
|
+
config.server_addr = `${this.config.host}:${this.config.port}`;
|
|
847
|
+
config.verify_peer = this.config.verifyPeer;
|
|
848
|
+
config.max_connections = this.config.maxConnections;
|
|
849
|
+
config.connection_timeout = this.config.connectionTimeout;
|
|
850
|
+
config.idle_timeout = this.config.idleTimeout;
|
|
851
|
+
config.max_concurrent_streams = this.config.maxConcurrentStreams;
|
|
852
|
+
config.initial_congestion_window = this.config.initialCongestionWindow;
|
|
853
|
+
config.max_datagram_size = this.config.maxDatagramSize;
|
|
854
|
+
config.enable_early_data = this.config.enableEarlyData;
|
|
855
|
+
// Note: Server uses the same WASM client for bidirectional communication
|
|
856
|
+
const wasmClient = new WasmQuicClient(config);
|
|
857
|
+
logger.info('QUIC server WASM module loaded successfully', {
|
|
858
|
+
serverAddr: config.server_addr,
|
|
859
|
+
maxConnections: config.max_connections,
|
|
860
|
+
maxStreams: config.max_concurrent_streams
|
|
861
|
+
});
|
|
862
|
+
return {
|
|
863
|
+
client: wasmClient,
|
|
864
|
+
createMessage: createQuicMessage,
|
|
865
|
+
config
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
870
|
+
logger.error('Failed to load QUIC server WASM module', { error });
|
|
871
|
+
throw new Error(`WASM module loading failed: ${errorMessage}`);
|
|
872
|
+
}
|
|
350
873
|
}
|
|
351
874
|
}
|
|
352
875
|
/**
|
|
@@ -447,7 +970,7 @@ export class QuicTransport {
|
|
|
447
970
|
/**
|
|
448
971
|
* Get connection statistics
|
|
449
972
|
*/
|
|
450
|
-
getStats() {
|
|
451
|
-
return this.client.getStats();
|
|
973
|
+
async getStats() {
|
|
974
|
+
return await this.client.getStats();
|
|
452
975
|
}
|
|
453
976
|
}
|