mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-06 12:48:22 +00:00
fix: bind to 127.0.0.1 by default instead of 0.0.0.0
The daemon was binding to all network interfaces (0.0.0.0) by default because server.listen(port) without a host argument listens on all interfaces in Node.js. This exposed unauthenticated endpoints (/balance, /status, /providers, /wallet/balance, etc.) to anyone who could reach the port. Changes: - Add 'host' field to RoutstrdConfig (default: 127.0.0.1) - Add --host CLI flag to 'start' and 'restart' commands - Add ROUTSTRD_HOST env var support in parseArgs - Pass host to server.listen(port, host, callback) - Add startup warning when bound to 0.0.0.0 - Update getDaemonBaseUrl to use config.host (with 0.0.0.0 -> localhost fallback for client connections since 0.0.0.0 is not connectable) - Update start-daemon.ts health checks to use the correct host - Pass host through all startDaemon() call sites in cli.ts - Document --host flag, config field, and security implications in README Closes ROUTSTRD-BIND-ADDRESS
This commit is contained in:
@@ -74,6 +74,16 @@ With custom port:
|
||||
routstrd start --port 9000
|
||||
```
|
||||
|
||||
With a specific bind address (default is `127.0.0.1` for security):
|
||||
```sh
|
||||
routstrd start --host 0.0.0.0
|
||||
```
|
||||
|
||||
> ⚠️ **Security note:** By default, routstrd binds to `127.0.0.1` (localhost only).
|
||||
> Several endpoints (e.g. `/balance`, `/status`, `/providers`) do not require
|
||||
> authentication and will leak sensitive information if exposed. Only bind to
|
||||
> `0.0.0.0` if you have a firewall or reverse proxy in place.
|
||||
|
||||
With specific provider:
|
||||
```sh
|
||||
routstrd start --provider https://your-provider.com
|
||||
@@ -139,6 +149,7 @@ Configuration is stored in `~/.routstrd/config.json`:
|
||||
```json
|
||||
{
|
||||
"port": 8008,
|
||||
"host": "127.0.0.1",
|
||||
"provider": null,
|
||||
"cocodPath": null
|
||||
}
|
||||
@@ -149,6 +160,7 @@ Configuration is stored in `~/.routstrd/config.json`:
|
||||
- `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`)
|
||||
- `ROUTSTRD_SOCKET` - Socket path (default: `~/.routstrd/routstrd.sock`)
|
||||
- `ROUTSTRD_PID` - PID file path (default: `~/.routstrd/routstrd.pid`)
|
||||
- `ROUTSTRD_HOST` - Bind address override (default: `127.0.0.1`)
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
+9
-3
@@ -168,6 +168,7 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
||||
console.log("Starting routstrd daemon...");
|
||||
await startDaemon({
|
||||
port: String(config.port || 8008),
|
||||
host: config.host || undefined,
|
||||
provider: config.provider || undefined,
|
||||
});
|
||||
console.log("routstrd daemon restarted.");
|
||||
@@ -238,7 +239,7 @@ async function initDaemon(): Promise<void> {
|
||||
console.log(`Database will be stored at: ${DB_PATH}`);
|
||||
initializeWallet();
|
||||
|
||||
await startDaemon({ port: String(config.port || 8008) });
|
||||
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
||||
|
||||
await setupIntegration(config);
|
||||
|
||||
@@ -497,8 +498,9 @@ program
|
||||
.command("start")
|
||||
.description("Start the background daemon")
|
||||
.option("--port <port>", "Port to listen on")
|
||||
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
||||
.option("-p, --provider <provider>", "Default provider to use")
|
||||
.action(async (options: { port?: string; provider?: string }) => {
|
||||
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
||||
await requireLocalDaemon();
|
||||
const config = await loadConfig();
|
||||
// Stop a legacy cocod daemon (from older routstrd versions) before
|
||||
@@ -506,6 +508,7 @@ program
|
||||
await stopLegacyCocod();
|
||||
await startDaemon({
|
||||
port: options.port || String(config.port || 8008),
|
||||
host: options.host || config.host || undefined,
|
||||
provider: options.provider,
|
||||
});
|
||||
});
|
||||
@@ -1793,8 +1796,9 @@ program
|
||||
.command("restart")
|
||||
.description("Restart the background daemon")
|
||||
.option("--port <port>", "Port to listen on")
|
||||
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
||||
.option("-p, --provider <provider>", "Default provider to use")
|
||||
.action(async (options: { port?: string; provider?: string }) => {
|
||||
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
||||
await requireLocalDaemon();
|
||||
const config = await loadConfig();
|
||||
const wasRunning = await isDaemonRunning();
|
||||
@@ -1832,6 +1836,7 @@ program
|
||||
console.log("Starting daemon...");
|
||||
await startDaemon({
|
||||
port: options.port || String(config.port || 8008),
|
||||
host: options.host || config.host || undefined,
|
||||
provider: options.provider,
|
||||
});
|
||||
console.log("Daemon restarted.");
|
||||
@@ -1914,6 +1919,7 @@ program
|
||||
console.log("Starting daemon...");
|
||||
await startDaemon({
|
||||
port: String(config.port || 8008),
|
||||
host: config.host || undefined,
|
||||
provider: config.provider || undefined,
|
||||
});
|
||||
console.log(`Daemon restarted with mode '${selectedMode}'.`);
|
||||
|
||||
+9
-1
@@ -1,8 +1,10 @@
|
||||
export function parseArgs(argv: string[]): {
|
||||
port: number;
|
||||
host: string | null;
|
||||
provider: string | null;
|
||||
} {
|
||||
const portFlagIndex = argv.findIndex((arg) => arg === "--port");
|
||||
const hostFlagIndex = argv.findIndex((arg) => arg === "--host");
|
||||
const providerFlagIndex = argv.findIndex(
|
||||
(arg) => arg === "--provider" || arg === "-p",
|
||||
);
|
||||
@@ -11,9 +13,15 @@ export function parseArgs(argv: string[]): {
|
||||
portFlagIndex !== -1
|
||||
? Number.parseInt(argv[portFlagIndex + 1] || "8008", 10)
|
||||
: 8008;
|
||||
|
||||
// --host flag takes precedence, then ROUTSTRD_HOST env var
|
||||
const hostValue =
|
||||
hostFlagIndex !== -1 ? argv[hostFlagIndex + 1] : undefined;
|
||||
const host = hostValue ? hostValue.trim() : (process.env.ROUTSTRD_HOST || null);
|
||||
|
||||
const providerValue =
|
||||
providerFlagIndex !== -1 ? argv[providerFlagIndex + 1] : undefined;
|
||||
const provider = providerValue ? providerValue.trim() : null;
|
||||
|
||||
return { port, provider };
|
||||
return { port, host, provider };
|
||||
}
|
||||
|
||||
+17
-3
@@ -65,6 +65,7 @@ async function main(): Promise<void> {
|
||||
const config = await loadDaemonConfig();
|
||||
|
||||
const port = args.port;
|
||||
const host = args.host || config.host || "127.0.0.1";
|
||||
const provider = args.provider || config.provider;
|
||||
const requestResponseLogDir =
|
||||
process.env.ROUTSTRD_REQUEST_RESPONSE_LOG_DIR ||
|
||||
@@ -80,7 +81,7 @@ async function main(): Promise<void> {
|
||||
|
||||
await ensureDirs();
|
||||
|
||||
const updatedConfig = { ...config, port, provider };
|
||||
const updatedConfig = { ...config, port, host, provider };
|
||||
saveDaemonConfig(updatedConfig);
|
||||
|
||||
const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger });
|
||||
@@ -304,8 +305,21 @@ async function main(): Promise<void> {
|
||||
process.once("SIGINT", shutdownForSignal);
|
||||
process.once("SIGTERM", shutdownForSignal);
|
||||
|
||||
server.listen(port, async () => {
|
||||
logger.log(`Routstr daemon listening on http://localhost:${port}/v1`);
|
||||
// Warn when binding to all interfaces — unauthenticated endpoints expose
|
||||
// balance info, provider lists, and internal state to anyone who can reach
|
||||
// the port.
|
||||
if (host === "0.0.0.0") {
|
||||
logger.warn(
|
||||
"⚠️ WARNING: Daemon is bound to 0.0.0.0 (all network interfaces). " +
|
||||
"Several endpoints (e.g. /balance, /status, /providers) do not require " +
|
||||
"authentication and will leak sensitive information to anyone on the " +
|
||||
"network. Consider binding to 127.0.0.1 unless you have a firewall or " +
|
||||
"reverse proxy in place.",
|
||||
);
|
||||
}
|
||||
|
||||
server.listen(port, host, async () => {
|
||||
logger.log(`Routstr daemon listening on http://${host}:${port}/v1`);
|
||||
if (requestResponseLogDir) {
|
||||
logger.log(`Raw request/response logs: ${requestResponseLogDir}`);
|
||||
}
|
||||
|
||||
+23
-9
@@ -27,11 +27,11 @@ function readDaemonOutput(offset: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
async function isDaemonHealthy(port: string): Promise<boolean> {
|
||||
async function isDaemonHealthy(port: string, host: string = "localhost"): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
||||
try {
|
||||
const existing = await fetch(`http://localhost:${port}/health`, {
|
||||
const existing = await fetch(`http://${host}:${port}/health`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
return existing.ok;
|
||||
@@ -42,22 +42,34 @@ async function isDaemonHealthy(port: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** When the daemon binds to 0.0.0.0, the CLI must still connect via
|
||||
* localhost (or 127.0.0.1) since 0.0.0.0 is not a connectable address. */
|
||||
function clientHost(host: string | undefined): string {
|
||||
if (!host || host === "0.0.0.0") return "localhost";
|
||||
return host;
|
||||
}
|
||||
|
||||
async function startDaemonUnlocked(
|
||||
options: { port?: string; provider?: string },
|
||||
options: { port?: string; host?: string; provider?: string },
|
||||
): Promise<void> {
|
||||
const args: string[] = [];
|
||||
const port = options.port || "8008";
|
||||
const host = options.host || "127.0.0.1";
|
||||
const ch = clientHost(host);
|
||||
const pollIntervalMs = 250;
|
||||
const startupTimeoutMs = 10 * 60 * 1000;
|
||||
|
||||
if (await isDaemonHealthy(port)) {
|
||||
console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
||||
if (await isDaemonHealthy(port, ch)) {
|
||||
console.log(`Routstr daemon already running on http://${ch}:${port}/v1`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.port) {
|
||||
args.push("--port", options.port);
|
||||
}
|
||||
if (options.host) {
|
||||
args.push("--host", options.host);
|
||||
}
|
||||
if (options.provider) {
|
||||
args.push("--provider", options.provider);
|
||||
}
|
||||
@@ -98,7 +110,7 @@ async function startDaemonUnlocked(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isDaemonHealthy(port)) {
|
||||
if (await isDaemonHealthy(port, ch)) {
|
||||
console.log(`Routstr daemon started (PID: ${proc.pid}).`);
|
||||
return;
|
||||
}
|
||||
@@ -110,13 +122,15 @@ async function startDaemonUnlocked(
|
||||
}
|
||||
|
||||
export async function startDaemon(
|
||||
options: { port?: string; provider?: string } = {},
|
||||
options: { port?: string; host?: string; provider?: string } = {},
|
||||
): Promise<void> {
|
||||
const port = options.port || "8008";
|
||||
const host = options.host || "127.0.0.1";
|
||||
const ch = clientHost(host);
|
||||
const startupTimeoutMs = 10 * 60 * 1000;
|
||||
|
||||
if (await isDaemonHealthy(port)) {
|
||||
console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
||||
if (await isDaemonHealthy(port, ch)) {
|
||||
console.log(`Routstr daemon already running on http://${ch}:${port}/v1`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ export interface NwcConfig {
|
||||
|
||||
export interface RoutstrdConfig {
|
||||
port: number;
|
||||
/** Bind address for the HTTP server. Defaults to 127.0.0.1 (localhost only)
|
||||
* for security — set to 0.0.0.0 to listen on all interfaces. */
|
||||
host: string;
|
||||
provider: string | null;
|
||||
cocodPath: string | null;
|
||||
mode?: "xcashu" | "apikeys";
|
||||
@@ -58,6 +61,7 @@ export interface RoutstrdConfig {
|
||||
|
||||
export const DEFAULT_CONFIG: RoutstrdConfig = {
|
||||
port: 8008,
|
||||
host: "127.0.0.1",
|
||||
provider: null,
|
||||
cocodPath: null,
|
||||
mode: "apikeys",
|
||||
|
||||
@@ -30,9 +30,16 @@ export async function loadConfig(): Promise<RoutstrdConfig> {
|
||||
}
|
||||
|
||||
export function getDaemonBaseUrl(config: RoutstrdConfig): string {
|
||||
return (
|
||||
config.daemonUrl?.replace(/\/$/, "") || `http://localhost:${config.port}`
|
||||
);
|
||||
if (config.daemonUrl) {
|
||||
return config.daemonUrl.replace(/\/$/, "");
|
||||
}
|
||||
// When bound to 0.0.0.0, connect via localhost since 0.0.0.0 is not
|
||||
// a connectable address from a client perspective.
|
||||
const host =
|
||||
!config.host || config.host === "0.0.0.0"
|
||||
? "localhost"
|
||||
: config.host;
|
||||
return `http://${host}:${config.port}`;
|
||||
}
|
||||
|
||||
export function getAuthBaseUrl(config: RoutstrdConfig): string {
|
||||
@@ -145,6 +152,7 @@ export async function startDaemonProcess(): Promise<void> {
|
||||
const config = await loadConfig();
|
||||
await startDaemon({
|
||||
port: String(config.port || 8008),
|
||||
host: config.host || undefined,
|
||||
provider: config.provider || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user