@@ -12,6 +12,23 @@ import 'dotenv/config';
1212
1313const RELAY_DIRECTORY = process . env . MONGO_RELAY_DIRECTORY_COLLECTION || 'relays' ;
1414
15+ // SECURITY: the prober dials ONLY this curated allowlist — never the harvested
16+ // `relays` directory, which is attacker-influenceable (anyone can publish a
17+ // relay list we harvest). Decoupling the target set from attacker-controlled
18+ // data is what stops the prober being used to dial arbitrary hosts. Expand
19+ // deliberately via PROBE_RELAYS (comma list); safe-then-expand, not the reverse.
20+ export const DEFAULT_PROBE_RELAYS = [
21+ 'wss://relay.damus.io' , 'wss://nos.lol' , 'wss://relay.primal.net' ,
22+ 'wss://relay.nostr.band' , 'wss://nostr.wine' , 'wss://relay.snort.social' ,
23+ 'wss://purplepag.es' , 'wss://nostr.mom' ,
24+ ] ;
25+
26+ /** The curated set of relays the prober is allowed to dial (screened). */
27+ export function proberRelays ( env = process . env ) {
28+ const raw = env . PROBE_RELAYS ? env . PROBE_RELAYS . split ( ',' ) : DEFAULT_PROBE_RELAYS ;
29+ return [ ...new Set ( raw . map ( ( u ) => safeRelayUrl ( String ( u ) . trim ( ) ) ) . filter ( Boolean ) ) ] ;
30+ }
31+
1532export const USAGE = `beacon relay-health prober
1633
1734Usage: node probe.js [flags] (or: node src/prober.js [flags])
@@ -20,8 +37,12 @@ Flags (override the matching env var):
2037 --once run a single sweep and exit (env RUN_ONCE=1)
2138 --concurrency <n> relays probed in parallel (env PROBE_CONCURRENCY, default 25)
2239 --timeout <ms> per-relay connect/HTTP timeout (env PROBE_TIMEOUT, default 7000)
23- --interval <ms> sweep interval when scheduling (env PROBE_INTERVAL, default 3600000)
24- -h, --help show this help` ;
40+ --interval <ms> sweep interval when scheduling (env PROBE_INTERVAL, default 86400000 = daily)
41+ -h, --help show this help
42+
43+ Targets: a curated allowlist only (env PROBE_RELAYS, comma list; default: a
44+ small built-in set of well-known relays). The harvested directory is NEVER
45+ dialed — expand the allowlist deliberately.` ;
2546
2647/** Parse prober CLI flags into an overlay (only keys the user passed). */
2748export function parseArgs ( argv = process . argv . slice ( 2 ) ) {
@@ -43,7 +64,7 @@ export function proberConfig(env = process.env, argv = process.argv.slice(2)) {
4364 const flags = parseArgs ( argv ) ;
4465 const pos = ( v , d ) => { const n = Number ( v ) ; return Number . isFinite ( n ) && n > 0 ? n : d ; } ;
4566 return {
46- interval : flags . interval ?? pos ( env . PROBE_INTERVAL , 3_600_000 ) ,
67+ interval : flags . interval ?? pos ( env . PROBE_INTERVAL , 86_400_000 ) ,
4768 concurrency : flags . concurrency ?? pos ( env . PROBE_CONCURRENCY , 25 ) ,
4869 timeout : flags . timeout ?? pos ( env . PROBE_TIMEOUT , 7_000 ) ,
4970 once : flags . once || env . RUN_ONCE === '1' || env . RUN_ONCE === 'true' ,
@@ -123,21 +144,16 @@ async function mapPool(items, concurrency, fn) {
123144 await Promise . all ( Array . from ( { length : Math . max ( 1 , Math . min ( concurrency , items . length ) ) } , worker ) ) ;
124145}
125146
126- /** One full sweep over every relay in the directory. Returns { total, online } . */
147+ /** One sweep over the curated allowlist (never the harvested directory) . */
127148export async function sweepOnce ( db , cfg ) {
128- const docs = await db . collection ( RELAY_DIRECTORY ) . find ( { } , { projection : { relay : 1 , _id : 0 } } ) . toArray ( ) ;
129- const all = [ ...new Set ( docs . map ( ( d ) => d . relay ) . filter ( Boolean ) ) ] ;
130- // Defense-in-depth: never dial loopback/private/reserved targets, even if
131- // junk slipped into the directory before harvest hardening (SSRF guard).
132- const relays = all . filter ( ( r ) => safeRelayUrl ( r ) ) ;
133- const skipped = all . length - relays . length ;
149+ const relays = proberRelays ( ) ; // allowlist only — attacker-fed data is never dialed
134150 let online = 0 ;
135151 await mapPool ( relays , cfg . concurrency , async ( relay ) => {
136152 try { if ( ( await probeRelay ( db , relay , cfg ) ) . online ) online ++ ; }
137153 catch ( e ) { console . error ( `[beacon] probe error ${ relay } : ${ e . message } ` ) ; }
138154 } ) ;
139- console . log ( `[beacon] relay sweep: ${ online } /${ relays . length } online${ skipped ? ` (skipped ${ skipped } unsafe)` : '' } ` ) ;
140- return { total : relays . length , online, skipped } ;
155+ console . log ( `[beacon] relay sweep (allowlist) : ${ online } /${ relays . length } online` ) ;
156+ return { total : relays . length , online } ;
141157}
142158
143159export async function runProber ( ) {
0 commit comments