Skip to content

Commit 86c958b

Browse files
committed
fix(API): mirror chia is_trusted_peer in /diagnostics trusted peers
Wallet get_connections does not return trusted status. Apply the same localhost, trusted_peers, and trusted_cidrs rules the chia CLI uses so localhost full-node connections are not falsely reported as untrusted.
1 parent b1e6974 commit 86c958b

2 files changed

Lines changed: 156 additions & 10 deletions

File tree

src/routes/diagnostics.js

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
* middleware returning 403), so there is no reduced-information path.
1212
*/
1313

14+
import net from 'net';
15+
1416
import _ from 'lodash';
1517

1618
import { getConfig, getConfigV2 } from '../utils/config-loader.js';
@@ -340,20 +342,77 @@ const normalizeNodeId = (id) => {
340342
return s.startsWith('0x') ? s.slice(2) : s;
341343
};
342344

345+
// Mirrors chia's chia.util.network.is_localhost. The wallet trusts any
346+
// localhost peer unconditionally, so a connection to 127.0.0.1 is trusted
347+
// even when wallet.trusted_peers is left at its default placeholder.
348+
const LOCALHOST_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1']);
349+
const stripBrackets = (host) => String(host ?? '').replace(/^\[/, '').replace(/\]$/, '');
350+
const isLocalhost = (peerHost) => LOCALHOST_HOSTS.has(stripBrackets(peerHost));
351+
343352
/**
344-
* Cross-reference connected wallet peers against the trusted_peers map in
345-
* the chia config.yaml. Returns a small object suitable for embedding in
346-
* the diagnostics response, including whether at least one connection is
347-
* trusted.
353+
* Mirror chia's chia.util.network.is_trusted_cidr: true when peerHost parses
354+
* to an IP that falls inside any configured CIDR. Never throws -- a malformed
355+
* CIDR entry is skipped rather than failing the whole diagnostics response.
356+
*/
357+
const isTrustedCidr = (peerHost, trustedCidrs) => {
358+
if (!Array.isArray(trustedCidrs) || trustedCidrs.length === 0) return false;
359+
const host = stripBrackets(peerHost);
360+
const hostType = net.isIP(host);
361+
if (hostType === 0) return false;
362+
const hostFamily = hostType === 6 ? 'ipv6' : 'ipv4';
363+
364+
for (const cidr of trustedCidrs) {
365+
const slash = String(cidr).lastIndexOf('/');
366+
if (slash === -1) continue;
367+
const network = String(cidr).slice(0, slash);
368+
const prefix = Number(String(cidr).slice(slash + 1));
369+
const netType = net.isIP(network);
370+
if (netType === 0 || !Number.isInteger(prefix)) continue;
371+
try {
372+
const blockList = new net.BlockList();
373+
blockList.addSubnet(network, prefix, netType === 6 ? 'ipv6' : 'ipv4');
374+
if (blockList.check(host, hostFamily)) return true;
375+
} catch {
376+
// Malformed CIDR (e.g. prefix out of range) -- skip it.
377+
}
378+
}
379+
return false;
380+
};
381+
382+
/**
383+
* Decide whether a single connection is trusted, mirroring chia's
384+
* chia.util.network.is_trusted_peer (and the `chia peer`/`chia wallet show`
385+
* CLI, which computes trust client-side -- the RPC does not return it):
386+
*
387+
* trusted = is_localhost(host) OR node_id in trusted_peers OR is_trusted_cidr
388+
*
389+
* Returns the matched reason so the diagnostics output explains *why* a peer
390+
* is trusted (e.g. a localhost full node is trusted even when trusted_peers
391+
* still holds the default placeholder node id).
392+
*/
393+
const classifyTrust = (peerHost, nodeId, normalizedTrustedSet, trustedCidrs) => {
394+
if (isLocalhost(peerHost)) return 'localhost';
395+
if (normalizedTrustedSet && normalizedTrustedSet.has(normalizeNodeId(nodeId))) return 'configured';
396+
if (isTrustedCidr(peerHost, trustedCidrs)) return 'cidr';
397+
return null;
398+
};
399+
400+
/**
401+
* Cross-reference connected wallet peers against the chia config.yaml trust
402+
* settings (wallet.trusted_peers + wallet.trusted_cidrs) plus chia's implicit
403+
* localhost rule. Returns a small object suitable for embedding in the
404+
* diagnostics response, including whether at least one connection is trusted.
348405
*/
349406
const buildTrustedPeerView = (connectionsResult, chiaConfigResult) => {
350407
const view = {
351408
configuredTrustedNodeIds: [],
409+
configuredTrustedCidrs: [],
352410
connected: [],
353411
hasTrustedConnection: false,
354412
};
355413

356414
let normalizedTrustedSet = null;
415+
let trustedCidrs = [];
357416
if (chiaConfigResult?.ok) {
358417
const trustedPeerMap = _.get(chiaConfigResult.value, 'wallet.trusted_peers', null);
359418
if (trustedPeerMap && typeof trustedPeerMap === 'object') {
@@ -364,18 +423,22 @@ const buildTrustedPeerView = (connectionsResult, chiaConfigResult) => {
364423
view.configuredTrustedNodeIds = keys;
365424
normalizedTrustedSet = new Set(keys.map(normalizeNodeId));
366425
}
426+
const configuredCidrs = _.get(chiaConfigResult.value, 'wallet.trusted_cidrs', null);
427+
if (Array.isArray(configuredCidrs)) {
428+
trustedCidrs = configuredCidrs;
429+
view.configuredTrustedCidrs = configuredCidrs;
430+
}
367431
} else if (chiaConfigResult) {
368432
view.chiaConfigError = chiaConfigResult.error;
369433
}
370434

371435
if (connectionsResult?.ok && connectionsResult.value?.success !== false) {
372436
const connections = connectionsResult.value?.connections || [];
373437
view.connected = connections.map((c) => {
374-
const trusted = !!(
375-
normalizedTrustedSet && normalizedTrustedSet.has(normalizeNodeId(c.nodeId))
376-
);
438+
const trustedReason = classifyTrust(c.peerHost, c.nodeId, normalizedTrustedSet, trustedCidrs);
439+
const trusted = trustedReason !== null;
377440
if (trusted) view.hasTrustedConnection = true;
378-
return { peerHost: c.peerHost, peerPort: c.peerPort, type: c.type, trusted };
441+
return { peerHost: c.peerHost, peerPort: c.peerPort, type: c.type, trusted, trustedReason };
379442
});
380443
} else if (connectionsResult) {
381444
view.connectionsError = connectionsResult.ok
@@ -880,6 +943,9 @@ export const __test = {
880943
collectSubscriptions,
881944
buildTrustedPeerView,
882945
normalizeNodeId,
946+
isLocalhost,
947+
isTrustedCidr,
948+
classifyTrust,
883949
collectOwnedStoreExpectations,
884950
escalateLostOwnedStores,
885951
StatusAccumulator,

tests/v2/integration/diagnostics.spec.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ describe('/diagnostics endpoint', function () {
218218
});
219219

220220
describe('buildTrustedPeerView', function () {
221-
const { buildTrustedPeerView } = (async () => null)(); // placeholder to keep diff small
222221
it('matches connected peers against trusted_peers regardless of 0x prefix or case', function () {
223222
const view = diagnostics.__test.buildTrustedPeerView(
224223
{
@@ -237,18 +236,99 @@ describe('/diagnostics endpoint', function () {
237236
);
238237
expect(view.hasTrustedConnection).to.equal(true);
239238
expect(view.connected[0].trusted).to.equal(true);
239+
expect(view.connected[0].trustedReason).to.equal('configured');
240240
expect(view.connected[1].trusted).to.equal(false);
241+
expect(view.connected[1].trustedReason).to.equal(null);
241242
expect(view.configuredTrustedNodeIds).to.deep.equal(['abcdef12']);
242243
});
243244

244245
it('reports empty trusted set when chia config has no trusted_peers', function () {
245246
const view = diagnostics.__test.buildTrustedPeerView(
246-
{ ok: true, value: { connections: [{ peerHost: 'h', peerPort: 0, type: 1, nodeId: 'aa' }] } },
247+
{ ok: true, value: { connections: [{ peerHost: '5.6.7.8', peerPort: 0, type: 1, nodeId: 'aa' }] } },
247248
{ ok: true, value: { wallet: {} } },
248249
);
249250
expect(view.hasTrustedConnection).to.equal(false);
250251
expect(view.connected[0].trusted).to.equal(false);
251252
expect(view.configuredTrustedNodeIds).to.deep.equal([]);
253+
expect(view.configuredTrustedCidrs).to.deep.equal([]);
254+
});
255+
256+
it('trusts a localhost peer even when trusted_peers holds only the default placeholder', function () {
257+
// Reproduces the real-world case: chia auto-trusts 127.0.0.1, but the
258+
// config still contains the example placeholder node id, so a
259+
// node-id-only check would wrongly report the peer as untrusted.
260+
const view = diagnostics.__test.buildTrustedPeerView(
261+
{
262+
ok: true,
263+
value: {
264+
connections: [
265+
{ peerHost: '127.0.0.1', peerPort: 58444, type: 1, nodeId: 'bf628b52deadbeef' },
266+
],
267+
},
268+
},
269+
{
270+
ok: true,
271+
value: {
272+
wallet: {
273+
trusted_peers: {
274+
'0ThisisanexampleNodeID7ff9d60f1c3fa270c213c0ad0cb89c01274634a7c3cb9': 'Does_not_matter',
275+
},
276+
},
277+
},
278+
},
279+
);
280+
expect(view.hasTrustedConnection).to.equal(true);
281+
expect(view.connected[0].trusted).to.equal(true);
282+
expect(view.connected[0].trustedReason).to.equal('localhost');
283+
});
284+
285+
it('trusts a peer whose IP falls inside a configured trusted CIDR', function () {
286+
const view = diagnostics.__test.buildTrustedPeerView(
287+
{
288+
ok: true,
289+
value: {
290+
connections: [
291+
{ peerHost: '10.0.0.5', peerPort: 8444, type: 1, nodeId: 'aa' },
292+
{ peerHost: '192.168.1.5', peerPort: 8444, type: 1, nodeId: 'bb' },
293+
],
294+
},
295+
},
296+
{ ok: true, value: { wallet: { trusted_cidrs: ['10.0.0.0/24'] } } },
297+
);
298+
expect(view.connected[0].trusted).to.equal(true);
299+
expect(view.connected[0].trustedReason).to.equal('cidr');
300+
expect(view.connected[1].trusted).to.equal(false);
301+
expect(view.configuredTrustedCidrs).to.deep.equal(['10.0.0.0/24']);
302+
});
303+
});
304+
305+
describe('isLocalhost', function () {
306+
it('recognizes the loopback hosts chia treats as localhost', function () {
307+
const { isLocalhost } = diagnostics.__test;
308+
expect(isLocalhost('127.0.0.1')).to.equal(true);
309+
expect(isLocalhost('localhost')).to.equal(true);
310+
expect(isLocalhost('::1')).to.equal(true);
311+
expect(isLocalhost('[::1]')).to.equal(true);
312+
expect(isLocalhost('0:0:0:0:0:0:0:1')).to.equal(true);
313+
expect(isLocalhost('1.2.3.4')).to.equal(false);
314+
expect(isLocalhost(null)).to.equal(false);
315+
});
316+
});
317+
318+
describe('isTrustedCidr', function () {
319+
it('matches IPv4 and IPv6 addresses inside configured ranges', function () {
320+
const { isTrustedCidr } = diagnostics.__test;
321+
expect(isTrustedCidr('10.0.0.5', ['10.0.0.0/24'])).to.equal(true);
322+
expect(isTrustedCidr('10.0.1.5', ['10.0.0.0/24'])).to.equal(false);
323+
expect(isTrustedCidr('2001:db8::1', ['2001:db8::/32'])).to.equal(true);
324+
});
325+
326+
it('never throws on malformed input', function () {
327+
const { isTrustedCidr } = diagnostics.__test;
328+
expect(isTrustedCidr('not-an-ip', ['10.0.0.0/24'])).to.equal(false);
329+
expect(isTrustedCidr('10.0.0.5', ['garbage', '10.0.0.0/99', '10.0.0.0'])).to.equal(false);
330+
expect(isTrustedCidr('10.0.0.5', [])).to.equal(false);
331+
expect(isTrustedCidr('10.0.0.5', null)).to.equal(false);
252332
});
253333
});
254334

0 commit comments

Comments
 (0)