Reject every non-loopback runtime listener
This commit is contained in:
@@ -21,7 +21,12 @@ class RemoteServer {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._config = opts;
|
||||
|
||||
const wssOpts = { port: opts.port, host: opts.host || '127.0.0.1', maxPayload: 256 * 1024 };
|
||||
const host = opts.host || '127.0.0.1';
|
||||
if (host !== '127.0.0.1' && host !== '::1') {
|
||||
reject(new Error('Remote server requires a loopback host'));
|
||||
return;
|
||||
}
|
||||
const wssOpts = { port: opts.port, host, maxPayload: 256 * 1024 };
|
||||
this._wss = new WebSocketServer(wssOpts, () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -3176,34 +3176,16 @@ function _buildDiagnosticHandler() {
|
||||
};
|
||||
}
|
||||
|
||||
function _getSuggestedRemoteHosts() {
|
||||
const os = require('os');
|
||||
const hosts = [];
|
||||
try {
|
||||
for (const entry of Object.values(os.networkInterfaces())) {
|
||||
for (const net of (entry || [])) {
|
||||
if (net && net.family === 'IPv4' && !net.internal && net.address) hosts.push(net.address);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return [...new Set(hosts)];
|
||||
}
|
||||
|
||||
function _diagAllowlist(diag) {
|
||||
return Array.isArray(diag && diag.allowlist) ? diag.allowlist.map((x) => String(x).trim()).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
function _diagBindHost(diag) {
|
||||
const mode = (diag && diag.bindMode) || 'local';
|
||||
if (mode === 'network' && _diagAllowlist(diag).length > 0) return '0.0.0.0';
|
||||
function _diagBindHost() {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
function _diagPublicHost(diag) {
|
||||
const explicit = String((diag && diag.publicHost) || '').trim();
|
||||
if (explicit) return explicit;
|
||||
if (_diagBindHost(diag) === '127.0.0.1') return '127.0.0.1';
|
||||
return _getSuggestedRemoteHosts()[0] || '127.0.0.1';
|
||||
function _diagPublicHost() {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
function buildDiagnosticCode(diag, fp) {
|
||||
@@ -3255,11 +3237,11 @@ ipcMain.handle('diagnostics:get-settings', () => {
|
||||
return {
|
||||
enabled: !!diag.enabled,
|
||||
port: diag.port || 9110,
|
||||
bindMode: diag.bindMode === 'network' ? 'network' : 'local',
|
||||
bindMode: 'local',
|
||||
bindAddress: _diagBindHost(diag),
|
||||
publicHost: diag.publicHost || '',
|
||||
publicHost: '127.0.0.1',
|
||||
allowlist: _diagAllowlist(diag),
|
||||
suggestedHosts: _getSuggestedRemoteHosts(),
|
||||
suggestedHosts: [],
|
||||
label: diag.label || require('os').hostname(),
|
||||
codeIssuedAt: diag.codeIssuedAt || 0,
|
||||
code: diag.token ? buildDiagnosticCode(diag) : ''
|
||||
@@ -3274,11 +3256,9 @@ ipcMain.handle('diagnostics:save-settings', async (_e, incoming) => {
|
||||
...cur,
|
||||
enabled: !!(incoming && incoming.enabled),
|
||||
port: (incoming && Number(incoming.port)) || cur.port || 9110,
|
||||
bindMode: (incoming && incoming.bindMode === 'network') ? 'network' : 'local',
|
||||
publicHost: (incoming && incoming.publicHost !== null && incoming.publicHost !== undefined) ? String(incoming.publicHost).trim() : (cur.publicHost || ''),
|
||||
allowlist: (incoming && Array.isArray(incoming.allowlist))
|
||||
? incoming.allowlist.map((x) => String(x).trim()).filter(Boolean)
|
||||
: _diagAllowlist(cur),
|
||||
bindMode: 'local',
|
||||
publicHost: '127.0.0.1',
|
||||
allowlist: [],
|
||||
label: (incoming && incoming.label !== null && incoming.label !== undefined) ? String(incoming.label) : cur.label
|
||||
};
|
||||
next.bindAddress = _diagBindHost(next);
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const os = require('os');
|
||||
const WebSocket = require('ws');
|
||||
const RemoteServer = require('../lib/remote-server');
|
||||
|
||||
const TOKEN = 'a'.repeat(64);
|
||||
|
||||
function firstLanIpv4() {
|
||||
for (const entry of Object.values(os.networkInterfaces())) {
|
||||
for (const net of (entry || [])) {
|
||||
if (net && net.family === 'IPv4' && !net.internal && net.address) return net.address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function startAgent(onDiagnosticRequest, extra) {
|
||||
const srv = new RemoteServer();
|
||||
return srv.start({ port: 0, host: '127.0.0.1', token: TOKEN, diagnosticMode: true, onDiagnosticRequest, ...(extra || {}) })
|
||||
@@ -88,19 +78,11 @@ test('a loopback diagnostic client connects even with a non-matching allowlist (
|
||||
ws.close(); agent.stop();
|
||||
});
|
||||
|
||||
test('network bind (0.0.0.0): an allowlisted non-loopback peer connects over a real socket (the Tailscale path)', async (t) => {
|
||||
const lan = firstLanIpv4();
|
||||
if (!lan) { t.skip('no non-internal IPv4 interface available'); return; }
|
||||
const agent = await startAgent(() => {}, { host: '0.0.0.0', allowlist: [lan] });
|
||||
const port = agent.getPort();
|
||||
const ws = new WebSocket(`ws://${lan}:${port}`);
|
||||
try {
|
||||
await new Promise((resolve, reject) => { ws.on('open', resolve); ws.on('error', reject); });
|
||||
ws.send(JSON.stringify({ type: 'auth', token: TOKEN, role: 'diagnostic' }));
|
||||
const ok = await once(ws, 'auth-ok');
|
||||
assert.ok(ok.clientId, 'allowlisted LAN peer authed over the 0.0.0.0 bind');
|
||||
} finally {
|
||||
ws.close(); agent.stop();
|
||||
test('diagnostic server rejects wildcard and non-loopback binds before opening a listener', async () => {
|
||||
for (const host of ['0.0.0.0', '::', '192.0.2.10']) {
|
||||
const server = new RemoteServer();
|
||||
await assert.rejects(server.start({ port: 0, host, token: TOKEN, diagnosticMode: true }), /requires a loopback host/);
|
||||
assert.equal(server._wss, null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user