Skip to content

Commit b003033

Browse files
committed
fix(API): report unknown trust when chia config is unavailable
When CADT cannot read the chia config.yaml (e.g. CADT and chia run in separate containers), it has no trusted_peers/trusted_cidrs to evaluate. Report non-localhost peers as trusted "unknown" instead of false and suppress the "severely degraded" warning in that case, so a split deployment is not falsely flagged. Localhost peers stay trusted since that is determinable without the config. Also harden trusted_cidrs parsing to mirror chia: reject non-decimal prefixes (an empty prefix like "10.0.0.0/" was misread as /0 and trusted every peer) and treat a bare IP as a /32 or /128 host route. Treat an unreadable wallet get_connections as unknown rather than untrusted.
1 parent 86c958b commit b003033

2 files changed

Lines changed: 204 additions & 19 deletions

File tree

src/routes/diagnostics.js

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -362,12 +362,29 @@ const isTrustedCidr = (peerHost, trustedCidrs) => {
362362
const hostFamily = hostType === 6 ? 'ipv6' : 'ipv4';
363363

364364
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));
365+
const cidrStr = String(cidr);
366+
const slash = cidrStr.lastIndexOf('/');
367+
let network;
368+
let prefix;
369+
if (slash === -1) {
370+
// Bare IP, no prefix. chia's ip_network() treats this as a host route
371+
// (/32 for IPv4, /128 for IPv6), so trust an exact-match peer.
372+
network = cidrStr;
373+
const bareType = net.isIP(network);
374+
if (bareType === 0) continue;
375+
prefix = bareType === 6 ? 128 : 32;
376+
} else {
377+
network = cidrStr.slice(0, slash);
378+
const prefixStr = cidrStr.slice(slash + 1);
379+
// Require an explicit decimal prefix. Number('') and Number(' ') both
380+
// coerce to 0, which would silently widen a typo'd CIDR like "10.0.0.0/"
381+
// into /0 and trust every peer; hex/float forms ("/0x10", "/1.5") would
382+
// also be misread. chia's ipaddress parser rejects all of these.
383+
if (!/^\d+$/.test(prefixStr)) continue;
384+
prefix = Number(prefixStr);
385+
}
369386
const netType = net.isIP(network);
370-
if (netType === 0 || !Number.isInteger(prefix)) continue;
387+
if (netType === 0) continue;
371388
try {
372389
const blockList = new net.BlockList();
373390
blockList.addSubnet(network, prefix, netType === 6 ? 'ipv6' : 'ipv4');
@@ -386,15 +403,23 @@ const isTrustedCidr = (peerHost, trustedCidrs) => {
386403
*
387404
* trusted = is_localhost(host) OR node_id in trusted_peers OR is_trusted_cidr
388405
*
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).
406+
* Returns `{ trusted, reason }` where `trusted` is true / false / 'unknown'.
407+
* Localhost is detectable from the peer host alone, so it is evaluated even
408+
* without the chia config. The trusted_peers / trusted_cidrs rules require
409+
* the config, so when it is unavailable (`configReadable === false`) a
410+
* non-localhost peer is 'unknown' rather than false -- CADT and chia can run
411+
* in separate containers where CADT has no access to the chia config.yaml,
412+
* and reporting 'untrusted' there would be wrong (the peer may well be
413+
* trusted via trusted_cidrs we simply can't see).
392414
*/
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;
415+
const classifyTrust = (peerHost, nodeId, normalizedTrustedSet, trustedCidrs, configReadable) => {
416+
if (isLocalhost(peerHost)) return { trusted: true, reason: 'localhost' };
417+
if (!configReadable) return { trusted: 'unknown', reason: 'chia-config-unavailable' };
418+
if (normalizedTrustedSet && normalizedTrustedSet.has(normalizeNodeId(nodeId))) {
419+
return { trusted: true, reason: 'configured' };
420+
}
421+
if (isTrustedCidr(peerHost, trustedCidrs)) return { trusted: true, reason: 'cidr' };
422+
return { trusted: false, reason: null };
398423
};
399424

400425
/**
@@ -409,11 +434,17 @@ const buildTrustedPeerView = (connectionsResult, chiaConfigResult) => {
409434
configuredTrustedCidrs: [],
410435
connected: [],
411436
hasTrustedConnection: false,
437+
// True when trust could not be determined -- either the chia config was
438+
// unavailable (so trusted_peers/trusted_cidrs can't be checked) or the
439+
// wallet connections couldn't be enumerated at all. Distinguishes "known
440+
// untrusted" from "can't tell" so callers don't raise a false alarm.
441+
trustUnknown: false,
412442
};
413443

444+
const configReadable = chiaConfigResult?.ok === true;
414445
let normalizedTrustedSet = null;
415446
let trustedCidrs = [];
416-
if (chiaConfigResult?.ok) {
447+
if (configReadable) {
417448
const trustedPeerMap = _.get(chiaConfigResult.value, 'wallet.trusted_peers', null);
418449
if (trustedPeerMap && typeof trustedPeerMap === 'object') {
419450
// trusted_peers is `{ <peer_node_id>: <cert_path or 'Does_not_matter'> }`
@@ -435,20 +466,43 @@ const buildTrustedPeerView = (connectionsResult, chiaConfigResult) => {
435466
if (connectionsResult?.ok && connectionsResult.value?.success !== false) {
436467
const connections = connectionsResult.value?.connections || [];
437468
view.connected = connections.map((c) => {
438-
const trustedReason = classifyTrust(c.peerHost, c.nodeId, normalizedTrustedSet, trustedCidrs);
439-
const trusted = trustedReason !== null;
440-
if (trusted) view.hasTrustedConnection = true;
441-
return { peerHost: c.peerHost, peerPort: c.peerPort, type: c.type, trusted, trustedReason };
469+
const { trusted, reason } = classifyTrust(
470+
c.peerHost,
471+
c.nodeId,
472+
normalizedTrustedSet,
473+
trustedCidrs,
474+
configReadable,
475+
);
476+
if (trusted === true) view.hasTrustedConnection = true;
477+
if (trusted === 'unknown') view.trustUnknown = true;
478+
return { peerHost: c.peerHost, peerPort: c.peerPort, type: c.type, trusted, trustedReason: reason };
442479
});
443480
} else if (connectionsResult) {
444481
view.connectionsError = connectionsResult.ok
445482
? connectionsResult.value?.error || 'wallet connections unavailable'
446483
: connectionsResult.error;
484+
// Connections couldn't be enumerated -- we can't tell whether a trusted
485+
// peer exists, so treat it as unknown rather than "definitively untrusted".
486+
view.trustUnknown = true;
447487
}
448488

449489
return view;
450490
};
451491

492+
/**
493+
* Whether to warn that the wallet is not connected to a trusted full-node
494+
* peer. Only warn when we can *definitively* say there is no trusted peer:
495+
* the wallet is reachable, nothing is trusted, and no peer's trust is
496+
* unknown. When trust is unknown (chia config unreadable, e.g. split
497+
* CADT/chia containers) we stay silent rather than raise a false warning.
498+
*/
499+
const shouldWarnNoTrustedPeer = (walletReachable, trustedFullNodePeers) => {
500+
if (!walletReachable || !trustedFullNodePeers) return false;
501+
if (trustedFullNodePeers.hasTrustedConnection) return false;
502+
if (trustedFullNodePeers.trustUnknown) return false;
503+
return true;
504+
};
505+
452506
/**
453507
* Build the full /diagnostics response payload.
454508
*
@@ -899,7 +953,7 @@ export const getDiagnosticsResponse = async () => {
899953
}
900954
}
901955

902-
if (walletReachable && walletSection.trustedFullNodePeers?.hasTrustedConnection === false) {
956+
if (shouldWarnNoTrustedPeer(walletReachable, walletSection.trustedFullNodePeers)) {
903957
walletStatus.escalate(
904958
'warning',
905959
'Performance is severely degraded when the Chia wallet is not connected to a trusted full node peer',
@@ -946,6 +1000,7 @@ export const __test = {
9461000
isLocalhost,
9471001
isTrustedCidr,
9481002
classifyTrust,
1003+
shouldWarnNoTrustedPeer,
9491004
collectOwnedStoreExpectations,
9501005
escalateLostOwnedStores,
9511006
StatusAccumulator,

tests/v2/integration/diagnostics.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,113 @@ describe('/diagnostics endpoint', function () {
298298
expect(view.connected[0].trusted).to.equal(true);
299299
expect(view.connected[0].trustedReason).to.equal('cidr');
300300
expect(view.connected[1].trusted).to.equal(false);
301+
expect(view.connected[1].trustedReason).to.equal(null);
301302
expect(view.configuredTrustedCidrs).to.deep.equal(['10.0.0.0/24']);
302303
});
304+
305+
it('marks trust unknown when wallet connections cannot be enumerated', function () {
306+
// Wallet reachable but get_connections failed: we can't tell whether a
307+
// trusted peer exists, so trust is unknown rather than untrusted.
308+
const view = diagnostics.__test.buildTrustedPeerView(
309+
{ ok: false, error: 'wallet RPC refused connection' },
310+
{ ok: true, value: { wallet: { trusted_cidrs: ['10.0.0.0/8'] } } },
311+
);
312+
expect(view.connected).to.deep.equal([]);
313+
expect(view.hasTrustedConnection).to.equal(false);
314+
expect(view.trustUnknown).to.equal(true);
315+
expect(view.connectionsError).to.be.a('string').and.not.empty;
316+
});
317+
318+
it('reports unknown (not untrusted) for non-localhost peers when chia config is unreadable', function () {
319+
// Split-deployment case: CADT and chia run in separate containers and
320+
// CADT cannot read the chia config.yaml, so trusted_peers/trusted_cidrs
321+
// are unavailable. A non-localhost peer's trust is unknown, not false.
322+
const view = diagnostics.__test.buildTrustedPeerView(
323+
{
324+
ok: true,
325+
value: {
326+
connections: [
327+
{ peerHost: '10.48.83.174', peerPort: 58444, type: 1, nodeId: 'a676d602' },
328+
],
329+
},
330+
},
331+
{ ok: false, error: "ENOENT: no such file or directory, open '/root/.chia/mainnet/config/config.yaml'" },
332+
);
333+
expect(view.connected[0].trusted).to.equal('unknown');
334+
expect(view.connected[0].trustedReason).to.equal('chia-config-unavailable');
335+
expect(view.hasTrustedConnection).to.equal(false);
336+
expect(view.trustUnknown).to.equal(true);
337+
expect(view.chiaConfigError).to.be.a('string').and.not.empty;
338+
});
339+
340+
it('still trusts a localhost peer when chia config is unreadable', function () {
341+
// Localhost is determinable from the peer host alone, so an
342+
// unreadable config does not make a localhost peer unknown.
343+
const view = diagnostics.__test.buildTrustedPeerView(
344+
{
345+
ok: true,
346+
value: {
347+
connections: [
348+
{ peerHost: '127.0.0.1', peerPort: 58444, type: 1, nodeId: 'aa' },
349+
],
350+
},
351+
},
352+
{ ok: false, error: 'ENOENT' },
353+
);
354+
expect(view.connected[0].trusted).to.equal(true);
355+
expect(view.connected[0].trustedReason).to.equal('localhost');
356+
expect(view.hasTrustedConnection).to.equal(true);
357+
expect(view.trustUnknown).to.equal(false);
358+
});
359+
});
360+
361+
describe('shouldWarnNoTrustedPeer', function () {
362+
it('warns only when trust is known and no peer is trusted', function () {
363+
expect(
364+
diagnostics.__test.shouldWarnNoTrustedPeer(true, { hasTrustedConnection: false, trustUnknown: false }),
365+
).to.equal(true);
366+
});
367+
368+
it('does not warn when a trusted connection exists', function () {
369+
expect(
370+
diagnostics.__test.shouldWarnNoTrustedPeer(true, { hasTrustedConnection: true, trustUnknown: false }),
371+
).to.equal(false);
372+
});
373+
374+
it('does not warn when trust is unknown (config unreadable)', function () {
375+
expect(
376+
diagnostics.__test.shouldWarnNoTrustedPeer(true, { hasTrustedConnection: false, trustUnknown: true }),
377+
).to.equal(false);
378+
});
379+
380+
it('does not warn when the wallet is unreachable', function () {
381+
expect(
382+
diagnostics.__test.shouldWarnNoTrustedPeer(false, { hasTrustedConnection: false, trustUnknown: false }),
383+
).to.equal(false);
384+
});
385+
});
386+
387+
describe('classifyTrust', function () {
388+
it('returns unknown for a non-localhost peer when config is not readable', function () {
389+
expect(diagnostics.__test.classifyTrust('1.2.3.4', 'aa', null, [], false)).to.deep.equal({
390+
trusted: 'unknown',
391+
reason: 'chia-config-unavailable',
392+
});
393+
});
394+
395+
it('returns localhost trust even when config is not readable', function () {
396+
expect(diagnostics.__test.classifyTrust('127.0.0.1', 'aa', null, [], false)).to.deep.equal({
397+
trusted: true,
398+
reason: 'localhost',
399+
});
400+
});
401+
402+
it('returns false for an untrusted peer when config is readable', function () {
403+
expect(diagnostics.__test.classifyTrust('1.2.3.4', 'aa', new Set(['bb']), [], true)).to.deep.equal({
404+
trusted: false,
405+
reason: null,
406+
});
407+
});
303408
});
304409

305410
describe('isLocalhost', function () {
@@ -323,13 +428,38 @@ describe('/diagnostics endpoint', function () {
323428
expect(isTrustedCidr('2001:db8::1', ['2001:db8::/32'])).to.equal(true);
324429
});
325430

431+
it('treats a host-bits-set CIDR like chia (strict=False masks host bits)', function () {
432+
// chia uses ip_network(cidr, strict=False); "10.0.0.5/24" masks to
433+
// 10.0.0.0/24 rather than being rejected.
434+
const { isTrustedCidr } = diagnostics.__test;
435+
expect(isTrustedCidr('10.0.0.1', ['10.0.0.5/24'])).to.equal(true);
436+
});
437+
438+
it('treats a bare IP as a host route (/32 or /128) like chia', function () {
439+
const { isTrustedCidr } = diagnostics.__test;
440+
expect(isTrustedCidr('10.0.0.5', ['10.0.0.5'])).to.equal(true);
441+
expect(isTrustedCidr('10.0.0.6', ['10.0.0.5'])).to.equal(false);
442+
expect(isTrustedCidr('2001:db8::1', ['2001:db8::1'])).to.equal(true);
443+
});
444+
326445
it('never throws on malformed input', function () {
327446
const { isTrustedCidr } = diagnostics.__test;
328447
expect(isTrustedCidr('not-an-ip', ['10.0.0.0/24'])).to.equal(false);
329448
expect(isTrustedCidr('10.0.0.5', ['garbage', '10.0.0.0/99', '10.0.0.0'])).to.equal(false);
330449
expect(isTrustedCidr('10.0.0.5', [])).to.equal(false);
331450
expect(isTrustedCidr('10.0.0.5', null)).to.equal(false);
332451
});
452+
453+
it('does not treat a malformed prefix as /0 (must not trust every peer)', function () {
454+
// Number('')===0 and Number('0x10')===16 would silently widen a
455+
// typo'd CIDR; the prefix must be an explicit decimal or be skipped.
456+
const { isTrustedCidr } = diagnostics.__test;
457+
expect(isTrustedCidr('8.8.8.8', ['10.0.0.0/'])).to.equal(false);
458+
expect(isTrustedCidr('8.8.8.8', ['10.0.0.0/ '])).to.equal(false);
459+
expect(isTrustedCidr('10.0.255.1', ['10.0.0.0/0x10'])).to.equal(false);
460+
// A genuine /0 written explicitly still matches everything.
461+
expect(isTrustedCidr('8.8.8.8', ['0.0.0.0/0'])).to.equal(true);
462+
});
333463
});
334464

335465
describe('StatusAccumulator', function () {

0 commit comments

Comments
 (0)