blind-peer 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +102 -1
  2. package/lib/top-k.js +2 -2
  3. package/package.json +8 -3
package/index.js CHANGED
@@ -14,7 +14,11 @@ const ScopeLock = require('scope-lock')
14
14
  const IdEnc = require('hypercore-id-encoding')
15
15
  const ProtomuxRpcClientPool = require('protomux-rpc-client-pool')
16
16
  const ProtomuxRpcClient = require('protomux-rpc-client')
17
+ const rrp = require('resolve-reject-promise')
18
+ const IpBanList = require('ip-ban-list')
17
19
  const {
20
+ ADMIN_CHANNEL_ID,
21
+ AdminQueryTopKEncoding,
18
22
  AddCoreEncoding,
19
23
  DeleteCoreEncoding,
20
24
  RouterResolvePeersRequest,
@@ -206,11 +210,14 @@ class BlindPeer extends ReadyResource {
206
210
  trustedPubKeys,
207
211
  routerKey,
208
212
  routerPoolOpts,
213
+ ipBanListKeys = [],
214
+ banTimeout = 16_000,
209
215
  port,
210
216
  announcingInterval = 100,
211
217
  wakeupGcTickTime = null,
212
218
  replicationLagThreshold = 100,
213
- topK = {}
219
+ topK = {},
220
+ adminRouter = null
214
221
  } = {}
215
222
  ) {
216
223
  super()
@@ -218,6 +225,10 @@ class BlindPeer extends ReadyResource {
218
225
  this.rocks = typeof rocks === 'string' ? new RocksDB(rocks) : rocks
219
226
  this.store = store || new Corestore(this.rocks, { active: false })
220
227
  this.swarm = swarm || null
228
+ const ipBanNs = this.store.namespace('ip-ban-lists')
229
+ this.ipBanLists = ipBanListKeys.map((key) => new IpBanList(ipBanNs, { key }))
230
+ this.banTimeout = banTimeout
231
+
221
232
  this._port = port || 0
222
233
  this.announcingInterval = announcingInterval
223
234
  this.trustedPubKeys = new Set()
@@ -240,6 +251,7 @@ class BlindPeer extends ReadyResource {
240
251
  this.routerKey = routerKey || null
241
252
  this.routerPoolOpts = routerPoolOpts || {}
242
253
  this.routerPool = null
254
+ this.adminRouter = adminRouter
243
255
 
244
256
  this.stats = {
245
257
  bytesGcd: 0,
@@ -261,6 +273,24 @@ class BlindPeer extends ReadyResource {
261
273
 
262
274
  this.topKByPeer = new TopKWindow(bucketCount, bucketTime, k, peerThreshold)
263
275
  this.topKByReferrer = new TopKWindow(bucketCount, bucketTime, k, referrerThreshold)
276
+ this.topKByIp = new TopKWindow(bucketCount, bucketTime, k, null) // we do not want to expose the ip spike
277
+
278
+ if (this.adminRouter) {
279
+ this.adminRouter.use({
280
+ onrequest: (ctx, next) => {
281
+ if (!this._isTrustedPeer(ctx.connection.remotePublicKey)) {
282
+ throw new Error('Only trusted peers can query top-k')
283
+ }
284
+
285
+ return next()
286
+ }
287
+ })
288
+ this.adminRouter.method(
289
+ 'query-top-k',
290
+ AdminQueryTopKEncoding,
291
+ this._onadminquerytopk.bind(this)
292
+ )
293
+ }
264
294
  }
265
295
 
266
296
  get encryptionPublicKey() {
@@ -287,6 +317,15 @@ class BlindPeer extends ReadyResource {
287
317
  return this.trustedPubKeys.has(IdEnc.normalize(key))
288
318
  }
289
319
 
320
+ _onadminquerytopk() {
321
+ return {
322
+ version: 1,
323
+ ip: this.topKByIp.topK,
324
+ referrer: this.topKByReferrer.topK,
325
+ peerPublicKey: this.topKByPeer.topK
326
+ }
327
+ }
328
+
290
329
  async _open() {
291
330
  await this.store.ready()
292
331
 
@@ -310,6 +349,13 @@ class BlindPeer extends ReadyResource {
310
349
  }
311
350
  this.swarm.on('connection', this._onconnection.bind(this))
312
351
 
352
+ await Promise.all(
353
+ this.ipBanLists.map(async (banIpList) => {
354
+ await banIpList.ready()
355
+ this.swarm.join(banIpList.discoveryKey, { server: false, client: true })
356
+ })
357
+ )
358
+
313
359
  if (this.routerKey) {
314
360
  const rpcClient = new ProtomuxRpcClient(this.swarm.dht)
315
361
  this.routerPool = new ProtomuxRpcClientPool([this.routerKey], rpcClient, this.routerPoolOpts)
@@ -317,6 +363,8 @@ class BlindPeer extends ReadyResource {
317
363
 
318
364
  await this.topKByPeer.ready()
319
365
  await this.topKByReferrer.ready()
366
+ await this.topKByIp.ready()
367
+ if (this.adminRouter) await this.adminRouter.ready()
320
368
 
321
369
  this._announceCores().catch(safetyCatch) // announcing cores asynchronously
322
370
  this.flushInterval = setInterval(this.flush.bind(this), 10_000)
@@ -465,6 +513,7 @@ class BlindPeer extends ReadyResource {
465
513
 
466
514
  rpc.respond('add-core', AddCoreEncoding, this._onaddcore.bind(this, conn))
467
515
  rpc.respond('delete-core', DeleteCoreEncoding, this._ondeletecore.bind(this, conn))
516
+ if (this.adminRouter) this.adminRouter.handleConnection(conn, ADMIN_CHANNEL_ID)
468
517
 
469
518
  const self = this
470
519
  BlindPeerMuxer.pair(conn, function () {
@@ -484,6 +533,36 @@ class BlindPeer extends ReadyResource {
484
533
  })
485
534
  }
486
535
 
536
+ _isBlocked(conn) {
537
+ // current behavior:
538
+ // as we do simple check ban
539
+ // bans can run in parallel across lists, but only the original banner can unban
540
+ for (const ipBanList of this.ipBanLists) {
541
+ if (ipBanList.isBanned(conn.rawStream.remoteHost)) {
542
+ return true
543
+ }
544
+ }
545
+ return false
546
+ }
547
+
548
+ async _timeoutThenThrow() {
549
+ const { promise, resolve } = rrp()
550
+
551
+ const done = () => {
552
+ clearTimeout(timer)
553
+ this.off('close', done)
554
+ resolve()
555
+ }
556
+
557
+ const timer = setTimeout(done, this.banTimeout)
558
+ timer.unref()
559
+
560
+ this.on('close', done)
561
+
562
+ await promise
563
+ throw new Error('Timed out')
564
+ }
565
+
487
566
  async _activateCore(stream, record) {
488
567
  this.stats.activations++
489
568
 
@@ -584,6 +663,10 @@ class BlindPeer extends ReadyResource {
584
663
 
585
664
  async _onaddcore(stream, record) {
586
665
  if (!this.opened) await this.ready()
666
+ if (this._isBlocked(stream)) {
667
+ this.emit('connection-banned', stream)
668
+ await this._timeoutThenThrow()
669
+ }
587
670
 
588
671
  record.priority = Math.min(record.priority, 1) // 2 is reserved for trusted peers
589
672
  if (record.announce !== false && !this._isTrustedPeer(stream.remotePublicKey)) {
@@ -624,6 +707,10 @@ class BlindPeer extends ReadyResource {
624
707
  }
625
708
 
626
709
  async _onaddcores(stream, request) {
710
+ if (this._isBlocked(stream)) {
711
+ this.emit('connection-banned', stream)
712
+ throw new Error('Timed out')
713
+ }
627
714
  this.stats.addCoresRx++
628
715
 
629
716
  const { cores, referrer } = request
@@ -631,6 +718,7 @@ class BlindPeer extends ReadyResource {
631
718
  this.topKByReferrer.hit(IdEnc.normalize(referrer))
632
719
  }
633
720
  this.topKByPeer.hit(IdEnc.normalize(stream.remotePublicKey))
721
+ this.topKByIp.hit(stream.rawStream.remoteHost)
634
722
 
635
723
  const priority = Math.min(request.priority, 1) // 2 is reserved for trusted peers
636
724
 
@@ -731,6 +819,10 @@ class BlindPeer extends ReadyResource {
731
819
  }
732
820
 
733
821
  async _ondeletecore(stream, { key }) {
822
+ if (this._isBlocked(stream)) {
823
+ this.emit('connection-banned', stream)
824
+ await this._timeoutThenThrow()
825
+ }
734
826
  if (!this._isTrustedPeer(stream.remotePublicKey)) {
735
827
  this.emit('delete-blocked', stream, { key })
736
828
  throw new Error('Only trusted peers can delete cores')
@@ -780,9 +872,11 @@ class BlindPeer extends ReadyResource {
780
872
  await this.routerPool.destroy()
781
873
  await this.routerPool.statelessRpc.close()
782
874
  }
875
+ if (this.adminRouter) await this.adminRouter.close()
783
876
  clearInterval(this.flushInterval)
784
877
  await this.topKByPeer.close()
785
878
  await this.topKByReferrer.close()
879
+ await this.topKByIp.close()
786
880
  if (this.ownsWakeup) this.wakeup.destroy()
787
881
  if (this.ownsSwarm) await this.swarm.destroy()
788
882
  await this.flush()
@@ -905,6 +999,13 @@ class BlindPeer extends ReadyResource {
905
999
  this.set(self.topKByReferrer.topKSum())
906
1000
  }
907
1001
  })
1002
+ new promClient.Gauge({
1003
+ name: 'blind_peer_add_cores_top5_by_remote_ip',
1004
+ help: 'The total number of requests from the top 5 remote IPs in the last minute',
1005
+ collect() {
1006
+ this.set(self.topKByIp.topKSum())
1007
+ }
1008
+ })
908
1009
  if (self.rocks.stats) {
909
1010
  new promClient.Gauge({
910
1011
  // eslint-disable-line no-new
package/lib/top-k.js CHANGED
@@ -44,11 +44,11 @@ class TopKWindow extends ReadyResource {
44
44
  return sum
45
45
  }
46
46
 
47
- async _open() {
47
+ _open() {
48
48
  this._timer = setInterval(this._rotate.bind(this), this.bucketTime)
49
49
  }
50
50
 
51
- async _close() {
51
+ _close() {
52
52
  if (this._timer !== null) {
53
53
  clearInterval(this._timer)
54
54
  this._timer = null
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "blind-peer",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Blind peers help keep hypercores available",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "autobase": "^7.0.18",
8
8
  "b4a": "^1.6.7",
9
- "blind-peer-encodings": "^3.2.0",
9
+ "blind-peer-encodings": "^3.3.0",
10
10
  "blind-peer-muxer": "^1.0.0",
11
11
  "compact-encoding": "^2.16.0",
12
12
  "corestore": "^7.4.4",
@@ -17,12 +17,14 @@
17
17
  "hyperdht": "^6.29.0",
18
18
  "hyperschema": "^1.10.3",
19
19
  "hyperswarm": "^4.13.1",
20
+ "ip-ban-list": "^0.3.0",
20
21
  "protomux-rpc": "^1.7.1",
21
22
  "protomux-rpc-client": "^2.1.0",
22
23
  "protomux-rpc-client-pool": "^2.1.0",
23
24
  "protomux-wakeup": "^2.9.0",
24
25
  "ready-resource": "^1.1.2",
25
26
  "repl-swarm": "^2.3.0",
27
+ "resolve-reject-promise": "^1.0.3",
26
28
  "rocksdb-native": "^3.1.6",
27
29
  "safety-catch": "^1.0.2",
28
30
  "scope-lock": "^1.2.4"
@@ -32,9 +34,11 @@
32
34
  "bare-process": "^4.2.2",
33
35
  "bare-prom-client": "^15.1.3",
34
36
  "blind-peer-router": "^0.2.2",
35
- "blind-peering": "^2.0.1",
37
+ "blind-peering": "^2.1.0",
36
38
  "brittle": "^3.7.0",
37
39
  "debounceify": "^1.1.0",
40
+ "graceful-goodbye": "^1.3.3",
41
+ "hyperdht-address": "^1.0.1",
38
42
  "lunte": "^1.6.0",
39
43
  "prettier": "^3.6.2",
40
44
  "prettier-config-holepunch": "^2.0.0",
@@ -49,6 +53,7 @@
49
53
  "format": "prettier --write .",
50
54
  "format:check": "prettier --check .",
51
55
  "test": "npm run format:check && lunte && brittle test/*.js",
56
+ "test:netns": "brittle test-netns/*.test.js",
52
57
  "test:bare": "bare test/test.js"
53
58
  },
54
59
  "repository": {