Skip to content

Commit 9e44d52

Browse files
committed
fix: ReverseConnection waits for incoming data before piping data back
There was an issue in obscure conditions that caused certain packets to be out of the expected order for HTTP2 leading to a protocol error. This ensure that the `MAGIC` frame comes before the server's `SETTING` frame. Updated `NodeConnectionManager` lifecycle logging. `Proxy` and `NodeConnection` rejects connections to wildcard `0.0.0.0` and `::` addresses. Fixed up any tests broken by this change. Added a test to check if you can connect to the target through the proxies after the proxies have created the UTP connection. Fixes #369
1 parent db3b913 commit 9e44d52

18 files changed

Lines changed: 199 additions & 10 deletions

src/network/ConnectionReverse.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ class ConnectionReverse extends Connection {
295295
await this.stop();
296296
});
297297
this.tlsSocket.pipe(this.serverSocket, { end: false });
298-
this.serverSocket.pipe(this.tlsSocket, { end: false });
298+
this.tlsSocket.once('data', () => {
299+
this.serverSocket.pipe(this.tlsSocket as TLSSocket, { end: false });
300+
});
299301
this.clientCertChain = clientCertChain;
300302
this.logger.info('Composed Connection Reverse');
301303
} catch (e) {

src/network/Proxy.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,13 @@ class Proxy {
434434
timer,
435435
);
436436
} catch (e) {
437+
if (e instanceof networkErrors.ErrorProxyConnectInvalidUrl) {
438+
if (!clientSocket.destroyed) {
439+
await clientSocketEnd('HTTP/1.1 400 Bad Request\r\n' + '\r\n');
440+
clientSocket.destroy(e);
441+
}
442+
return;
443+
}
437444
if (e instanceof networkErrors.ErrorConnectionStartTimeout) {
438445
if (!clientSocket.destroyed) {
439446
await clientSocketEnd('HTTP/1.1 504 Gateway Timeout\r\n' + '\r\n');
@@ -519,6 +526,9 @@ class Proxy {
519526
proxyPort: Port,
520527
timer?: Timer,
521528
): Promise<ConnectionForward> {
529+
if (networkUtils.isHostWildcard(proxyHost)) {
530+
throw new networkErrors.ErrorProxyConnectInvalidUrl();
531+
}
522532
const proxyAddress = networkUtils.buildAddress(proxyHost, proxyPort);
523533
let conn: ConnectionForward | undefined;
524534
conn = this.connectionsForward.proxy.get(proxyAddress);
@@ -681,6 +691,9 @@ class Proxy {
681691
proxyPort: Port,
682692
timer?: Timer,
683693
): Promise<ConnectionReverse> {
694+
if (networkUtils.isHostWildcard(proxyHost)) {
695+
throw new networkErrors.ErrorProxyConnectInvalidUrl();
696+
}
684697
const proxyAddress = networkUtils.buildAddress(proxyHost, proxyPort);
685698
let conn = this.connectionsReverse.proxy.get(proxyAddress);
686699
if (conn != null) {

src/network/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ function isHost(host: any): host is Host {
2929
return isIPv4 || isIPv6;
3030
}
3131

32+
function isHostWildcard(host: Host): boolean {
33+
return host === '0.0.0.0' || host === '::';
34+
}
35+
3236
/**
3337
* Validates hostname as per RFC 1123
3438
*/
@@ -353,6 +357,7 @@ export {
353357
pingBuffer,
354358
pongBuffer,
355359
isHost,
360+
isHostWildcard,
356361
isHostname,
357362
isPort,
358363
toAuthToken,

src/nodes/NodeConnection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class NodeConnection<T extends GRPCClient> {
5959
logger?: Logger;
6060
}): Promise<NodeConnection<T>> {
6161
logger.info(`Creating ${this.name}`);
62+
// Checking if attempting to connect to a wildcard IP
63+
if (networkUtils.isHostWildcard(targetHost)) {
64+
throw new nodesErrors.ErrorNodeConnectionHostWildcard();
65+
}
6266
const proxyConfig = {
6367
host: proxy.getForwardHost(),
6468
port: proxy.getForwardPort(),

src/nodes/NodeConnectionManager.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class NodeConnectionManager {
129129
targetNodeId: NodeId,
130130
): Promise<ResourceAcquire<NodeConnection<GRPCClientAgent>>> {
131131
return async () => {
132-
const connAndLock = await this.createConnection(targetNodeId);
132+
const connAndLock = await this.getConnection(targetNodeId);
133133
// Acquire the read lock and the release function
134134
const release = await connAndLock.lock.acquireRead();
135135
// Resetting TTL timer
@@ -161,6 +161,11 @@ class NodeConnectionManager {
161161
return await withF(
162162
[await this.acquireConnection(targetNodeId)],
163163
async ([conn]) => {
164+
this.logger.info(
165+
`withConnF calling function with connection to ${nodesUtils.encodeNodeId(
166+
targetNodeId,
167+
)}`,
168+
);
164169
return await f(conn);
165170
},
166171
);
@@ -219,11 +224,11 @@ class NodeConnectionManager {
219224
* @param targetNodeId Id of node we are creating connection to
220225
* @returns ConnectionAndLock that was create or exists in the connection map.
221226
*/
222-
protected async createConnection(
227+
protected async getConnection(
223228
targetNodeId: NodeId,
224229
): Promise<ConnectionAndLock> {
225230
this.logger.info(
226-
`Creating connection to ${nodesUtils.encodeNodeId(targetNodeId)}`,
231+
`Getting connection to ${nodesUtils.encodeNodeId(targetNodeId)}`,
227232
);
228233
let connection: NodeConnection<GRPCClientAgent> | undefined;
229234
let lock: RWLock;
@@ -241,10 +246,13 @@ class NodeConnectionManager {
241246
targetNodeId.toString() as NodeIdString,
242247
);
243248
if (connAndLock != null && connAndLock.connection != null) {
249+
this.logger.info(
250+
`existing entry found for ${nodesUtils.encodeNodeId(targetNodeId)}`,
251+
);
244252
return connAndLock;
245253
}
246254
this.logger.info(
247-
`existing lock: creating connection to ${nodesUtils.encodeNodeId(
255+
`existing lock, creating connection to ${nodesUtils.encodeNodeId(
248256
targetNodeId,
249257
)}`,
250258
);
@@ -260,7 +268,7 @@ class NodeConnectionManager {
260268
);
261269
return await lock.withWrite(async () => {
262270
this.logger.info(
263-
`no existing entry: creating connection to ${nodesUtils.encodeNodeId(
271+
`no existing entry, creating connection to ${nodesUtils.encodeNodeId(
264272
targetNodeId,
265273
)}`,
266274
);
@@ -318,7 +326,9 @@ class NodeConnectionManager {
318326
nodeConnectionManager: this,
319327
destroyCallback,
320328
connConnectTime: this.connConnectTime,
321-
logger: this.logger.getChild(`${targetHost}:${targetAddress.port}`),
329+
logger: this.logger.getChild(
330+
`${NodeConnection.name} ${targetHost}:${targetAddress.port}`,
331+
),
322332
clientFactory: async (args) =>
323333
GRPCClientAgent.createGRPCClientAgent(args),
324334
});
@@ -521,7 +531,7 @@ class NodeConnectionManager {
521531
// Add the node to the database so that we can find its address in
522532
// call to getConnectionToNode
523533
await this.nodeGraph.setNode(nextNode.id, nextNode.address);
524-
await this.createConnection(nextNode.id);
534+
await this.getConnection(nextNode.id);
525535
} catch (e) {
526536
// If we can't connect to the node, then skip it
527537
continue;
@@ -620,7 +630,7 @@ class NodeConnectionManager {
620630
for (const seedNodeId of this.getSeedNodes()) {
621631
// Check if the connection is viable
622632
try {
623-
await this.createConnection(seedNodeId);
633+
await this.getConnection(seedNodeId);
624634
} catch (e) {
625635
if (e instanceof nodesErrors.ErrorNodeConnectionTimeout) continue;
626636
throw e;

src/nodes/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ class ErrorNodeConnectionManagerNotRunning extends ErrorNodes {
6262
exitCode = sysexits.USAGE;
6363
}
6464

65+
class ErrorNodeConnectionHostWildcard extends ErrorNodes {
66+
description = 'An IP wildcard was provided for the target host';
67+
exitCode = sysexits.USAGE;
68+
}
69+
6570
export {
6671
ErrorNodes,
6772
ErrorNodeGraphRunning,
@@ -76,4 +81,5 @@ export {
7681
ErrorNodeConnectionInfoNotExist,
7782
ErrorNodeConnectionPublicKeyNotFound,
7883
ErrorNodeConnectionManagerNotRunning,
84+
ErrorNodeConnectionHostWildcard,
7985
};

tests/agent/service/nodesCrossSignClaim.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ describe('nodesCrossSignClaim', () => {
5252
rootKeyPairBits: 2048,
5353
},
5454
seedNodes: {}, // Explicitly no seed nodes on startup
55+
networkConfig: {
56+
proxyHost: '127.0.0.1' as Host,
57+
},
5558
logger,
5659
});
5760
localId = pkAgent.keyManager.getNodeId();
@@ -63,6 +66,9 @@ describe('nodesCrossSignClaim', () => {
6366
rootKeyPairBits: 2048,
6467
},
6568
seedNodes: {}, // Explicitly no seed nodes on startup
69+
networkConfig: {
70+
proxyHost: '127.0.0.1' as Host,
71+
},
6672
logger,
6773
});
6874
remoteId = remoteNode.keyManager.getNodeId();

tests/bin/vaults/vaults.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { NodeIdEncoded, NodeAddress, NodeInfo } from '@/nodes/types';
22
import type { VaultId, VaultName } from '@/vaults/types';
3+
import type { Host } from '@/network/types';
34
import os from 'os';
45
import path from 'path';
56
import fs from 'fs';
@@ -207,6 +208,9 @@ describe('CLI vaults', () => {
207208
const targetPolykeyAgent = await PolykeyAgent.createPolykeyAgent({
208209
password,
209210
nodePath: dataDir2,
211+
networkConfig: {
212+
proxyHost: '127.0.0.1' as Host,
213+
},
210214
logger: logger,
211215
});
212216
const vaultId = await targetPolykeyAgent.vaultManager.createVault(
@@ -701,6 +705,9 @@ describe('CLI vaults', () => {
701705
password,
702706
logger,
703707
nodePath: path.join(dataDir, 'remoteOnline'),
708+
networkConfig: {
709+
proxyHost: '127.0.0.1' as Host,
710+
},
704711
});
705712
const remoteOnlineNodeId = remoteOnline.keyManager.getNodeId();
706713
const remoteOnlineNodeIdEncoded =

tests/network/Proxy.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ describe(Proxy.name, () => {
208208
`127.0.0.1:80?nodeId=${encodeURIComponent(nodeIdSomeEncoded)}`,
209209
),
210210
).rejects.toThrow('407');
211+
// Wildcard as host
212+
await expect(() =>
213+
httpConnect(
214+
proxy.getForwardHost(),
215+
proxy.getForwardPort(),
216+
authToken,
217+
`0.0.0.0:80?nodeId=${encodeURIComponent(nodeIdSomeEncoded)}`,
218+
),
219+
).rejects.toThrow('400');
211220
// No node id
212221
await expect(() =>
213222
httpConnect(

tests/nodes/NodeConnection.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,47 @@ describe(`${NodeConnection.name} test`, () => {
433433
});
434434
await conn.destroy();
435435
});
436+
test('connects to its target but proxies connect first', async () => {
437+
await clientproxy.openConnectionForward(
438+
targetNodeId,
439+
localHost,
440+
targetPort,
441+
);
442+
const conn = await NodeConnection.createNodeConnection({
443+
targetNodeId: targetNodeId,
444+
targetHost: localHost,
445+
targetPort: targetPort,
446+
proxy: clientproxy,
447+
keyManager: clientKeyManager,
448+
nodeConnectionManager: dummyNodeConnectionManager,
449+
destroyCallback,
450+
logger: logger,
451+
clientFactory: async (args) =>
452+
GRPCClientAgent.createGRPCClientAgent(args),
453+
});
454+
// Because the connection will not have enough time to compose before we
455+
// attempt to acquire the connection info, we need to wait and poll it
456+
const connInfo = await poll<ConnectionInfo | undefined>(
457+
async () => {
458+
return serverProxy.getConnectionInfoByProxy(localHost, sourcePort);
459+
},
460+
(e) => {
461+
if (e instanceof networkErrors.ErrorConnectionNotComposed) return false;
462+
if (e instanceof networkErrors.ErrorConnectionNotRunning) return false;
463+
return true;
464+
},
465+
);
466+
expect(connInfo).toBeDefined();
467+
expect(connInfo).toMatchObject({
468+
remoteNodeId: sourceNodeId,
469+
remoteCertificates: expect.any(Array),
470+
localHost: localHost,
471+
localPort: targetPort,
472+
remoteHost: localHost,
473+
remotePort: sourcePort,
474+
});
475+
await conn.destroy();
476+
});
436477
test('grpcCall after connection drops', async () => {
437478
let nodeConnection: NodeConnection<GRPCClientAgent> | undefined;
438479
let polykeyAgent: PolykeyAgent | undefined;
@@ -441,6 +482,9 @@ describe(`${NodeConnection.name} test`, () => {
441482
password,
442483
nodePath: path.join(dataDir, 'PolykeyAgent3'),
443484
logger: logger,
485+
networkConfig: {
486+
proxyHost: localHost,
487+
},
444488
});
445489
// Have a nodeConnection try to connect to it
446490
const killSelf = jest.fn();
@@ -629,6 +673,9 @@ describe(`${NodeConnection.name} test`, () => {
629673
password,
630674
nodePath: path.join(dataDir, 'PolykeyAgent3'),
631675
logger: logger,
676+
networkConfig: {
677+
proxyHost: localHost,
678+
},
632679
});
633680
// Have a nodeConnection try to connect to it
634681
const killSelf = jest.fn();

0 commit comments

Comments
 (0)