aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py40
1 files changed, 40 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 003cd23..64ba75c 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -143,6 +143,16 @@ def _link_preview_cache_put(url: str, value: dict) -> None:
_link_preview_cache.pop(oldest, None)
_link_preview_cache[url] = (time.time(), value)
+
+# A member pasting a link is normal; a member — or a hub minting tokens for many
+# accounts — firing hundreds is amplification/DoS and a way to make the node
+# reach arbitrary hosts on demand (finding M3). Only a real outbound fetch is
+# counted (a cache hit costs nothing), and the ceilings are generous enough that
+# ordinary chat never meets them.
+_LINK_PREVIEW_RATE_WINDOW = 60.0
+_LINK_PREVIEW_RATE_PER_CONN = 15
+_LINK_PREVIEW_RATE_NODE = 60
+
# Upload limits (finding C5a). Uploads used to land directly in the shared root under
# a name the client chose, overwriting whatever was already there — which both violated
# node sovereignty and defeated the delete authorization (overwrite a file, become its
@@ -3588,6 +3598,27 @@ class WebRTCPeerSession:
],
})
+ def _link_preview_rate_ok(self) -> bool:
+ """
+ True when this preview fetch is within both the per-connection and the
+ node-wide window; records it when so, and both counts are trimmed to the
+ window on every call so the lists cannot grow without bound.
+ """
+ now = time.monotonic()
+ w = _LINK_PREVIEW_RATE_WINDOW
+ mine = [t for t in getattr(self, "_link_preview_hits", []) if now - t < w]
+ node = [t for t in self._ctx.get("link_preview_hits", []) if now - t < w]
+ if (len(mine) >= _LINK_PREVIEW_RATE_PER_CONN
+ or len(node) >= _LINK_PREVIEW_RATE_NODE):
+ self._link_preview_hits = mine
+ self._ctx["link_preview_hits"] = node
+ return False
+ mine.append(now)
+ node.append(now)
+ self._link_preview_hits = mine
+ self._ctx["link_preview_hits"] = node
+ return True
+
async def _do_link_preview_request(self, msg: dict) -> None:
"""
Unfurl a URL a member pasted into chat (draft-v6 §2.7 enrichment rule:
@@ -3608,6 +3639,15 @@ class WebRTCPeerSession:
self._send({**cached, "type": MNP.LINK_PREVIEW_RESP, "v": MNP_VERSION})
return
+ if not self._link_preview_rate_ok():
+ # Same shape as any other miss — the client shows the bare link. A
+ # rate-limited result is not cached, so it is retried once the
+ # window clears rather than pinned as "no preview".
+ log.debug("link_preview_req: rate-limited (peer=%s)", self._peer_id)
+ self._send({"type": MNP.LINK_PREVIEW_RESP, "v": MNP_VERSION,
+ "url": key, "ok": False})
+ return
+
resp: dict = {"type": MNP.LINK_PREVIEW_RESP, "v": MNP_VERSION,
"url": key, "ok": False}
try: