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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,942 @@
1
+ """Experimental QUIC-shaped framing over UDP for the iroh rung. Not encrypted.
2
+
3
+ What this module is: a pure-stdlib framing layer that borrows QUIC's wire
4
+ shapes (RFC 9000 variable-length integers, long and short headers, STREAM /
5
+ ACK / PING / CONNECTION_CLOSE frames, per-stream offset reassembly, a
6
+ STUN-style reflexive probe for NAT pinholes) so that several DIDComm
7
+ messages can be in flight on one UDP 4-tuple without head-of-line blocking.
8
+
9
+ What it is not: QUIC. There is no TLS 1.3 handshake, no packet protection,
10
+ no header protection, no connection-ID authentication. Every byte on the
11
+ wire is readable and forgeable by anyone on the path, and a datagram
12
+ claiming a connection ID is believed. The module used to describe itself as
13
+ a real QUIC transport; that was not true and the words are corrected here
14
+ because a reader deciding what this rung protects must not be misled.
15
+
16
+ Confidentiality and authenticity come from one place only: the DIDComm JWE
17
+ carried inside each stream. The receiving side opens every completed stream
18
+ with didcomm_inbound.open_inbound, which refuses plaintext and binds the
19
+ sender's name to the ECDH-1PU skid, so a forged datagram can at worst waste
20
+ a stream, never speak as a peer. Anything this layer offers beyond that -
21
+ ordering, reassembly, the probe - is best-effort plumbing, and it is bounded
22
+ (MAX_CONNECTIONS, MAX_STREAM_BYTES, MAX_CHUNKS_PER_STREAM, MAX_STREAMS_PER_CONN)
23
+ because an unauthenticated UDP listener's only defence against a flood is
24
+ to refuse to remember too much of it.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import collections
29
+ import json
30
+ import logging
31
+ import os
32
+ import queue
33
+ import socket
34
+ import struct
35
+ import threading
36
+ import time
37
+ from typing import (
38
+ Any,
39
+ Callable,
40
+ Counter,
41
+ Deque,
42
+ Mapping,
43
+ Optional,
44
+ Union,
45
+ )
46
+
47
+ from fleetmesh_core.identity.did import did_nostr
48
+ from fleetmesh_core.identity import didcomm
49
+ from .. import didcomm_inbound
50
+ from .. import const as transport_const
51
+ from ..const import Inbound, TransportError
52
+ from fleetmesh_core.identity.iroh_ticket import ALPN, IrohTicket
53
+
54
+ log = logging.getLogger(__name__)
55
+
56
+ # QUIC Packet Header Types
57
+ PACKET_TYPE_INITIAL: int = 0x00
58
+ PACKET_TYPE_0RTT: int = 0x01
59
+ PACKET_TYPE_HANDSHAKE: int = 0x02
60
+ PACKET_TYPE_RETRY: int = 0x03
61
+
62
+ # QUIC Version (1 = RFC 9000, 0x49524f48 = IROH)
63
+ QUIC_VERSION_1: int = 0x00000001
64
+ IROH_ALPN: str = ALPN
65
+
66
+ # QUIC Frame Types
67
+ FRAME_PADDING: int = 0x00
68
+ FRAME_PING: int = 0x01
69
+ FRAME_ACK: int = 0x02
70
+ FRAME_RESET_STREAM: int = 0x04
71
+ FRAME_STREAM: int = 0x08 # 0x08..0x0f
72
+ FRAME_CONNECTION_CLOSE: int = 0x1c
73
+ FRAME_PROBE_REQ: int = 0x50 # Fleetmesh P2P NAT Probe Request
74
+ FRAME_PROBE_RESP: int = 0x51 # Fleetmesh P2P NAT Probe Response
75
+
76
+ # Stream Frame Flags
77
+ STREAM_FLAG_OFF: int = 0x04
78
+ STREAM_FLAG_LEN: int = 0x02
79
+ STREAM_FLAG_FIN: int = 0x01
80
+
81
+ MAX_DATAGRAM_SIZE: int = 1472 # Standard Ethernet MTU minus IP/UDP headers
82
+
83
+ # Bounds on what an unauthenticated UDP listener will remember. A stream
84
+ # carries one DIDComm envelope, so a stream larger than the envelope cap is
85
+ # not a message; chunks are counted as well because a sender can fill the
86
+ # byte budget with one-byte pieces at distinct offsets and the dict of
87
+ # offsets is what grows. Connections are evicted oldest-idle-first when the
88
+ # table is full, because a single host on the path can mint as many
89
+ # connection IDs as it has packets.
90
+ MAX_CONNECTIONS: int = 256
91
+ MAX_STREAMS_PER_CONN: int = 64
92
+ MAX_STREAM_BYTES: int = didcomm_inbound.MAX_ENVELOPE_BYTES
93
+ MAX_CHUNKS_PER_STREAM: int = 4096
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # QUIC Variable-Length Integer Encoding (RFC 9000 Section 16)
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def encode_varint(v: int) -> bytes:
101
+ """Encode an integer into QUIC variable-length format (1, 2, 4, or 8 bytes)."""
102
+ if v < 0:
103
+ raise ValueError(f"QUIC varint cannot be negative: {v}")
104
+ if v < 64:
105
+ return bytes([v])
106
+ elif v < 16384:
107
+ return struct.pack('>H', 0x4000 | v)
108
+ elif v < 1073741824:
109
+ return struct.pack('>I', 0x80000000 | v)
110
+ elif v < (1 << 62):
111
+ return struct.pack('>Q', 0xc000000000000000 | v)
112
+ raise ValueError(f"QUIC varint value out of range: {v}")
113
+
114
+
115
+ def decode_varint(data: bytes, offset: int = 0) -> tuple[int, int]:
116
+ """Decode QUIC variable-length integer. Returns (value, next_offset)."""
117
+ if offset >= len(data):
118
+ raise ValueError("truncated varint buffer")
119
+ first = data[offset]
120
+ prefix = first >> 6
121
+ if prefix == 0:
122
+ return first & 0x3f, offset + 1
123
+ elif prefix == 1:
124
+ if offset + 2 > len(data):
125
+ raise ValueError("truncated 2-byte varint")
126
+ val = struct.unpack('>H', data[offset:offset + 2])[0] & 0x3fff
127
+ return val, offset + 2
128
+ elif prefix == 2:
129
+ if offset + 4 > len(data):
130
+ raise ValueError("truncated 4-byte varint")
131
+ val = struct.unpack('>I', data[offset:offset + 4])[0] & 0x3fffffff
132
+ return val, offset + 4
133
+ else:
134
+ if offset + 8 > len(data):
135
+ raise ValueError("truncated 8-byte varint")
136
+ val = struct.unpack('>Q', data[offset:offset + 8])[0] & 0x3fffffffffffffff
137
+ return val, offset + 8
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # QUIC Frame Structures
142
+ # ---------------------------------------------------------------------------
143
+
144
+ class StreamFrame:
145
+ """QUIC STREAM Frame (RFC 9000 Section 19.8)."""
146
+
147
+ def __init__(
148
+ self,
149
+ stream_id: int,
150
+ offset: int = 0,
151
+ data: bytes = b'',
152
+ fin: bool = False,
153
+ ) -> None:
154
+ self.stream_id: int = stream_id
155
+ self.offset: int = offset
156
+ self.data: bytes = data
157
+ self.fin: bool = fin
158
+
159
+ def encode(self) -> bytes:
160
+ flags = 0x08
161
+ if self.offset > 0:
162
+ flags |= STREAM_FLAG_OFF
163
+ flags |= STREAM_FLAG_LEN
164
+ if self.fin:
165
+ flags |= STREAM_FLAG_FIN
166
+
167
+ out = [bytes([flags]), encode_varint(self.stream_id)]
168
+ if self.offset > 0:
169
+ out.append(encode_varint(self.offset))
170
+ out.append(encode_varint(len(self.data)))
171
+ out.append(self.data)
172
+ return b''.join(out)
173
+
174
+ @classmethod
175
+ def decode(cls, data: bytes, offset: int) -> tuple[StreamFrame, int]:
176
+ first = data[offset]
177
+ flags = first
178
+ offset += 1
179
+
180
+ stream_id, offset = decode_varint(data, offset)
181
+ stream_offset = 0
182
+ if flags & STREAM_FLAG_OFF:
183
+ stream_offset, offset = decode_varint(data, offset)
184
+
185
+ if flags & STREAM_FLAG_LEN:
186
+ length, offset = decode_varint(data, offset)
187
+ else:
188
+ length = len(data) - offset
189
+
190
+ chunk = data[offset:offset + length]
191
+ offset += length
192
+ fin = bool(flags & STREAM_FLAG_FIN)
193
+ return cls(stream_id=stream_id, offset=stream_offset, data=chunk, fin=fin), offset
194
+
195
+
196
+ class AckFrame:
197
+ """QUIC ACK Frame (RFC 9000 Section 19.3)."""
198
+
199
+ def __init__(self, largest_ack: int, ack_delay_ms: int = 0) -> None:
200
+ self.largest_ack: int = largest_ack
201
+ self.ack_delay_ms: int = ack_delay_ms
202
+
203
+ def encode(self) -> bytes:
204
+ return b''.join([
205
+ bytes([FRAME_ACK]),
206
+ encode_varint(self.largest_ack),
207
+ encode_varint(self.ack_delay_ms),
208
+ encode_varint(0), # ACK range count
209
+ encode_varint(0), # First ACK range
210
+ ])
211
+
212
+ @classmethod
213
+ def decode(cls, data: bytes, offset: int) -> tuple[AckFrame, int]:
214
+ offset += 1 # Skip frame type byte
215
+ largest_ack, offset = decode_varint(data, offset)
216
+ ack_delay, offset = decode_varint(data, offset)
217
+ range_count, offset = decode_varint(data, offset)
218
+ first_range, offset = decode_varint(data, offset)
219
+ for _ in range(range_count):
220
+ _, offset = decode_varint(data, offset)
221
+ _, offset = decode_varint(data, offset)
222
+ return cls(largest_ack=largest_ack, ack_delay_ms=ack_delay), offset
223
+
224
+
225
+ class ConnectionCloseFrame:
226
+ """QUIC CONNECTION_CLOSE Frame (RFC 9000 Section 19.19)."""
227
+
228
+ def __init__(self, error_code: int = 0, reason: str = '') -> None:
229
+ self.error_code: int = error_code
230
+ self.reason: str = reason
231
+
232
+ def encode(self) -> bytes:
233
+ reason_bytes = self.reason.encode('utf-8')
234
+ return b''.join([
235
+ bytes([FRAME_CONNECTION_CLOSE]),
236
+ encode_varint(self.error_code),
237
+ encode_varint(0), # frame type
238
+ encode_varint(len(reason_bytes)),
239
+ reason_bytes,
240
+ ])
241
+
242
+ @classmethod
243
+ def decode(cls, data: bytes, offset: int) -> tuple[ConnectionCloseFrame, int]:
244
+ offset += 1
245
+ code, offset = decode_varint(data, offset)
246
+ _, offset = decode_varint(data, offset)
247
+ rlen, offset = decode_varint(data, offset)
248
+ reason = data[offset:offset + rlen].decode('utf-8', errors='replace')
249
+ return cls(error_code=code, reason=reason), offset + rlen
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # QUIC Packet Serialization (Long and Short Headers)
254
+ # ---------------------------------------------------------------------------
255
+
256
+ class QuicPacket:
257
+ """QUIC Packet representation (Long Header for handshake, Short Header for 1-RTT data)."""
258
+
259
+ def __init__(
260
+ self,
261
+ is_long_header: bool,
262
+ dcid: bytes,
263
+ packet_number: int,
264
+ scid: Optional[bytes] = None,
265
+ packet_type: int = PACKET_TYPE_INITIAL,
266
+ version: int = QUIC_VERSION_1,
267
+ payload: bytes = b'',
268
+ ) -> None:
269
+ self.is_long_header: bool = is_long_header
270
+ self.dcid: bytes = dcid
271
+ self.scid: bytes = scid or b''
272
+ self.packet_number: int = packet_number
273
+ self.packet_type: int = packet_type
274
+ self.version: int = version
275
+ self.payload: bytes = payload
276
+
277
+ def encode(self) -> bytes:
278
+ pn_bytes = struct.pack('>I', self.packet_number)
279
+ if self.is_long_header:
280
+ # Long Header: Form=1 (0x80), Fixed=1 (0x40), Type (2 bits), PN len (2 bits: 3 = 4 bytes)
281
+ first_byte = 0xc0 | (self.packet_type << 4) | 0x03
282
+ header = struct.pack('>B I B', first_byte, self.version, len(self.dcid))
283
+ header += self.dcid
284
+ header += bytes([len(self.scid)]) + self.scid
285
+ # Token length = 0 for normal packets
286
+ header += encode_varint(0)
287
+ # Length = len(pn_bytes) + len(self.payload)
288
+ header += encode_varint(len(pn_bytes) + len(self.payload))
289
+ return header + pn_bytes + self.payload
290
+ else:
291
+ # Short Header: Form=0 (0x00), Fixed=1 (0x40), Key Phase=0, PN len=3 (4 bytes)
292
+ first_byte = 0x43
293
+ header = bytes([first_byte]) + self.dcid + pn_bytes
294
+ return header + self.payload
295
+
296
+ @classmethod
297
+ def decode(cls, data: bytes) -> QuicPacket:
298
+ if len(data) < 2:
299
+ raise ValueError("datagram too short for QUIC header")
300
+ first = data[0]
301
+ is_long = bool(first & 0x80)
302
+
303
+ if is_long:
304
+ if len(data) < 7:
305
+ raise ValueError("datagram too short for QUIC long header")
306
+ ptype = (first >> 4) & 0x03
307
+ version = struct.unpack('>I', data[1:5])[0]
308
+ dcil = data[5]
309
+ offset = 6
310
+ if offset + dcil > len(data):
311
+ raise ValueError("truncated destination CID in long header")
312
+ dcid = data[offset:offset + dcil]
313
+ offset += dcil
314
+
315
+ if offset >= len(data):
316
+ raise ValueError("missing source CID length in long header")
317
+ scil = data[offset]
318
+ offset += 1
319
+ if offset + scil > len(data):
320
+ raise ValueError("truncated source CID in long header")
321
+ scid = data[offset:offset + scil]
322
+ offset += scil
323
+
324
+ token_len, offset = decode_varint(data, offset)
325
+ offset += token_len
326
+
327
+ length, offset = decode_varint(data, offset)
328
+ if offset + 4 > len(data):
329
+ raise ValueError("truncated packet number in long header")
330
+ pn = struct.unpack('>I', data[offset:offset + 4])[0]
331
+ offset += 4
332
+ payload = data[offset:offset + (length - 4)]
333
+ return cls(
334
+ is_long_header=True,
335
+ dcid=dcid,
336
+ scid=scid,
337
+ packet_number=pn,
338
+ packet_type=ptype,
339
+ version=version,
340
+ payload=payload,
341
+ )
342
+ else:
343
+ # Short header: 1-byte flag, 8-byte DCID, 4-byte PN
344
+ if len(data) < 13:
345
+ raise ValueError("datagram too short for QUIC short header")
346
+ dcid = data[1:9]
347
+ pn = struct.unpack('>I', data[9:13])[0]
348
+ payload = data[13:]
349
+ return cls(
350
+ is_long_header=False,
351
+ dcid=dcid,
352
+ packet_number=pn,
353
+ payload=payload,
354
+ )
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Multiplexed Stream State
359
+ # ---------------------------------------------------------------------------
360
+
361
+ class StreamRefused(Exception):
362
+ """A stream grew past what one message may be; it is abandoned."""
363
+
364
+
365
+ class QuicStream:
366
+ """A multiplexed unidirectional or bidirectional stream on a QUIC connection."""
367
+
368
+ def __init__(self, stream_id: int, on_complete: Optional[Callable[[int, bytes], None]] = None) -> None:
369
+ self.stream_id: int = stream_id
370
+ self.on_complete: Optional[Callable[[int, bytes], None]] = on_complete
371
+ self.chunks: dict[int, bytes] = {} # offset -> data
372
+ self.received_bytes: int = 0
373
+ self.fin_received: bool = False
374
+ self.final_offset: Optional[int] = None
375
+ self.completed: bool = False
376
+ self.refused: bool = False
377
+ self.send_offset: int = 0
378
+ self._lock: threading.Lock = threading.Lock()
379
+
380
+ def receive_chunk(self, offset: int, data: bytes, fin: bool) -> None:
381
+ with self._lock:
382
+ if self.completed or self.refused:
383
+ return
384
+ if data:
385
+ # Bound before storing: the offset alone can name a byte
386
+ # position past any message, and the byte count is the sum
387
+ # of what has actually been kept, so neither a far offset
388
+ # nor a flood of tiny pieces gets to grow the dict.
389
+ if offset + len(data) > MAX_STREAM_BYTES or self.received_bytes + len(data) > MAX_STREAM_BYTES:
390
+ self.refused = True
391
+ self.chunks.clear()
392
+ raise StreamRefused('stream %d exceeds %d bytes' % (self.stream_id, MAX_STREAM_BYTES))
393
+ if offset not in self.chunks and len(self.chunks) >= MAX_CHUNKS_PER_STREAM:
394
+ self.refused = True
395
+ self.chunks.clear()
396
+ raise StreamRefused('stream %d exceeds %d chunks' % (self.stream_id, MAX_CHUNKS_PER_STREAM))
397
+ self.received_bytes += len(data)
398
+ self.chunks[offset] = data
399
+ if fin:
400
+ self.fin_received = True
401
+ self.final_offset = offset + len(data)
402
+
403
+ if self.fin_received and not self.completed:
404
+ target_len = self.final_offset if self.final_offset is not None else 0
405
+ if target_len == 0:
406
+ self.completed = True
407
+ if self.on_complete:
408
+ self.on_complete(self.stream_id, b'')
409
+ return
410
+
411
+ # Assemble contiguous chunks starting from 0
412
+ assembled = bytearray()
413
+ curr = 0
414
+ sorted_offsets = sorted(self.chunks.keys())
415
+ for off in sorted_offsets:
416
+ if off == curr:
417
+ chunk = self.chunks[off]
418
+ assembled.extend(chunk)
419
+ curr += len(chunk)
420
+ elif off < curr:
421
+ overlap = curr - off
422
+ if overlap < len(self.chunks[off]):
423
+ chunk = self.chunks[off][overlap:]
424
+ assembled.extend(chunk)
425
+ curr += len(chunk)
426
+
427
+ if curr >= target_len:
428
+ self.completed = True
429
+ payload = bytes(assembled[:target_len])
430
+ if self.on_complete:
431
+ self.on_complete(self.stream_id, payload)
432
+
433
+
434
+ # ---------------------------------------------------------------------------
435
+ # QUIC Connection State Machine
436
+ # ---------------------------------------------------------------------------
437
+
438
+ class QuicConnection:
439
+ """Manages state for one peer-to-peer QUIC connection."""
440
+
441
+ def __init__(
442
+ self,
443
+ local_cid: bytes,
444
+ remote_cid: bytes,
445
+ remote_addr: tuple[str, int],
446
+ is_server: bool = False,
447
+ on_stream_complete: Optional[Callable[[QuicConnection, int, bytes], None]] = None,
448
+ ) -> None:
449
+ self.local_cid: bytes = local_cid
450
+ self.remote_cid: bytes = remote_cid
451
+ self.remote_addr: tuple[str, int] = remote_addr
452
+ self.is_server: bool = is_server
453
+ self.on_stream_complete: Optional[Callable[[QuicConnection, int, bytes], None]] = on_stream_complete
454
+ self.state: str = 'INITIAL' if not is_server else 'ESTABLISHED'
455
+
456
+ self.next_packet_number: int = 0
457
+ self.largest_received_pn: int = -1
458
+ self.rtt_estimate_ms: float = 20.0
459
+ self.last_sent_at: float = 0.0
460
+ self.last_received_at: float = time.time()
461
+
462
+ self.streams: dict[int, QuicStream] = {}
463
+ self.next_stream_id: int = 0 if not is_server else 1
464
+ self._lock: threading.Lock = threading.Lock()
465
+
466
+ # Completed streams queue
467
+ self.incoming_messages: queue.Queue[bytes] = queue.Queue(maxsize=1000)
468
+
469
+ def next_pn(self) -> int:
470
+ with self._lock:
471
+ pn = self.next_packet_number
472
+ self.next_packet_number += 1
473
+ return pn
474
+
475
+ def get_or_create_stream(self, stream_id: int) -> QuicStream:
476
+ with self._lock:
477
+ if stream_id not in self.streams:
478
+ # Completed streams are dropped as new ones open, so the
479
+ # cap counts streams still being assembled, not history.
480
+ if len(self.streams) >= MAX_STREAMS_PER_CONN:
481
+ for sid in [k for k, v in self.streams.items() if v.completed or v.refused]:
482
+ del self.streams[sid]
483
+ if len(self.streams) >= MAX_STREAMS_PER_CONN:
484
+ raise StreamRefused('connection has %d open streams' % MAX_STREAMS_PER_CONN)
485
+ s = QuicStream(stream_id, on_complete=self._on_stream_complete)
486
+ self.streams[stream_id] = s
487
+ return self.streams[stream_id]
488
+
489
+ def _on_stream_complete(self, stream_id: int, data: bytes) -> None:
490
+ try:
491
+ self.incoming_messages.put_nowait(data)
492
+ except queue.Full:
493
+ # Bounded (1000) per connection; a blocking put here would stall
494
+ # the engine's receive loop on one slow consumer. The engine's
495
+ # raw_inbox is the delivery path anyway; this queue is a mirror.
496
+ log.warning('iroh_live: per-connection inbox full, stream %d not mirrored', stream_id)
497
+ if self.on_stream_complete:
498
+ try:
499
+ self.on_stream_complete(self, stream_id, data)
500
+ except Exception:
501
+ # The callback is the engine's (or a test's); a fault in it
502
+ # must not kill the UDP receive loop that called us, but it
503
+ # is a bug and gets logged, not hidden.
504
+ log.exception('iroh_live: on_stream_complete callback failed for stream %d', stream_id)
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # Live Iroh P2P QUIC Transport Engine
509
+ # ---------------------------------------------------------------------------
510
+
511
+ class IrohLiveEngine:
512
+ """High-performance UDP-based QUIC P2P streaming engine for Fleetmesh.
513
+
514
+ Zero external dependencies: uses Python standard library socket.SOCK_DGRAM.
515
+ """
516
+
517
+ def __init__(
518
+ self,
519
+ node_id: str,
520
+ host: str = '127.0.0.1',
521
+ port: int = 0,
522
+ alpn: str = IROH_ALPN,
523
+ timeout: float = 5.0,
524
+ ) -> None:
525
+ self.node_id: str = node_id.lower()
526
+ self.host: str = host
527
+ self.port: int = port
528
+ self.alpn: str = alpn
529
+ self.timeout: float = timeout
530
+
531
+ self.sock: Optional[socket.socket] = None
532
+ self._rx_thread: Optional[threading.Thread] = None
533
+ self._running: bool = False
534
+
535
+ self.connections: dict[bytes, QuicConnection] = {} # CID -> QuicConnection
536
+ self.addr_to_conn: dict[tuple[str, int], QuicConnection] = {} # (host, port) -> QuicConnection
537
+ self._lock: threading.Lock = threading.Lock()
538
+
539
+ # Telemetry & Performance counters
540
+ self.packets_in: int = 0
541
+ self.packets_out: int = 0
542
+ self.bytes_in: int = 0
543
+ self.bytes_out: int = 0
544
+ self.streams_in: int = 0
545
+ self.streams_out: int = 0
546
+ self.dropped: Deque[tuple[str, Optional[str]]] = collections.deque(maxlen=200)
547
+ # Replay and freshness for streams this engine opens itself
548
+ # (receive_didcomm). Wall clock, as send_didcomm dates with.
549
+ self.guard: didcomm_inbound.InboundGuard = didcomm_inbound.InboundGuard()
550
+ self.anonymous_refused: int = 0
551
+ self.evicted: int = 0
552
+ self.drop_counts: Counter[str] = collections.Counter()
553
+ self.probe_responses: dict[str, tuple[str, int]] = {} # nonce -> (reflexive_ip, reflexive_port)
554
+
555
+ # Inbound queues for multiplexed streams and unpacked DIDComm events
556
+ self.raw_inbox: queue.Queue[tuple[QuicConnection, int, bytes]] = queue.Queue(maxsize=10000)
557
+ self.inbox: queue.Queue[Inbound] = queue.Queue(maxsize=10000)
558
+
559
+ @property
560
+ def endpoint(self) -> str:
561
+ return f"{self.host}:{self.port}"
562
+
563
+ @property
564
+ def ticket(self) -> str:
565
+ return IrohTicket(self.node_id, self.host, self.port).encode()
566
+
567
+ def start(self) -> IrohLiveEngine:
568
+ """Bind UDP socket on host:port and launch background async receiver thread."""
569
+ if self.sock is not None:
570
+ return self
571
+
572
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
573
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
574
+ s.bind((self.host, self.port))
575
+ self.port = s.getsockname()[1]
576
+ s.settimeout(0.5)
577
+ self.sock = s
578
+
579
+ self._running = True
580
+ self._rx_thread = threading.Thread(target=self._rx_loop, daemon=True, name=f"iroh-quic-{self.port}")
581
+ self._rx_thread.start()
582
+ return self
583
+
584
+ def stop(self) -> None:
585
+ """Gracefully stop engine and close UDP socket."""
586
+ self._running = False
587
+ if self.sock:
588
+ try:
589
+ self.sock.close()
590
+ except OSError:
591
+ pass # best-effort close; the socket may already be gone
592
+ self.sock = None
593
+ if self._rx_thread and self._rx_thread.is_alive():
594
+ self._rx_thread.join(timeout=1.0)
595
+
596
+ def _evict_idle_locked(self) -> None:
597
+ """Forget the connection idle longest. Called with self._lock held."""
598
+ by_addr = list(self.addr_to_conn.items())
599
+ if not by_addr:
600
+ return
601
+ addr, victim = min(by_addr, key=lambda kv: kv[1].last_received_at)
602
+ self.addr_to_conn.pop(addr, None)
603
+ for cid in [k for k, v in self.connections.items() if v is victim]:
604
+ del self.connections[cid]
605
+ self.evicted += 1
606
+
607
+ def _drop(self, reason: str, ident: Optional[str] = None) -> None:
608
+ prefix = reason.split(':', 1)[0].strip()
609
+ self.drop_counts[prefix] += 1
610
+ self.dropped.append((reason, ident))
611
+
612
+ def _on_conn_stream_complete(self, conn: QuicConnection, stream_id: int, data: bytes) -> None:
613
+ try:
614
+ self.raw_inbox.put_nowait((conn, stream_id, data))
615
+ except queue.Full:
616
+ # raw_inbox is bounded (10000): a full one means the consumer
617
+ # is not keeping up, and the stream is dropped and counted
618
+ # like every other drop rather than blocking the receive loop.
619
+ self._drop('inbox full: raw_inbox', '%s:%d' % conn.remote_addr)
620
+
621
+ # -----------------------------------------------------------------------
622
+ # Background Packet Receive Loop
623
+ # -----------------------------------------------------------------------
624
+
625
+ def _rx_loop(self) -> None:
626
+ while self._running and self.sock:
627
+ try:
628
+ raw_bytes, sender_addr = self.sock.recvfrom(65535)
629
+ except (socket.timeout, OSError):
630
+ continue
631
+
632
+ if not raw_bytes:
633
+ continue
634
+
635
+ self.packets_in += 1
636
+ self.bytes_in += len(raw_bytes)
637
+
638
+ try:
639
+ # 1. Check for NAT Probe frames
640
+ if raw_bytes.startswith(bytes([FRAME_PROBE_REQ])):
641
+ nonce = raw_bytes[1:17].hex()
642
+ resp = (
643
+ bytes([FRAME_PROBE_RESP])
644
+ + bytes.fromhex(nonce)
645
+ + socket.inet_aton(sender_addr[0])
646
+ + struct.pack('>H', sender_addr[1])
647
+ )
648
+ if self.sock:
649
+ try:
650
+ self.sock.sendto(resp, sender_addr)
651
+ except OSError:
652
+ pass
653
+ continue
654
+ elif raw_bytes.startswith(bytes([FRAME_PROBE_RESP])):
655
+ nonce = raw_bytes[1:17].hex()
656
+ ref_ip = socket.inet_ntoa(raw_bytes[17:21])
657
+ ref_port = struct.unpack('>H', raw_bytes[21:23])[0]
658
+ self.probe_responses[nonce] = (ref_ip, ref_port)
659
+ continue
660
+
661
+ # 2. Parse QUIC packet
662
+ pkt = QuicPacket.decode(raw_bytes)
663
+ self._handle_packet(pkt, sender_addr)
664
+ except Exception as e:
665
+ self._drop(f"bad packet: {e}", f"{sender_addr[0]}:{sender_addr[1]}")
666
+
667
+ def _handle_packet(self, pkt: QuicPacket, sender_addr: tuple[str, int]) -> None:
668
+ conn = None
669
+ with self._lock:
670
+ # Match by DCID or sender address
671
+ conn = self.connections.get(pkt.dcid) or self.addr_to_conn.get(sender_addr)
672
+ if conn is None and pkt.is_long_header:
673
+ # Inbound new connection request. When the table is full
674
+ # the connection that has been quiet longest goes; a peer
675
+ # that is still talking is never the one evicted for a
676
+ # stranger's first packet.
677
+ if len(self.addr_to_conn) >= MAX_CONNECTIONS:
678
+ self._evict_idle_locked()
679
+ local_cid = pkt.dcid
680
+ remote_cid = pkt.scid or os.urandom(8)
681
+ conn = QuicConnection(
682
+ local_cid=local_cid,
683
+ remote_cid=remote_cid,
684
+ remote_addr=sender_addr,
685
+ is_server=True,
686
+ on_stream_complete=self._on_conn_stream_complete,
687
+ )
688
+ self.connections[local_cid] = conn
689
+ self.connections[remote_cid] = conn
690
+ self.addr_to_conn[sender_addr] = conn
691
+
692
+ if conn is None:
693
+ return
694
+
695
+ conn.last_received_at = time.time()
696
+ conn.largest_received_pn = max(conn.largest_received_pn, pkt.packet_number)
697
+
698
+ # Parse frames inside payload
699
+ offset = 0
700
+ data = pkt.payload
701
+ while offset < len(data):
702
+ ftype = data[offset]
703
+ if ftype == FRAME_PING:
704
+ offset += 1
705
+ self._send_ack(conn)
706
+ elif ftype == FRAME_ACK:
707
+ ack, offset = AckFrame.decode(data, offset)
708
+ elif (ftype & 0xf8) == FRAME_STREAM:
709
+ sframe, offset = StreamFrame.decode(data, offset)
710
+ self.streams_in += 1
711
+ try:
712
+ stream = conn.get_or_create_stream(sframe.stream_id)
713
+ stream.receive_chunk(sframe.offset, sframe.data, sframe.fin)
714
+ except StreamRefused as e:
715
+ self._drop('stream refused: %s' % e, f"{sender_addr[0]}:{sender_addr[1]}")
716
+ continue
717
+ self._send_ack(conn)
718
+ elif ftype == FRAME_CONNECTION_CLOSE:
719
+ cclose, offset = ConnectionCloseFrame.decode(data, offset)
720
+ conn.state = 'CLOSED'
721
+ else:
722
+ offset += 1 # Skip unknown byte
723
+
724
+ def _send_ack(self, conn: QuicConnection) -> None:
725
+ if conn.largest_received_pn >= 0:
726
+ ack_frame = AckFrame(conn.largest_received_pn, ack_delay_ms=2)
727
+ pkt = QuicPacket(
728
+ is_long_header=False,
729
+ dcid=conn.remote_cid,
730
+ packet_number=conn.next_pn(),
731
+ payload=ack_frame.encode(),
732
+ )
733
+ self._send_packet(pkt, conn.remote_addr)
734
+
735
+ def _send_packet(self, pkt: QuicPacket, dest_addr: tuple[str, int]) -> None:
736
+ if not self.sock:
737
+ return
738
+ raw = pkt.encode()
739
+ try:
740
+ self.sock.sendto(raw, dest_addr)
741
+ self.packets_out += 1
742
+ self.bytes_out += len(raw)
743
+ except Exception as e:
744
+ self._drop(f"send failed: {e}", f"{dest_addr[0]}:{dest_addr[1]}")
745
+
746
+ # -----------------------------------------------------------------------
747
+ # Connection Management & Stream Dispatch
748
+ # -----------------------------------------------------------------------
749
+
750
+ def get_or_connect(self, ticket: Union[str, IrohTicket]) -> QuicConnection:
751
+ """Establish or retrieve live QUIC connection to an Iroh peer ticket."""
752
+ t = IrohTicket.parse(ticket) if isinstance(ticket, str) else ticket
753
+ remote_addr = (t.host, t.port)
754
+
755
+ with self._lock:
756
+ conn = self.addr_to_conn.get(remote_addr)
757
+ if conn and conn.state != 'CLOSED':
758
+ return conn
759
+
760
+ # Handshake: allocate connection IDs
761
+ local_cid = os.urandom(8)
762
+ remote_cid = os.urandom(8)
763
+ conn = QuicConnection(
764
+ local_cid=local_cid,
765
+ remote_cid=remote_cid,
766
+ remote_addr=remote_addr,
767
+ is_server=False,
768
+ on_stream_complete=self._on_conn_stream_complete,
769
+ )
770
+
771
+ with self._lock:
772
+ self.connections[local_cid] = conn
773
+ self.connections[remote_cid] = conn
774
+ self.addr_to_conn[remote_addr] = conn
775
+
776
+ # Send Initial packet with PING
777
+ init_pkt = QuicPacket(
778
+ is_long_header=True,
779
+ dcid=remote_cid,
780
+ scid=local_cid,
781
+ packet_number=conn.next_pn(),
782
+ packet_type=PACKET_TYPE_INITIAL,
783
+ payload=bytes([FRAME_PING]),
784
+ )
785
+ self._send_packet(init_pkt, remote_addr)
786
+ conn.state = 'ESTABLISHED'
787
+ return conn
788
+
789
+ def send_stream(self, conn: QuicConnection, data: bytes, stream_id: Optional[int] = None) -> int:
790
+ """Transmit payload across a new or existing multiplexed QUIC stream."""
791
+ sid = stream_id if stream_id is not None else conn.next_stream_id
792
+ if stream_id is None:
793
+ conn.next_stream_id += 4
794
+
795
+ # Chunk payload into MTU-safe STREAM frames
796
+ max_chunk = MAX_DATAGRAM_SIZE - 64
797
+ offset = 0
798
+ while offset < len(data) or len(data) == 0:
799
+ chunk = data[offset:offset + max_chunk]
800
+ offset += len(chunk)
801
+ fin = (offset >= len(data))
802
+
803
+ sframe = StreamFrame(stream_id=sid, offset=offset - len(chunk), data=chunk, fin=fin)
804
+ pkt = QuicPacket(
805
+ is_long_header=False,
806
+ dcid=conn.remote_cid,
807
+ packet_number=conn.next_pn(),
808
+ payload=sframe.encode(),
809
+ )
810
+ self._send_packet(pkt, conn.remote_addr)
811
+ self.streams_out += 1
812
+ if len(data) == 0 or offset >= len(data):
813
+ break
814
+
815
+ return sid
816
+
817
+ def receive_stream(self, timeout: Optional[float] = 5.0) -> Optional[tuple[QuicConnection, int, bytes]]:
818
+ """Fetch completed raw stream (conn, stream_id, payload) from incoming queue."""
819
+ try:
820
+ return self.raw_inbox.get(timeout=timeout)
821
+ except queue.Empty:
822
+ return None
823
+
824
+ def probe_nat(self, target_host: str, target_port: int, timeout: float = 1.0) -> Optional[tuple[str, int]]:
825
+ """Probe remote endpoint for reflexive NAT address discovery."""
826
+ nonce = os.urandom(16)
827
+ probe_req = bytes([FRAME_PROBE_REQ]) + nonce
828
+ if self.sock:
829
+ try:
830
+ self.sock.sendto(probe_req, (target_host, target_port))
831
+ except OSError:
832
+ return None
833
+
834
+ deadline = time.time() + timeout
835
+ n_hex = nonce.hex()
836
+ while time.time() < deadline:
837
+ if n_hex in self.probe_responses:
838
+ return self.probe_responses.pop(n_hex)
839
+ time.sleep(0.02)
840
+ return None
841
+
842
+ def punch_hole(self, target_host: str, target_port: int, count: int = 5, interval: float = 0.05) -> None:
843
+ """Send repeated probe bursts to open/punch NAT pinholes to a remote peer."""
844
+ for _ in range(count):
845
+ nonce = os.urandom(16)
846
+ probe_req = bytes([FRAME_PROBE_REQ]) + nonce
847
+ if self.sock:
848
+ try:
849
+ self.sock.sendto(probe_req, (target_host, target_port))
850
+ except OSError:
851
+ pass
852
+ if interval > 0:
853
+ time.sleep(interval)
854
+
855
+ # -----------------------------------------------------------------------
856
+ # High-Level DIDComm Integration
857
+ # -----------------------------------------------------------------------
858
+
859
+ def send_didcomm(
860
+ self,
861
+ keystore: Any,
862
+ peers: Any,
863
+ message: Mapping[str, Any],
864
+ to_pubkey: str,
865
+ ticket: Union[str, IrohTicket],
866
+ anonymous: bool = False,
867
+ ) -> tuple[bool, str]:
868
+ """Encrypt and dispatch a DIDComm v2 message over a QUIC stream."""
869
+ conn = self.get_or_connect(ticket)
870
+ their_agree = peers.agreement(to_pubkey) if hasattr(peers, 'agreement') else None
871
+ if their_agree is None:
872
+ raise TransportError(f"no live agreement key for peer {to_pubkey[:16]}")
873
+
874
+ msg_dict = dict(message)
875
+ msg_dict.setdefault('from', keystore.did)
876
+ recipient_did = did_nostr(to_pubkey)
877
+ msg_dict.setdefault('to', [recipient_did])
878
+ msg_dict.setdefault('created_time', int(time.time()))
879
+
880
+ kid = did_nostr(to_pubkey) + '#agree'
881
+ sender = None if anonymous else (keystore.kid('agreement'), keystore.agreement_priv)
882
+ jwe = didcomm.encrypt(msg_dict, [(kid, their_agree)], sender=sender)
883
+ envelope_bytes = json.dumps(jwe, separators=(',', ':'), sort_keys=True).encode('utf-8')
884
+
885
+ sid = self.send_stream(conn, envelope_bytes)
886
+ return True, f"delivered via iroh quic stream {sid} to {conn.remote_addr[0]}:{conn.remote_addr[1]}"
887
+
888
+ def receive_didcomm(
889
+ self,
890
+ keystore: Any,
891
+ peers: Any,
892
+ timeout: float = 5.0,
893
+ ) -> Optional[Inbound]:
894
+ """Wait for and unpack incoming DIDComm message from any active connection."""
895
+ deadline = time.time() + timeout
896
+ while time.time() < deadline:
897
+ rem = max(0.01, deadline - time.time())
898
+ try:
899
+ conn, stream_id, raw_bytes = self.raw_inbox.get(timeout=rem)
900
+ except queue.Empty:
901
+ break
902
+
903
+ # One rule for every rung (didcomm_inbound): a JWE this node can
904
+ # open, sender proven by the agreement, or a drop with a reason.
905
+ # The plaintext branch that used to sit here is gone; see
906
+ # iroh_transport._process_inbound_bytes for why.
907
+ inbound, why = didcomm_inbound.open_inbound(raw_bytes, keystore, peers, f"iroh-quic-{stream_id}",
908
+ guard=self.guard, now=int(time.time()))
909
+ if inbound is None:
910
+ self._drop(f"unpack error: {why}", f"{conn.remote_addr[0]}:{conn.remote_addr[1]}")
911
+ continue
912
+ if not inbound.authenticated and not transport_const.anonymous_allowed(inbound.message.get('type')):
913
+ self.anonymous_refused += 1
914
+ self._drop('unauthenticated: anoncrypt %s is not a first-contact type'
915
+ % str(inbound.message.get('type'))[:80], f"{conn.remote_addr[0]}:{conn.remote_addr[1]}")
916
+ continue
917
+ try:
918
+ self.inbox.put_nowait(inbound)
919
+ except queue.Full:
920
+ self._drop('inbox full')
921
+ return inbound
922
+ return None
923
+
924
+ def status(self) -> Mapping[str, Any]:
925
+ """Telemetry snapshot of the live Iroh QUIC engine."""
926
+ return {
927
+ 'node_id': self.node_id,
928
+ 'host': self.host,
929
+ 'port': self.port,
930
+ 'alpn': self.alpn,
931
+ 'running': self._running,
932
+ 'endpoint': self.endpoint,
933
+ 'ticket': self.ticket,
934
+ 'connections_count': len(self.addr_to_conn),
935
+ 'packets_in': self.packets_in,
936
+ 'packets_out': self.packets_out,
937
+ 'bytes_in': self.bytes_in,
938
+ 'bytes_out': self.bytes_out,
939
+ 'streams_in': self.streams_in,
940
+ 'streams_out': self.streams_out,
941
+ 'drops': dict(self.drop_counts),
942
+ }