mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
Use X-Forwarded-For header on trusted proxies
If the controller is behind a proxy then the socket.remoteAddress will be that of the proxy instead of the end client that connected. In this case the X-Forwarded-For header will contain the address of the end client, but this should only be relied upon if the party connecting is a trusted proxy, otherwise a malicious user could spoof the IP address that is logged by providing a forged X-Forwarded-For header. Implement a trusted proxy list consisting of a configured list of IP addresses and/or CIDR blocks and use the last address[1] in the X-Forwarded-For header if the remoteAddress address of the connecting party is in the trusted proxy list. [1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
This commit is contained in:
1 parent
c2a3d4361a
commit
5f25aad273
6 files changed
+191
-9
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type winston from "winston";
|
||||
import type { AddressInfo } from "net";
|
||||
import { BlockList, type AddressInfo, isIPv4, isIPv6 } from "net";
|
||||
import type { ControllerArgs } from "../controller";
|
||||
import express, { type Request, type Response, type NextFunction, type Application } from "express";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
@@ -74,6 +74,7 @@ export default class Controller {
|
||||
|
||||
/** WebSocket server */
|
||||
wsServer: WsServer;
|
||||
trustedProxies: BlockList;
|
||||
debugEvents: events.EventEmitter = new events.EventEmitter();
|
||||
private _events: events.EventEmitter = new events.EventEmitter();
|
||||
|
||||
@@ -153,6 +154,7 @@ export default class Controller {
|
||||
this.app.locals.controller = this;
|
||||
this.app.locals.streams = new Map();
|
||||
|
||||
this.trustedProxies = this.parseTrustedProxies();
|
||||
this.wsServer = new WsServer(this);
|
||||
|
||||
this.modStore.on("change", mod => {
|
||||
@@ -228,6 +230,8 @@ export default class Controller {
|
||||
this.onAutosaveIntervalChanged();
|
||||
} else if (field === "controller.system_metrics_interval") {
|
||||
this.onSystemMetricsIntervalChanged();
|
||||
} else if (field === "controller.trusted_proxies") {
|
||||
this.trustedProxies = this.parseTrustedProxies();
|
||||
}
|
||||
lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", field, curr, prev);
|
||||
});
|
||||
@@ -404,6 +408,34 @@ export default class Controller {
|
||||
logger.info("Goodbye");
|
||||
}
|
||||
|
||||
parseTrustedProxies() {
|
||||
const trustedProxies = new BlockList();
|
||||
const proxiesString = this.config.get("controller.trusted_proxies");
|
||||
if (proxiesString) {
|
||||
const proxies = proxiesString.split(",").map(s => s.trim());
|
||||
for (const proxy of proxies) {
|
||||
const [ip, prefix] = proxy.split("/");
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const type = isIPv4(ip) ? "ipv4" : isIPv6(ip) ? "ipv6" : undefined;
|
||||
if (!type) {
|
||||
logger.error(`Invalid proxy '${proxy}': not an IP address`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (prefix) {
|
||||
trustedProxies.addSubnet(ip, Number.parseInt(prefix, 10), type);
|
||||
} else {
|
||||
trustedProxies.addAddress(ip, type);
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(`Invalid proxy '${proxy}': ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trustedProxies;
|
||||
}
|
||||
|
||||
onSystemMetricsIntervalChanged() {
|
||||
if (this.systemMetricsInterval) {
|
||||
clearInterval(this.systemMetricsInterval);
|
||||
|
||||
@@ -59,6 +59,20 @@ export default class WsServer {
|
||||
);
|
||||
}
|
||||
|
||||
remoteAddr(req: IncomingMessage) {
|
||||
const remote = req.socket.remoteAddress ?? "";
|
||||
if (
|
||||
remote && req.headers["x-forwarded-for"]
|
||||
&& (
|
||||
(req.socket.remoteFamily === "IPv4" && this.controller.trustedProxies.check(remote, "ipv4"))
|
||||
|| (req.socket.remoteFamily === "IPv6" && this.controller.trustedProxies.check(remote, "ipv6"))
|
||||
)
|
||||
) {
|
||||
return (req.headers["x-forwarded-for"] as string).split(",").slice(-1)[0].trim();
|
||||
}
|
||||
return remote;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.stopAcceptingNewSessions = true;
|
||||
|
||||
@@ -100,7 +114,7 @@ export default class WsServer {
|
||||
}
|
||||
|
||||
handleConnection(socket: lib.WebSocketClusterio, req: IncomingMessage) {
|
||||
logger.verbose(`WsServer | new connection from ${req.socket.remoteAddress}`);
|
||||
logger.verbose(`WsServer | new connection from ${this.remoteAddr(req)}`);
|
||||
|
||||
// Track statistics
|
||||
wsConnectionsCounter.inc();
|
||||
@@ -136,7 +150,7 @@ export default class WsServer {
|
||||
this.pendingSockets.add(socket);
|
||||
|
||||
let timeoutId = setTimeout(() => {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after timing out on handshake`);
|
||||
logger.verbose(`WsServer | closing ${this.remoteAddr(req)} after timing out on handshake`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.terminate();
|
||||
this.pendingSockets.delete(socket);
|
||||
@@ -168,7 +182,7 @@ ${err.stack}`
|
||||
lib.Message.validate(json);
|
||||
message = lib.Message.fromJSON(json);
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid message`);
|
||||
logger.verbose(`WsServer | closing ${this.remoteAddr(req)} after receiving invalid message`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1002, `Invalid message: ${err.message}`);
|
||||
return;
|
||||
@@ -180,14 +194,14 @@ ${err.stack}`
|
||||
}
|
||||
|
||||
if (message.type !== "registerHost" && message.type !== "registerControl") {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid handshake`);
|
||||
logger.verbose(`WsServer | closing ${this.remoteAddr(req)} after receiving invalid handshake`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1002, "Bad handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stopAcceptingNewSessions) {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress}, server is shutting down`);
|
||||
logger.verbose(`WsServer | closing ${this.remoteAddr(req)}, server is shutting down`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1001, "Shutting down");
|
||||
return;
|
||||
@@ -247,7 +261,7 @@ ${err.stack}`
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
logger.verbose(`WsServer | authentication failed for ${this.remoteAddr(req)}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
return;
|
||||
@@ -306,7 +320,7 @@ ${err.stack}`
|
||||
user.checkPermission("core.control.connect");
|
||||
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
logger.verbose(`WsServer | authentication failed for ${this.remoteAddr(req)}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
return;
|
||||
@@ -316,7 +330,7 @@ ${err.stack}`
|
||||
this.nextControlId += 1;
|
||||
let [connector, sessionToken] = this.createSession(new lib.Address(lib.Address.control, id));
|
||||
|
||||
logger.verbose(`WsServer | registered control ${id} from ${req.socket.remoteAddress}`);
|
||||
logger.verbose(`WsServer | registered control ${id} from ${this.remoteAddr(req)}`);
|
||||
let connection = new ControlConnection(data, connector, this.controller, user, id);
|
||||
connector.on("close", () => {
|
||||
this.controlConnections.delete(id);
|
||||
|
||||
Reference in new issue
Block a user