fleetmesh-node 0.2.0__py3-none-any.whl
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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""A FIPS daemon stand-in for the local testnet.
|
|
2
|
+
|
|
3
|
+
Speaks enough of the fipsd control socket protocol for node/fips_transport.py
|
|
4
|
+
to bind, send and receive datagrams between the cluster's nodes on one
|
|
5
|
+
machine, with no real fipsd installed. It is a test double, and it lives
|
|
6
|
+
apart from node/testnet/cluster.py because two suites (test_cluster_fips,
|
|
7
|
+
test_didcomm_inbound) drive it on its own, with no cluster around it.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import array
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import threading
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ClusterFipsDaemon:
|
|
20
|
+
"""In-process FIPS Native Control daemon for LocalCluster simulation.
|
|
21
|
+
|
|
22
|
+
Manages bilateral datagram flows between cluster nodes without requiring
|
|
23
|
+
an external fipsd daemon:
|
|
24
|
+
- Handles 'listen' and 'connect' commands over Unix domain control socket.
|
|
25
|
+
- Passes connected datagram sockets to clients via SCM_RIGHTS.
|
|
26
|
+
- Routes bilateral datagrams directly between peer sockets.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, sock_path: str) -> None:
|
|
30
|
+
self.sock_path = os.path.abspath(sock_path)
|
|
31
|
+
if os.path.exists(self.sock_path):
|
|
32
|
+
try:
|
|
33
|
+
os.unlink(self.sock_path)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
os.makedirs(os.path.dirname(self.sock_path), exist_ok=True)
|
|
37
|
+
self.srv_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
38
|
+
self.srv_sock.bind(self.sock_path)
|
|
39
|
+
self.srv_sock.listen(32)
|
|
40
|
+
self.running = True
|
|
41
|
+
self._lock = threading.Lock()
|
|
42
|
+
self.peer_listeners: dict[tuple[str, int], socket.socket] = {}
|
|
43
|
+
self.listeners: dict[int, list[socket.socket]] = {}
|
|
44
|
+
self.listener_conns: dict[socket.socket, socket.socket] = {}
|
|
45
|
+
self.active_pairs: list[tuple[socket.socket, socket.socket]] = []
|
|
46
|
+
self.threads: list[threading.Thread] = []
|
|
47
|
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
48
|
+
self.thread.start()
|
|
49
|
+
|
|
50
|
+
def _run(self) -> None:
|
|
51
|
+
while self.running:
|
|
52
|
+
try:
|
|
53
|
+
conn, _ = self.srv_sock.accept()
|
|
54
|
+
except Exception:
|
|
55
|
+
break
|
|
56
|
+
t = threading.Thread(target=self._handle_client, args=(conn,), daemon=True)
|
|
57
|
+
t.start()
|
|
58
|
+
self.threads.append(t)
|
|
59
|
+
|
|
60
|
+
def _handle_client(self, conn: socket.socket) -> None:
|
|
61
|
+
buf = b""
|
|
62
|
+
client_node_pk: Optional[str] = None
|
|
63
|
+
registered_listeners: list[tuple[Optional[str], int, socket.socket]] = []
|
|
64
|
+
try:
|
|
65
|
+
while self.running:
|
|
66
|
+
try:
|
|
67
|
+
chunk = conn.recv(4096)
|
|
68
|
+
if not chunk:
|
|
69
|
+
break
|
|
70
|
+
buf += chunk
|
|
71
|
+
while b"\n" in buf:
|
|
72
|
+
line, buf = buf.split(b"\n", 1)
|
|
73
|
+
if not line.strip():
|
|
74
|
+
continue
|
|
75
|
+
req = json.loads(line.decode("utf-8"))
|
|
76
|
+
cmd = req.get("command") or req.get("cmd")
|
|
77
|
+
params = req.get("params") if isinstance(req.get("params"), dict) else req
|
|
78
|
+
port = int(params.get("local_port") or params.get("remote_port") or params.get("port") or 11801)
|
|
79
|
+
|
|
80
|
+
# The socket protocol has three verbs; a request naming none of them
|
|
81
|
+
# is ignored and the connection stays open, as before.
|
|
82
|
+
match cmd:
|
|
83
|
+
case "listen":
|
|
84
|
+
# Client wishes to listen on port
|
|
85
|
+
client_node_pk = params.get("node_pk") or req.get("node_pk") or client_node_pk
|
|
86
|
+
client_sock, daemon_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
87
|
+
with self._lock:
|
|
88
|
+
self.active_pairs.append((client_sock, daemon_sock))
|
|
89
|
+
self.listeners.setdefault(port, []).append(daemon_sock)
|
|
90
|
+
if client_node_pk:
|
|
91
|
+
self.peer_listeners[(client_node_pk, port)] = daemon_sock
|
|
92
|
+
self.listener_conns[daemon_sock] = conn
|
|
93
|
+
registered_listeners.append((client_node_pk, port, daemon_sock))
|
|
94
|
+
|
|
95
|
+
resp_dict = {
|
|
96
|
+
"status": "ok",
|
|
97
|
+
"port": port,
|
|
98
|
+
"state": "listening",
|
|
99
|
+
"data": {
|
|
100
|
+
"backlog": 16,
|
|
101
|
+
"local_port": port,
|
|
102
|
+
"node": client_node_pk or "local",
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
resp = json.dumps(resp_dict).encode("utf-8") + b"\n"
|
|
106
|
+
fds = array.array("i", [client_sock.fileno()])
|
|
107
|
+
cmsg = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
|
|
108
|
+
conn.sendmsg([resp], cmsg)
|
|
109
|
+
client_sock.close()
|
|
110
|
+
|
|
111
|
+
case "connect":
|
|
112
|
+
peer = str(params.get("peer") or req.get("peer") or "")
|
|
113
|
+
if not client_node_pk and (params.get("caller") or req.get("caller")):
|
|
114
|
+
client_node_pk = params.get("caller") or req.get("caller")
|
|
115
|
+
|
|
116
|
+
with self._lock:
|
|
117
|
+
target_sock = self.peer_listeners.get((peer, port))
|
|
118
|
+
if not target_sock:
|
|
119
|
+
for (p_k, p_port), s_sock in self.peer_listeners.items():
|
|
120
|
+
if p_port == port and (p_k.startswith(peer) or peer.startswith(p_k)):
|
|
121
|
+
target_sock = s_sock
|
|
122
|
+
break
|
|
123
|
+
if not target_sock and not peer and port in self.listeners and self.listeners[port]:
|
|
124
|
+
target_sock = self.listeners[port][0]
|
|
125
|
+
|
|
126
|
+
if target_sock is not None:
|
|
127
|
+
target_conn = self.listener_conns.get(target_sock)
|
|
128
|
+
if target_conn is not None:
|
|
129
|
+
try:
|
|
130
|
+
peek = target_conn.recv(1, socket.MSG_PEEK | socket.MSG_DONTWAIT)
|
|
131
|
+
if peek == b"":
|
|
132
|
+
target_sock = None
|
|
133
|
+
except (BlockingIOError, socket.error):
|
|
134
|
+
pass
|
|
135
|
+
except Exception:
|
|
136
|
+
target_sock = None
|
|
137
|
+
|
|
138
|
+
if target_sock is None or getattr(target_sock, 'fileno', lambda: -1)() == -1:
|
|
139
|
+
resp = json.dumps({"status": "error", "error": f"peer {peer}:{port} not listening or unreachable"}).encode("utf-8") + b"\n"
|
|
140
|
+
conn.sendall(resp)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
client_sock, daemon_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
144
|
+
with self._lock:
|
|
145
|
+
self.active_pairs.append((client_sock, daemon_sock))
|
|
146
|
+
|
|
147
|
+
def forward(src: socket.socket, dst: socket.socket):
|
|
148
|
+
while self.running:
|
|
149
|
+
try:
|
|
150
|
+
data = src.recv(65536)
|
|
151
|
+
if not data:
|
|
152
|
+
break
|
|
153
|
+
dst.send(data)
|
|
154
|
+
except Exception:
|
|
155
|
+
break
|
|
156
|
+
t = threading.Thread(target=forward, args=(daemon_sock, target_sock), daemon=True)
|
|
157
|
+
t.start()
|
|
158
|
+
self.threads.append(t)
|
|
159
|
+
|
|
160
|
+
resp_dict = {
|
|
161
|
+
"status": "connected",
|
|
162
|
+
"port": port,
|
|
163
|
+
"peer": peer,
|
|
164
|
+
"flow": f"{peer}:{port}",
|
|
165
|
+
"data": {
|
|
166
|
+
"flow_id": len(self.active_pairs),
|
|
167
|
+
"local_port": 49152,
|
|
168
|
+
"max_payload": 1170,
|
|
169
|
+
"node": client_node_pk or "local",
|
|
170
|
+
"peer": peer,
|
|
171
|
+
"remote_port": port,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
resp = json.dumps(resp_dict).encode("utf-8") + b"\n"
|
|
175
|
+
fds = array.array("i", [client_sock.fileno()])
|
|
176
|
+
cmsg = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
|
|
177
|
+
conn.sendmsg([resp], cmsg)
|
|
178
|
+
client_sock.close()
|
|
179
|
+
|
|
180
|
+
case "status":
|
|
181
|
+
resp = json.dumps({
|
|
182
|
+
"status": "ok",
|
|
183
|
+
"protocol": "fleetmesh-fips/1",
|
|
184
|
+
"listeners": len(self.listeners),
|
|
185
|
+
"active_flows": len(self.active_pairs),
|
|
186
|
+
}).encode("utf-8") + b"\n"
|
|
187
|
+
conn.sendall(resp)
|
|
188
|
+
except Exception:
|
|
189
|
+
break
|
|
190
|
+
finally:
|
|
191
|
+
with self._lock:
|
|
192
|
+
for pk, p, ds in registered_listeners:
|
|
193
|
+
if pk and (pk, p) in self.peer_listeners and self.peer_listeners[(pk, p)] is ds:
|
|
194
|
+
del self.peer_listeners[(pk, p)]
|
|
195
|
+
if p in self.listeners and ds in self.listeners[p]:
|
|
196
|
+
try:
|
|
197
|
+
self.listeners[p].remove(ds)
|
|
198
|
+
except ValueError:
|
|
199
|
+
pass
|
|
200
|
+
self.listener_conns.pop(ds, None)
|
|
201
|
+
try:
|
|
202
|
+
conn.close()
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def stop(self) -> None:
|
|
207
|
+
self.running = False
|
|
208
|
+
try:
|
|
209
|
+
self.srv_sock.close()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
with self._lock:
|
|
213
|
+
for c_sock, d_sock in self.active_pairs:
|
|
214
|
+
try:
|
|
215
|
+
c_sock.close()
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
try:
|
|
219
|
+
d_sock.close()
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
self.active_pairs.clear()
|
|
223
|
+
self.listeners.clear()
|
|
224
|
+
self.peer_listeners.clear()
|
|
225
|
+
self.listener_conns.clear()
|
|
226
|
+
if os.path.exists(self.sock_path):
|
|
227
|
+
try:
|
|
228
|
+
os.unlink(self.sock_path)
|
|
229
|
+
except OSError:
|
|
230
|
+
pass
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""The transport adapter, and its first implementation: gift wraps over relays.
|
|
2
|
+
|
|
3
|
+
A DIDComm plaintext message goes out as a JWE (didcomm.py) inside a NIP-59
|
|
4
|
+
wrap (giftwrap.py) published as kind 21059 to every relay this node uses,
|
|
5
|
+
addressed to the peer's node key. Inbound, the node holds one subscription
|
|
6
|
+
per relay for kind 21059 with #p equal to its own key, unwraps, decrypts,
|
|
7
|
+
and checks that the three names agree: the seal's signer, the envelope's
|
|
8
|
+
skid, and the plaintext's `from` must all be the same node. Any disagreement
|
|
9
|
+
is dropped, not surfaced, because a message that lies about who sent it is
|
|
10
|
+
not a message.
|
|
11
|
+
|
|
12
|
+
Every message carries a thread. A reply's `thid` is the id of the message it
|
|
13
|
+
answers, so request() can wait for exactly its answer while unrelated
|
|
14
|
+
traffic goes to the inbox. This is the interface the direct rungs (fips-native,
|
|
15
|
+
iroh, libp2p) implement behind TransportRouter; the NIP says this one always works.
|
|
16
|
+
|
|
17
|
+
It does not. A relay can accept a stored wrap, answer OK, and never serve it -
|
|
18
|
+
relay.pocketstr.com did exactly that to every receipt for seventeen hours on
|
|
19
|
+
2026-09-07, and four of the largest public relays refuse the ephemeral kind
|
|
20
|
+
outright. The publish result is the relay's word about itself, not about
|
|
21
|
+
delivery, and nothing in this node could tell the two apart. So every stored
|
|
22
|
+
message now asks for an acknowledgment and is held until one arrives, using
|
|
23
|
+
the two headers DIDComm v2.1 defines for it: `please_ack` on the message and
|
|
24
|
+
`ack` on the answer, carried by the empty message when nothing else is due.
|
|
25
|
+
A peer that has spoken since and still has not acknowledged is the proof
|
|
26
|
+
that the stored path failed, and that is what `sweep()` records. The ack is
|
|
27
|
+
a live wrap, because the peer is online by construction when it sends one.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')))
|
|
35
|
+
# nostr and router do `from . import const` while this file is still running;
|
|
36
|
+
# that resolves as a submodule import against the half-built package, so the
|
|
37
|
+
# order here is not load-bearing. const is a leaf and stays first anyway, so
|
|
38
|
+
# the dependency reads the way it runs. node/tests/test_imports.py imports
|
|
39
|
+
# the package and each submodule cold to hold that.
|
|
40
|
+
from . import const, fips_rung, iroh_rung, ladder, netfetch, relay_rung, router, ws # noqa: E402
|
|
41
|
+
from .const import ( # noqa: E402
|
|
42
|
+
WRAP_KIND,
|
|
43
|
+
RELAY_CONTENT_MAX,
|
|
44
|
+
FRAGMENT_BYTES,
|
|
45
|
+
MAX_FRAGMENTS,
|
|
46
|
+
ENVELOPE_MAX,
|
|
47
|
+
SEEN_MAX,
|
|
48
|
+
PARTIALS_PER_SENDER,
|
|
49
|
+
PARTIALS_MAX,
|
|
50
|
+
INBOX_MAX,
|
|
51
|
+
DROPPED_KEPT,
|
|
52
|
+
BACKOFF_MAX,
|
|
53
|
+
EMPTY_TYPE,
|
|
54
|
+
ACK_SLACK,
|
|
55
|
+
ACK_GRACE,
|
|
56
|
+
ACK_EXPIRE,
|
|
57
|
+
TransportError,
|
|
58
|
+
Inbound,
|
|
59
|
+
_foreign,
|
|
60
|
+
)
|
|
61
|
+
from .relay_rung import NostrTransport, RelayRung # noqa: E402
|
|
62
|
+
from .router import TransportRouter # noqa: E402
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
'WRAP_KIND',
|
|
66
|
+
'RELAY_CONTENT_MAX',
|
|
67
|
+
'FRAGMENT_BYTES',
|
|
68
|
+
'MAX_FRAGMENTS',
|
|
69
|
+
'ENVELOPE_MAX',
|
|
70
|
+
'SEEN_MAX',
|
|
71
|
+
'PARTIALS_PER_SENDER',
|
|
72
|
+
'PARTIALS_MAX',
|
|
73
|
+
'INBOX_MAX',
|
|
74
|
+
'DROPPED_KEPT',
|
|
75
|
+
'BACKOFF_MAX',
|
|
76
|
+
'EMPTY_TYPE',
|
|
77
|
+
'ACK_SLACK',
|
|
78
|
+
'ACK_GRACE',
|
|
79
|
+
'ACK_EXPIRE',
|
|
80
|
+
'TransportError',
|
|
81
|
+
'Inbound',
|
|
82
|
+
'_foreign',
|
|
83
|
+
'NostrTransport',
|
|
84
|
+
'RelayRung',
|
|
85
|
+
'TransportRouter',
|
|
86
|
+
'ladder',
|
|
87
|
+
'const',
|
|
88
|
+
'fips_rung',
|
|
89
|
+
'iroh_rung',
|
|
90
|
+
'netfetch',
|
|
91
|
+
'relay_rung',
|
|
92
|
+
'router',
|
|
93
|
+
'ws',
|
|
94
|
+
]
|
node/transport/const.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Limits, kinds and timeouts shared by every transport rung, plus TransportError and Inbound.
|
|
2
|
+
|
|
3
|
+
Separate so that the rungs and the router can read each value through this module at call time,
|
|
4
|
+
which is what lets a test (or an operator) reassign one and have every reader see the new value.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from . import didcomm_inbound
|
|
11
|
+
from fleetmesh_core.crypto import giftwrap
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
WRAP_KIND: int = giftwrap.WRAP_LIVE
|
|
15
|
+
# A wrap's seal is one nip44/2 payload, and nip44 caps plaintext at 65535
|
|
16
|
+
# bytes. A deliver of a few dozen events with their envelopes is bigger, so
|
|
17
|
+
# an envelope larger than this rides in numbered fragments, each its own wrap,
|
|
18
|
+
# reassembled by the receiver before anything is decrypted.
|
|
19
|
+
# A fragment is plaintext; what reaches the relay is that text sealed and then
|
|
20
|
+
# wrapped, two NIP-44 layers, each padded and base64. A 40000-byte fragment
|
|
21
|
+
# became a 76548-byte wrap content, and relay.pocketstr.com caps content at
|
|
22
|
+
# 65536, so every deliver over one fragment was refused and the peer waited on
|
|
23
|
+
# an envelope that never finished. 24000 wraps to 54704: under the cap with
|
|
24
|
+
# room for a relay that pads differently. test_transport.py holds it there.
|
|
25
|
+
RELAY_CONTENT_MAX: int = 65536
|
|
26
|
+
FRAGMENT_BYTES: int = 24000
|
|
27
|
+
# The receiver refuses an envelope claiming more than this many fragments, so
|
|
28
|
+
# a sender that splits into more is publishing wraps nobody will ever
|
|
29
|
+
# reassemble. It recorded nothing and the peer simply timed out - the same
|
|
30
|
+
# shape as the fragment that did not fit: the sender never looked at whether
|
|
31
|
+
# what it sent could arrive. Both ends read this number.
|
|
32
|
+
MAX_FRAGMENTS: int = 64
|
|
33
|
+
ENVELOPE_MAX: int = FRAGMENT_BYTES * MAX_FRAGMENTS
|
|
34
|
+
# How long a peer's kind 10050 relay list (NIP-17) is trusted before it is read again.
|
|
35
|
+
DM_RELAYS_TTL: int = 3600
|
|
36
|
+
# What a peer, or a relay, may make this node hold on its behalf. Every one of
|
|
37
|
+
# these was unbounded: the set of wrap ids seen, the fragments of envelopes
|
|
38
|
+
# nobody finished, the inbox, and the list of drops. A node that runs for a
|
|
39
|
+
# month must not remember every wrap it ever saw, and a peer that sends the
|
|
40
|
+
# first fragment of ten thousand envelopes must not own the node's memory.
|
|
41
|
+
SEEN_MAX: int = 20000
|
|
42
|
+
PARTIALS_PER_SENDER: int = 4
|
|
43
|
+
PARTIALS_MAX: int = 64
|
|
44
|
+
INBOX_MAX: int = 10000
|
|
45
|
+
DROPPED_KEPT: int = 200
|
|
46
|
+
BACKOFF_MAX: float = 30.0
|
|
47
|
+
# The empty message: DIDComm v2.1's carrier for headers with no body, which
|
|
48
|
+
# is what a pure acknowledgment is. It is handled here, below the protocols,
|
|
49
|
+
# and never reaches a handler.
|
|
50
|
+
EMPTY_TYPE: str = 'https://didcomm.org/empty/1.0/empty'
|
|
51
|
+
# A stored message is judged undelivered only against the peer's own
|
|
52
|
+
# behaviour, never against a clock alone: the peer may be offline for a day,
|
|
53
|
+
# which is the reason the message was stored. ACK_SLACK is how long after the
|
|
54
|
+
# send a message from the peer counts as "heard since" - a relay serves a
|
|
55
|
+
# stored wrap to a live subscription within seconds, so a peer that is
|
|
56
|
+
# talking two minutes later has had it if it was ever going to. ACK_GRACE is
|
|
57
|
+
# how long after that first hearing the verdict waits, because a node coming
|
|
58
|
+
# back may send a receipt before it opens the replayed wrap. ACK_EXPIRE is the
|
|
59
|
+
# relay's own horizon: past it nothing can arrive and the entry is dropped
|
|
60
|
+
# without a verdict.
|
|
61
|
+
ACK_SLACK: int = 120
|
|
62
|
+
ACK_GRACE: int = 60
|
|
63
|
+
ACK_EXPIRE: int = giftwrap.TWO_DAYS
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TransportError(Exception):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Anoncrypt on a direct rung. A JWE with no skid proves nothing about who
|
|
71
|
+
# wrote it, and on a fips flow, an iroh stream or a libp2p stream there is
|
|
72
|
+
# no seal either, so Inbound.sender is whatever `from` claimed. Every
|
|
73
|
+
# handler in this node identifies the peer by inbound.sender, and the one
|
|
74
|
+
# that checks `authenticated` for itself (mediation) is the exception, not
|
|
75
|
+
# the rule. So a direct rung delivers an unauthenticated message only when
|
|
76
|
+
# its type is one that is anonymous by design: a protocol whose handler
|
|
77
|
+
# already treats the sender as a stranger. On the relay rung the seal
|
|
78
|
+
# signer is verified (giftwrap.unwrap), so this gate does not apply there.
|
|
79
|
+
#
|
|
80
|
+
# - routing/2.0/forward: anyone may post to a leaf through its mediator;
|
|
81
|
+
# handle_forward checks body.next against the keylist, never the sender.
|
|
82
|
+
# - trust-ping/2.0: reachability, anonymous by the DIDComm spec; no handler
|
|
83
|
+
# in this node acts on it.
|
|
84
|
+
# - discover-features/2.0/queries: answered from the public descriptor, so a
|
|
85
|
+
# stranger learns nothing a relay would not tell it; the handler writes
|
|
86
|
+
# nothing against the sender.
|
|
87
|
+
# Peering propose is NOT here: the offer handler writes a grant to `sender`.
|
|
88
|
+
ANONYMOUS_OK: tuple[str, ...] = (
|
|
89
|
+
'https://didcomm.org/routing/2.0/forward',
|
|
90
|
+
'https://didcomm.org/trust-ping/2.0/',
|
|
91
|
+
'https://didcomm.org/discover-features/2.0/queries',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def anonymous_allowed(msg_type: Any) -> bool:
|
|
96
|
+
"""True if an unauthenticated (anoncrypt) message of this type may be
|
|
97
|
+
delivered off a direct rung. Exact match or prefix with a trailing slash."""
|
|
98
|
+
if not isinstance(msg_type, str):
|
|
99
|
+
return False
|
|
100
|
+
for allowed in ANONYMOUS_OK:
|
|
101
|
+
if msg_type == allowed or (allowed.endswith('/') and msg_type.startswith(allowed)):
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Inbound and the envelope-opening rule live in didcomm_inbound so that every
|
|
107
|
+
# rung imports one definition; the name is re-exported here because the rest
|
|
108
|
+
# of the node has always said transport.Inbound.
|
|
109
|
+
Inbound = didcomm_inbound.Inbound
|
|
110
|
+
_foreign = didcomm_inbound.foreign
|