Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/constants/source/environment-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const EnvironmentVariableNames = [
"MAINSAIL_P2P_DEVELOPMENT_MODE_ENABLED",
"MAINSAIL_P2P_HOST",
"MAINSAIL_P2P_LOG_LEVEL",
"MAINSAIL_P2P_MAX_CONNECTIONS",
"MAINSAIL_P2P_MAX_PEERS_BROADCAST",
"MAINSAIL_P2P_MAX_PEERS_SAME_SUBNET",
"MAINSAIL_P2P_MIN_NETWORK_REACH",
Expand Down
1 change: 1 addition & 0 deletions packages/p2p/source/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const defaults = {
enabled: Environment.isTrue(EnvironmentVariables.MAINSAIL_P2P_DEVELOPMENT_MODE_ENABLED),
},
getBlocksTimeout: 30_000,
maxConnections: Environment.get(EnvironmentVariables.MAINSAIL_P2P_MAX_CONNECTIONS, 500),
maxPeersBroadcast: Environment.get(EnvironmentVariables.MAINSAIL_P2P_MAX_PEERS_BROADCAST, 4),
maxSameSubnetPeers: Environment.get(EnvironmentVariables.MAINSAIL_P2P_MAX_PEERS_SAME_SUBNET, 5),
minimumNetworkReach: Environment.get(EnvironmentVariables.MAINSAIL_P2P_MIN_NETWORK_REACH, 20),
Expand Down
37 changes: 37 additions & 0 deletions packages/p2p/source/hapi-nes/listener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ describeSkip("Listener", ({ it, spy, beforeEach, assert, nock, each }) => {
await server.stop();
});

it("limits number of connections per ip", async () => {
const server = Hapi.server();
await server.register({ plugin: plugin, options: { maxConnectionsPerIp: 1 } });

await server.start();
const client = new Client("http://127.0.0.1:" + server.info.port);
await client.connect();

const client2 = new Client("http://127.0.0.1:" + server.info.port);
client2.onError = Hoek.ignore;

await assert.rejects(() => client2.connect());

await client.disconnect();
await client2.disconnect();
await server.stop();
});

it("exempts whitelisted ips from the per-ip connection limit", async () => {
const server = Hapi.server();
await server.register({
plugin: plugin,
options: { maxConnectionsPerIp: 1, maxConnectionsPerIpWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"] },
});

await server.start();
const client = new Client("http://127.0.0.1:" + server.info.port);
await client.connect();

const client2 = new Client("http://127.0.0.1:" + server.info.port);
await client2.connect();

await client.disconnect();
await client2.disconnect();
await server.stop();
});

it("rejects unknown origin", async () => {
const server = Hapi.server();
await server.register({ plugin: plugin, options: { origin: ["http://127.0.0.1:12345"] } });
Expand Down
27 changes: 26 additions & 1 deletion packages/p2p/source/hapi-nes/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class Listener {
private _heartbeat;
private _beatTimeout;
private _wss;
private _connectionsPerIp;

public constructor(server, settings) {
this._server = server;
Expand All @@ -31,6 +32,7 @@ export class Listener {
this._heartbeat = null;
this._beatTimeout = null;
this._stopped = false;
this._connectionsPerIp = new Set();

// WebSocket listener

Expand All @@ -50,7 +52,8 @@ export class Listener {

if (
this._stopped ||
(this._settings.maxConnections && this._sockets.length() >= this._settings.maxConnections)
(this._settings.maxConnections && this._sockets.length() >= this._settings.maxConnections) ||
this._hasExceededMaxConnectionsPerIp(req.socket.remoteAddress)
) {
return ws.close();
}
Expand Down Expand Up @@ -127,9 +130,11 @@ export class Listener {
const socket = new Socket(ws, req, this);

this._sockets.add(socket);
this._trackConnection(socket.info.remoteAddress);

ws.once("close", async (code, message) => {
this._sockets.remove(socket);
this._untrackConnection(socket.info.remoteAddress);

if (this._settings.onDisconnection) {
this._settings.onDisconnection(socket);
Expand All @@ -138,6 +143,26 @@ export class Listener {
socket._removed.attend();
});
}

private _hasExceededMaxConnectionsPerIp(ip) {
return this._connectionsPerIp.has(ip);
}

private _trackConnection(ip) {
if (ip === undefined) {
return;
}

this._connectionsPerIp.set(ip);
}

private _untrackConnection(ip) {
if (ip === undefined) {
return;
}

this._connectionsPerIp.delete(ip);
}
}

// Sockets manager
Expand Down
19 changes: 19 additions & 0 deletions packages/p2p/source/service-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe<{

assert.array(result.value.blacklist);
assert.number(result.value.getBlocksTimeout);
assert.number(result.value.maxConnections);
assert.number(result.value.maxPeersBroadcast);
assert.number(result.value.maxSameSubnetPeers);
assert.number(result.value.minimumNetworkReach);
Expand Down Expand Up @@ -243,6 +244,24 @@ describe<{
assert.equal(result.error?.message, '"maxSameSubnetPeers" must be a number');
});

it("should parse process.env.MAINSAIL_P2P_MAX_CONNECTIONS", async ({ serviceProvider }) => {
process.env.MAINSAIL_P2P_MAX_CONNECTIONS = "2500";

const result = serviceProvider.configSchema().validate(await importDefaults());

assert.undefined(result.error);
assert.equal(result.value.maxConnections, 2500);
});

it("should throw if process.env.MAINSAIL_P2P_MAX_CONNECTIONS is not number", async ({ serviceProvider }) => {
process.env.MAINSAIL_P2P_MAX_CONNECTIONS = "dummy";

const result = serviceProvider.configSchema().validate(await importDefaults());

assert.defined(result.error);
assert.equal(result.error?.message, '"maxConnections" must be a number');
});

it("should parse process.env.MAINSAIL_P2P_MAX_PEERS_BROADCAST", async ({ serviceProvider }) => {
process.env.MAINSAIL_P2P_MAX_PEERS_BROADCAST = "10";

Expand Down
1 change: 1 addition & 0 deletions packages/p2p/source/service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class ServiceProvider extends Providers.ServiceProvider {
disableDiscovery: Joi.bool(),
getBlocksTimeout: Joi.number().integer().min(0).required(),
ignoreMinimumNetworkReach: Joi.bool(),
maxConnections: Joi.number().integer().min(1).required(),
maxPeersBroadcast: Joi.number().integer().min(0).required(),
maxSameSubnetPeers: Joi.number().integer().min(0).required(),
minimumNetworkReach: Joi.number().integer().min(0).required(),
Expand Down
7 changes: 6 additions & 1 deletion packages/p2p/source/socket-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Contracts } from "@mainsail/contracts";

import { Plugin, Server as HapiServer, ServerInjectOptions, ServerInjectResponse, ServerRoute } from "@hapi/hapi";
import { Identifiers } from "@mainsail/constants";
import { inject, injectable, multiInject } from "@mainsail/container";
import { inject, injectable, multiInject, tagged } from "@mainsail/container";
import { ensureError } from "@mainsail/utils";

import { constants } from "../constants.js";
Expand All @@ -26,6 +26,10 @@ export class Server implements Contracts.P2P.Server {
@inject(Identifiers.Services.Log.Service)
private readonly logger!: Contracts.Kernel.Logger;

@inject(Identifiers.ServiceProvider.Configuration)
@tagged("plugin", "p2p")
private readonly configuration!: Contracts.Kernel.PluginConfiguration;

@multiInject(Identifiers.P2P.Routes)
private readonly routes!: Contracts.P2P.Route[];

Expand All @@ -43,6 +47,7 @@ export class Server implements Contracts.P2P.Server {
this.server.app = this.app;
await this.server.register({
options: {
maxConnections: this.configuration.getRequired<number | false>("maxConnections"),
maxPayload: constants.MAX_PAYLOAD_SERVER,
},
plugin: hapiNesPlugin,
Expand Down
Loading