gnutella 1.0.0 → 1.1.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 (75) hide show
  1. package/CLI.md +1 -0
  2. package/gnutella.json.example +1 -0
  3. package/package.json +4 -3
  4. package/src/cli_shared.ts +32 -43
  5. package/src/const.ts +1 -9
  6. package/src/descriptor_routing/index.ts +17 -0
  7. package/src/descriptor_routing/pong_cache.ts +32 -0
  8. package/src/descriptor_routing/response_routes.ts +15 -0
  9. package/src/descriptor_routing/seen.ts +20 -0
  10. package/src/descriptor_routing/ttl.ts +37 -0
  11. package/src/descriptor_routing/types.ts +27 -0
  12. package/src/gwebcache/bootstrap.ts +21 -58
  13. package/src/gwebcache/types.ts +6 -10
  14. package/src/handshake_policy/admission.ts +17 -0
  15. package/src/handshake_policy/capabilities.ts +167 -0
  16. package/src/handshake_policy/headers.ts +157 -0
  17. package/src/handshake_policy/index.ts +21 -0
  18. package/src/handshake_policy/types.ts +36 -0
  19. package/src/peer_address.ts +68 -0
  20. package/src/peer_discovery/candidate_policy.ts +80 -0
  21. package/src/peer_discovery/index.ts +8 -0
  22. package/src/peer_discovery/types.ts +26 -0
  23. package/src/persistence/config_doc.ts +61 -0
  24. package/src/persistence/index.ts +14 -0
  25. package/src/persistence/peer_state.ts +113 -0
  26. package/src/persistence/types.ts +28 -0
  27. package/src/protocol/codec.ts +27 -67
  28. package/src/protocol/content_urn.ts +5 -1
  29. package/src/protocol/file_hash.ts +12 -0
  30. package/src/protocol/file_server.ts +1 -1
  31. package/src/protocol/ggep.ts +13 -8
  32. package/src/protocol/handshake.ts +18 -161
  33. package/src/protocol/http_download_reader.ts +9 -7
  34. package/src/protocol/magnet.ts +15 -13
  35. package/src/protocol/node.ts +1 -1
  36. package/src/protocol/node_handshake.ts +55 -113
  37. package/src/protocol/node_protocol_runtime.ts +69 -60
  38. package/src/protocol/node_qrp_runtime.ts +7 -6
  39. package/src/protocol/node_query_routing.ts +43 -132
  40. package/src/protocol/node_state.ts +2 -3
  41. package/src/protocol/node_topology.ts +38 -82
  42. package/src/protocol/node_transfer.ts +52 -35
  43. package/src/protocol/peer_state.ts +36 -207
  44. package/src/protocol/qrp.ts +1 -549
  45. package/src/protocol/query_matching.ts +22 -0
  46. package/src/protocol/share_index.ts +8 -70
  47. package/src/protocol/share_library.ts +30 -73
  48. package/src/query_routing/dynamic_query.ts +117 -0
  49. package/src/query_routing/index.ts +27 -0
  50. package/src/query_routing/qrp/constants.ts +9 -0
  51. package/src/query_routing/qrp/hash.ts +27 -0
  52. package/src/query_routing/qrp/patch_values.ts +29 -0
  53. package/src/query_routing/qrp/remote_state.ts +98 -0
  54. package/src/query_routing/qrp/routing.ts +46 -0
  55. package/src/query_routing/qrp/table.ts +319 -0
  56. package/src/query_routing/qrp/terms.ts +62 -0
  57. package/src/query_routing/qrp/types.ts +31 -0
  58. package/src/query_routing/qrp.ts +13 -0
  59. package/src/share_catalog/catalog.ts +108 -0
  60. package/src/share_catalog/index.ts +16 -0
  61. package/src/share_catalog/keywords.ts +15 -0
  62. package/src/share_catalog/manifest.ts +81 -0
  63. package/src/share_catalog/types.ts +43 -0
  64. package/src/shared.ts +9 -68
  65. package/src/topology/admission.ts +51 -0
  66. package/src/topology/classify.ts +19 -0
  67. package/src/topology/index.ts +17 -0
  68. package/src/topology/slots.ts +43 -0
  69. package/src/topology/types.ts +25 -0
  70. package/src/transfers/index.ts +13 -0
  71. package/src/transfers/planner.ts +52 -0
  72. package/src/transfers/ranges.ts +57 -0
  73. package/src/transfers/results.ts +45 -0
  74. package/src/transfers/types.ts +43 -0
  75. package/src/types.ts +43 -55
@@ -1,6 +1,14 @@
1
1
  import net from "node:net";
2
2
 
3
- import { DEFAULT_USER_AGENT, MAX_XTRY } from "../const";
3
+ import { MAX_XTRY } from "../const";
4
+ import {
5
+ buildBaseHandshakeHeaders,
6
+ buildClientFinalHeaders as buildPolicyClientFinalHeaders,
7
+ buildPeerCapabilities,
8
+ buildRejectHeaders,
9
+ buildServerHandshakeHeaders as buildPolicyServerHandshakeHeaders,
10
+ type LocalHandshakePolicy,
11
+ } from "../handshake_policy";
4
12
  import {
5
13
  errMsg,
6
14
  normalizeIpv4,
@@ -19,13 +27,9 @@ import {
19
27
  describeHandshakeResponse,
20
28
  findHeaderEnd,
21
29
  hasToken,
22
- lowerCaseHeaders,
23
30
  mergeHeaders,
24
- parseBoolHeader,
25
31
  parseHandshakeBlock,
26
- parseListenIpHeader,
27
32
  parsePeerHeaderList,
28
- parsePositiveIntHeader,
29
33
  } from "./handshake";
30
34
  import type { GnutellaServent } from "./node";
31
35
  import type { ProbeCtx } from "./node_types";
@@ -140,70 +144,40 @@ function emitHandshakeBlock(
140
144
  );
141
145
  }
142
146
 
143
- function ultrapeerNeededHeader(node: GnutellaServent): string | undefined {
144
- if (node.nodeMode() !== "ultrapeer") return undefined;
145
- if (
146
- node.connectedMeshPeerCount() < node.config().maxUltrapeerConnections
147
- )
148
- return "True";
149
- if (node.connectedLeafCount() < node.config().maxLeafConnections)
150
- return "False";
151
- return undefined;
152
- }
153
-
154
- const GTK_MODERN_ULTRAPEER_MIN_DEGREE = 16;
155
-
156
- function baseRoleHeaders(node: GnutellaServent): Record<string, string> {
157
- const headers: Record<string, string> = {
158
- "x-ultrapeer": node.nodeMode() === "ultrapeer" ? "True" : "False",
159
- };
160
- const ultrapeerNeeded = ultrapeerNeededHeader(node);
161
- if (ultrapeerNeeded) headers["x-ultrapeer-needed"] = ultrapeerNeeded;
162
- if (node.nodeMode() === "ultrapeer") {
163
- headers["x-ultrapeer-query-routing"] = "0.1";
164
- headers["x-dynamic-querying"] = "0.1";
165
- headers["x-ext-probes"] = "0.1";
166
- headers["x-degree"] = String(
167
- Math.max(
168
- GTK_MODERN_ULTRAPEER_MIN_DEGREE,
169
- node.config().maxUltrapeerConnections,
170
- ),
171
- );
172
- }
173
- return headers;
174
- }
175
-
176
- function baseFeatureHeaders(
147
+ function localHandshakePolicy(
177
148
  node: GnutellaServent,
178
- ): Record<string, string> {
149
+ tlsEnabled = node.tlsEnabled(),
150
+ ): LocalHandshakePolicy {
179
151
  const c = node.config();
180
- const headers: Record<string, string> = {};
181
- if (c.enableQrp)
182
- headers["x-query-routing"] = c.queryRoutingVersion || "0.1";
183
- if (c.enableCompression) headers["accept-encoding"] = "deflate";
184
- if (c.enablePongCaching) headers["pong-caching"] = "0.1";
185
- if (c.enableGgep) headers["ggep"] = "0.5";
186
- if (c.enableBye) headers["bye-packet"] = "0.1";
187
- return headers;
152
+ return {
153
+ userAgent: c.userAgent,
154
+ advertisedHost: node.currentAdvertisedHost(),
155
+ advertisedPort: node.currentAdvertisedPort(),
156
+ maxTtl: c.maxTtl,
157
+ nodeMode: node.nodeMode(),
158
+ maxUltrapeerConnections: c.maxUltrapeerConnections,
159
+ maxLeafConnections: c.maxLeafConnections,
160
+ connectedMeshPeerCount: node.connectedMeshPeerCount(),
161
+ connectedLeafCount: node.connectedLeafCount(),
162
+ enableQrp: c.enableQrp,
163
+ queryRoutingVersion: c.queryRoutingVersion,
164
+ enableCompression: c.enableCompression,
165
+ enablePongCaching: c.enablePongCaching,
166
+ enableGgep: c.enableGgep,
167
+ enableBye: c.enableBye,
168
+ tlsEnabled,
169
+ tlsUpgradeToken: node.tlsUpgradeToken(),
170
+ };
188
171
  }
189
172
 
190
173
  export function baseHandshakeHeaders(
191
174
  node: GnutellaServent,
192
175
  remoteIp?: string,
193
176
  ): Record<string, string> {
194
- const c = node.config();
195
- const advertisedHost = node.currentAdvertisedHost();
196
- const advertisedPort = node.currentAdvertisedPort();
197
- const headers: Record<string, string> = {
198
- "user-agent": c.userAgent || DEFAULT_USER_AGENT,
199
- "listen-ip": `${advertisedHost}:${advertisedPort}`,
200
- "x-max-ttl": String(c.maxTtl),
201
- ...baseRoleHeaders(node),
202
- ...baseFeatureHeaders(node),
203
- };
204
- const observedRemote = normalizeIpv4(remoteIp);
205
- if (observedRemote) headers["remote-ip"] = observedRemote;
206
- return headers;
177
+ return buildBaseHandshakeHeaders(
178
+ localHandshakePolicy(node, false),
179
+ remoteIp,
180
+ );
207
181
  }
208
182
 
209
183
  export function buildServerHandshakeHeaders(
@@ -211,18 +185,11 @@ export function buildServerHandshakeHeaders(
211
185
  requestHeaders: Record<string, string>,
212
186
  remoteIp?: string,
213
187
  ): Record<string, string> {
214
- const headers = node.baseHandshakeHeaders(remoteIp);
215
- if (node.tlsEnabled() && node.peerRequestedTlsUpgrade(requestHeaders)) {
216
- headers.upgrade = node.tlsUpgradeToken();
217
- headers.connection = "Upgrade";
218
- }
219
- if (
220
- node.config().enableCompression &&
221
- hasToken(requestHeaders["accept-encoding"], "deflate")
222
- ) {
223
- headers["content-encoding"] = "deflate";
224
- }
225
- return headers;
188
+ return buildPolicyServerHandshakeHeaders(
189
+ localHandshakePolicy(node),
190
+ requestHeaders,
191
+ remoteIp,
192
+ );
226
193
  }
227
194
 
228
195
  export function buildClientFinalHeaders(
@@ -230,18 +197,11 @@ export function buildClientFinalHeaders(
230
197
  serverHeaders: Record<string, string>,
231
198
  remoteIp?: string,
232
199
  ): Record<string, string> {
233
- const headers: Record<string, string> = {};
234
- const observedRemote = normalizeIpv4(remoteIp);
235
- if (observedRemote) headers["remote-ip"] = observedRemote;
236
- if (node.tlsEnabled() && node.peerAcceptedTlsUpgrade(serverHeaders))
237
- headers.connection = "Upgrade";
238
- if (
239
- node.config().enableCompression &&
240
- hasToken(serverHeaders["accept-encoding"], "deflate")
241
- ) {
242
- headers["content-encoding"] = "deflate";
243
- }
244
- return headers;
200
+ return buildPolicyClientFinalHeaders(
201
+ localHandshakePolicy(node),
202
+ serverHeaders,
203
+ remoteIp,
204
+ );
245
205
  }
246
206
 
247
207
  export function buildCapabilities(
@@ -251,30 +211,14 @@ export function buildCapabilities(
251
211
  compressIn: boolean,
252
212
  compressOut: boolean,
253
213
  ): PeerCapabilities {
254
- const h = lowerCaseHeaders(headers);
255
- return {
214
+ return buildPeerCapabilities({
256
215
  version,
257
- headers: h,
258
- userAgent: h["user-agent"],
259
- supportsGgep: !!h["ggep"],
260
- supportsPongCaching: !!h["pong-caching"],
261
- supportsBye: !!h["bye-packet"],
262
- supportsTls: node.peerRequestedTlsUpgrade(h),
263
- supportsCompression:
264
- hasToken(h["accept-encoding"], "deflate") ||
265
- hasToken(h["content-encoding"], "deflate"),
216
+ headers,
266
217
  compressIn,
267
218
  compressOut,
268
- isUltrapeer: parseBoolHeader(h["x-ultrapeer"]),
269
- ultrapeerNeeded: parseBoolHeader(h["x-ultrapeer-needed"]),
270
- queryRoutingVersion: h["x-query-routing"],
271
- ultrapeerQueryRoutingVersion: h["x-ultrapeer-query-routing"],
272
- dynamicQueryingVersion: h["x-dynamic-querying"],
273
- extProbesVersion: h["x-ext-probes"],
274
- degree: parsePositiveIntHeader(h["x-degree"]),
275
- isCrawler: !!h["crawler"],
276
- listenIp: parseListenIpHeader(h["listen-ip"]),
277
- };
219
+ tlsEnabled: node.tlsEnabled(),
220
+ tlsUpgradeToken: node.tlsUpgradeToken(),
221
+ });
278
222
  }
279
223
 
280
224
  export function selectTryPeers(
@@ -337,13 +281,11 @@ export function reject06(
337
281
  extraHeaders: Record<string, string> = {},
338
282
  ): void {
339
283
  const tryPeers = node.selectTryPeers();
340
- const headers = lowerCaseHeaders(extraHeaders);
341
- const observedRemote = normalizeIpv4(socket.remoteAddress);
342
- if (observedRemote) headers["remote-ip"] = observedRemote;
343
- if (tryPeers.length) {
344
- headers["x-try"] = tryPeers.join(",");
345
- headers["x-try-ultrapeers"] = tryPeers.join(",");
346
- }
284
+ const headers = buildRejectHeaders({
285
+ extraHeaders,
286
+ remoteIp: socket.remoteAddress,
287
+ tryPeers,
288
+ });
347
289
  emitHandshakeBlock(
348
290
  node,
349
291
  "inbound",
@@ -1,8 +1,25 @@
1
- import crypto from "node:crypto";
2
1
  import type net from "node:net";
3
2
  import zlib from "node:zlib";
4
3
 
5
- import { HEADER_LEN, LOCAL_ROUTE, TYPE, TYPE_NAME } from "../const";
4
+ import { HEADER_LEN, TYPE, TYPE_NAME } from "../const";
5
+ import {
6
+ forwardedDescriptorLifetime,
7
+ normalizeQueryLifetime as normalizeQueryLifetimePolicy,
8
+ overflowPongCacheKeys,
9
+ pongCacheKey,
10
+ pongReplyTtl,
11
+ queryHitReplyTtl,
12
+ responseRouteDecision,
13
+ selectCachedPongPayloads,
14
+ shouldMarkDescriptorSeen,
15
+ shouldRelayPing,
16
+ shouldSuppressDescriptor,
17
+ } from "../descriptor_routing";
18
+ import type { DescriptorLifetime } from "../descriptor_routing/types";
19
+ import {
20
+ initialRemoteQrpState,
21
+ splitSearchTerms,
22
+ } from "../query_routing/qrp";
6
23
  import { errMsg, toBuffer, ts } from "../shared";
7
24
  import type {
8
25
  PendingPush,
@@ -42,11 +59,7 @@ import type {
42
59
  Peer,
43
60
  } from "./node_types";
44
61
  import { firstSha1Urn } from "./content_urn";
45
- import {
46
- initialRemoteQrpState,
47
- matchQuery as shareMatchesQuery,
48
- splitSearchTerms,
49
- } from "./qrp";
62
+ import { matchQuery as shareMatchesQuery } from "./query_matching";
50
63
 
51
64
  function descriptorTypeName(payloadType: number): string {
52
65
  return TYPE_NAME[payloadType] || `0x${payloadType.toString(16)}`;
@@ -426,15 +439,16 @@ export function forwardToRoute(
426
439
  hops: number,
427
440
  payload: Buffer,
428
441
  ): void {
429
- if (ttl <= 0) return;
442
+ const lifetime = forwardedDescriptorLifetime(ttl, hops);
443
+ if (!lifetime) return;
430
444
  const peer = node.peers.get(route.peerKey);
431
445
  if (!peer) return;
432
446
  node.sendToPeer(
433
447
  peer,
434
448
  payloadType,
435
449
  descriptorId,
436
- Math.max(0, ttl - 1),
437
- hops + 1,
450
+ lifetime.ttl,
451
+ lifetime.hops,
438
452
  payload,
439
453
  );
440
454
  }
@@ -479,11 +493,8 @@ export function normalizeQueryLifetime(
479
493
  node: GnutellaServent,
480
494
  ttl: number,
481
495
  hops: number,
482
- ): { ttl: number; hops: number } | null {
483
- if (ttl > 15) return null;
484
- const maxLife = Math.max(1, node.config().maxTtl);
485
- if (hops > maxLife) return null;
486
- return { ttl: Math.max(0, Math.min(ttl, maxLife - hops)), hops };
496
+ ): DescriptorLifetime | null {
497
+ return normalizeQueryLifetimePolicy(ttl, hops, node.config().maxTtl);
487
498
  }
488
499
 
489
500
  export function isIndexQuery(
@@ -532,16 +543,14 @@ export function cachePongPayload(
532
543
  node: GnutellaServent,
533
544
  payload: Buffer,
534
545
  ): void {
535
- const digest = crypto.createHash("sha1").update(payload).digest("hex");
546
+ const digest = pongCacheKey(payload);
536
547
  node.pongCache.set(digest, {
537
548
  payload: Buffer.from(payload),
538
549
  at: node.now(),
539
550
  });
540
- if (node.pongCache.size <= 64) return;
541
- const oldest = [...node.pongCache.entries()]
542
- .sort((a, b) => a[1].at - b[1].at)
543
- .slice(0, node.pongCache.size - 64);
544
- for (const [key] of oldest) node.pongCache.delete(key);
551
+ for (const key of overflowPongCacheKeys(node.pongCache.entries(), 64)) {
552
+ node.pongCache.delete(key);
553
+ }
545
554
  }
546
555
 
547
556
  export function shouldIgnoreDescriptor(
@@ -550,15 +559,15 @@ export function shouldIgnoreDescriptor(
550
559
  hdr: RoutedDescriptor,
551
560
  payload: Buffer,
552
561
  ): boolean {
553
- if (
554
- peer.closingAfterBye &&
555
- hdr.payloadType !== TYPE.QUERY_HIT &&
556
- hdr.payloadType !== TYPE.PUSH
557
- ) {
558
- return true;
559
- }
560
- if (hdr.payloadType === TYPE.ROUTE_TABLE_UPDATE) return false;
561
- return node.hasSeen(hdr.payloadType, hdr.descriptorIdHex, payload);
562
+ return shouldSuppressDescriptor({
563
+ closingAfterBye: !!peer.closingAfterBye,
564
+ payloadType: hdr.payloadType,
565
+ alreadySeen: node.hasSeen(
566
+ hdr.payloadType,
567
+ hdr.descriptorIdHex,
568
+ payload,
569
+ ),
570
+ });
562
571
  }
563
572
 
564
573
  export function rejectRelayedLeafDescriptor(
@@ -589,7 +598,7 @@ export function onPingDescriptor(
589
598
  });
590
599
  node.respondPong(peer, hdr);
591
600
  if (!node.shouldRelayPings()) return;
592
- if (hdr.ttl <= 1 || node.now() - peer.lastPingAt < 1000) return;
601
+ if (!shouldRelayPing(hdr.ttl, node.now(), peer.lastPingAt, 1000)) return;
593
602
  peer.lastPingAt = node.now();
594
603
  broadcastPingToPeers(
595
604
  node,
@@ -681,7 +690,7 @@ export function handleDescriptor(
681
690
  ): void {
682
691
  if (node.rejectRelayedLeafDescriptor(peer, hdr)) return;
683
692
  if (node.shouldIgnoreDescriptor(peer, hdr, payload)) return;
684
- if (hdr.payloadType !== TYPE.ROUTE_TABLE_UPDATE) {
693
+ if (shouldMarkDescriptorSeen(hdr.payloadType)) {
685
694
  node.markSeen(hdr.payloadType, hdr.descriptorIdHex, payload);
686
695
  }
687
696
  node.dispatchDescriptor(peer, hdr, payload);
@@ -709,7 +718,7 @@ export function respondPong(
709
718
  peer: Peer,
710
719
  hdr: Pick<DescriptorHeader, "descriptorId" | "hops">,
711
720
  ): void {
712
- const ttl = Math.max(1, hdr.hops);
721
+ const ttl = pongReplyTtl(hdr.hops);
713
722
  const own = encodePong(
714
723
  node.currentAdvertisedPort(),
715
724
  node.currentAdvertisedHost(),
@@ -719,17 +728,12 @@ export function respondPong(
719
728
  node.sendToPeer(peer, TYPE.PONG, hdr.descriptorId, ttl, 0, own);
720
729
  if (!node.config().enablePongCaching) return;
721
730
  let sent = 1;
722
- const cached = [...node.pongCache.values()].sort((a, b) => b.at - a.at);
723
- for (const entry of cached) {
724
- if (sent >= 10) break;
725
- node.sendToPeer(
726
- peer,
727
- TYPE.PONG,
728
- hdr.descriptorId,
729
- ttl,
730
- 0,
731
- entry.payload,
732
- );
731
+ for (const payload of selectCachedPongPayloads(
732
+ node.pongCache.values(),
733
+ sent,
734
+ 10,
735
+ )) {
736
+ node.sendToPeer(peer, TYPE.PONG, hdr.descriptorId, ttl, 0, payload);
733
737
  sent++;
734
738
  }
735
739
  }
@@ -750,10 +754,7 @@ export function respondQueryHit(
750
754
  const limit = Math.max(1, node.config().maxResultsPerQuery);
751
755
  const batchSize = 16;
752
756
  const chosen = matches.slice(0, limit);
753
- const replyTtl = Math.min(
754
- node.config().maxTtl,
755
- Math.max(1, hdr.hops + 2),
756
- );
757
+ const replyTtl = queryHitReplyTtl(hdr.hops, node.config().maxTtl);
757
758
  for (let off = 0; off < chosen.length; off += batchSize) {
758
759
  const batch = chosen.slice(off, off + batchSize);
759
760
  const out = encodeQueryHit(
@@ -792,9 +793,12 @@ export function onPong(
792
793
  const pong = parsePong(payload);
793
794
  node.cachePongPayload(payload);
794
795
  node.addKnownPeer(pong.ip, pong.port);
795
- const route = node.pingRoutes.get(hdr.descriptorIdHex);
796
- if (!route) return;
797
- if (route === LOCAL_ROUTE) {
796
+ const decision = responseRouteDecision(
797
+ node.pingRoutes.get(hdr.descriptorIdHex),
798
+ { forwardInLeaf: true },
799
+ );
800
+ if (decision.kind === "drop") return;
801
+ if (decision.kind === "local") {
798
802
  node.emitEvent({
799
803
  type: "PONG",
800
804
  at: ts(),
@@ -806,7 +810,7 @@ export function onPong(
806
810
  return;
807
811
  }
808
812
  node.forwardToRoute(
809
- route,
813
+ decision.route,
810
814
  TYPE.PONG,
811
815
  hdr.descriptorId,
812
816
  hdr.ttl,
@@ -826,9 +830,12 @@ export function onQueryHit(
826
830
  peerKey: peer.key,
827
831
  ts: node.now(),
828
832
  });
829
- const route = node.queryRoutes.get(hdr.descriptorIdHex);
830
- if (!route) return;
831
- if (route === LOCAL_ROUTE) {
833
+ const decision = responseRouteDecision(
834
+ node.queryRoutes.get(hdr.descriptorIdHex),
835
+ { nodeMode: node.nodeMode() },
836
+ );
837
+ if (decision.kind === "drop") return;
838
+ if (decision.kind === "local") {
832
839
  for (const result of qh.results) {
833
840
  const hit: SearchHit = {
834
841
  resultNo: node.resultSeq++,
@@ -854,9 +861,8 @@ export function onQueryHit(
854
861
  }
855
862
  return;
856
863
  }
857
- if (node.nodeMode() === "leaf") return;
858
864
  node.forwardToRoute(
859
- route,
865
+ decision.route,
860
866
  TYPE.QUERY_HIT,
861
867
  hdr.descriptorId,
862
868
  hdr.ttl,
@@ -877,10 +883,13 @@ export async function onPush(
877
883
  return;
878
884
  }
879
885
  if (node.nodeMode() === "leaf") return;
880
- const route = node.pushRoutes.get(push.serventIdHex);
881
- if (!route) return;
886
+ const decision = responseRouteDecision(
887
+ node.pushRoutes.get(push.serventIdHex),
888
+ { nodeMode: node.nodeMode() },
889
+ );
890
+ if (decision.kind !== "forward") return;
882
891
  node.forwardToRoute(
883
- route,
892
+ decision.route,
884
893
  TYPE.PUSH,
885
894
  hdr.descriptorId,
886
895
  hdr.ttl,
@@ -1,4 +1,10 @@
1
- import { DEFAULT_QRP_ENTRY_BITS, TYPE } from "../const";
1
+ import { TYPE } from "../const";
2
+ import {
3
+ DEFAULT_QRP_ENTRY_BITS,
4
+ QrpTable,
5
+ validateRemoteQrpPatchSequence,
6
+ validateRemoteQrpReset,
7
+ } from "../query_routing/qrp";
2
8
  import { errMsg } from "../shared";
3
9
  import { parseRouteTableUpdate } from "./codec";
4
10
  import type { GnutellaServent } from "./node";
@@ -7,11 +13,6 @@ import {
7
13
  sendPublishedQrpToMeshPeers,
8
14
  } from "./node_query_routing";
9
15
  import type { Peer } from "./node_types";
10
- import {
11
- QrpTable,
12
- validateRemoteQrpPatchSequence,
13
- validateRemoteQrpReset,
14
- } from "./qrp";
15
16
 
16
17
  function rejectQrpUpdate(
17
18
  node: GnutellaServent,