mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-27 03:01:51 +02:00
Pass Tor E2E shared DM flow and harden mesh relay for fleet participants.
MLS export/reset and accept use live HTTP so uvicorn privacy-core state stays consistent; relay persistence and sender_seal fixes enable invite-accept-shared decrypt across onion peers. Adds participant/e2e compose overlays and harness recovery with optional Tor-only replicate mode. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -863,6 +863,7 @@ _ROUTE_TRANSPORT_POLICY: dict[tuple[str, str], RouteTransportPolicy] = {
|
|||||||
# ── Wormhole DM (strong) ──────────────────────────────────────────
|
# ── Wormhole DM (strong) ──────────────────────────────────────────
|
||||||
("POST", "/api/wormhole/dm/compose"): _local_only_route_policy("private_control_only"),
|
("POST", "/api/wormhole/dm/compose"): _local_only_route_policy("private_control_only"),
|
||||||
("POST", "/api/wormhole/dm/decrypt"): _local_only_route_policy("private_control_only"),
|
("POST", "/api/wormhole/dm/decrypt"): _local_only_route_policy("private_control_only"),
|
||||||
|
("POST", "/api/wormhole/dm/mls-key-package"): _local_only_route_policy("private_control_only"),
|
||||||
("POST", "/api/wormhole/dm/register-key"): _local_only_route_policy("private_control_only"),
|
("POST", "/api/wormhole/dm/register-key"): _local_only_route_policy("private_control_only"),
|
||||||
("POST", "/api/wormhole/dm/prekey/register"): _local_only_route_policy("private_control_only"),
|
("POST", "/api/wormhole/dm/prekey/register"): _local_only_route_policy("private_control_only"),
|
||||||
("POST", "/api/wormhole/dm/bootstrap-encrypt"): _local_only_route_policy("private_control_only"),
|
("POST", "/api/wormhole/dm/bootstrap-encrypt"): _local_only_route_policy("private_control_only"),
|
||||||
|
|||||||
+60
-14
@@ -1072,6 +1072,10 @@ def _release_gate_status(
|
|||||||
|
|
||||||
|
|
||||||
def _validate_privacy_core_startup() -> None:
|
def _validate_privacy_core_startup() -> None:
|
||||||
|
# The wormhole child agent reuses this app on WORMHOLE_PORT; the parent
|
||||||
|
# backend already validated privacy-core before spawning it.
|
||||||
|
if os.environ.get("WORMHOLE_PORT"):
|
||||||
|
return
|
||||||
from services.privacy_core_attestation import validate_privacy_core_startup
|
from services.privacy_core_attestation import validate_privacy_core_startup
|
||||||
|
|
||||||
validate_privacy_core_startup()
|
validate_privacy_core_startup()
|
||||||
@@ -1624,6 +1628,12 @@ def _hydrate_dm_relay_from_chain(events: list[dict]) -> int:
|
|||||||
sender_token_hash = hashlib.sha256(
|
sender_token_hash = hashlib.sha256(
|
||||||
f"hashchain-dm-sender|{event_id}|{canonical.get('node_id', '')}".encode("utf-8")
|
f"hashchain-dm-sender|{event_id}|{canonical.get('node_id', '')}".encode("utf-8")
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
try:
|
||||||
|
from services.mesh.mesh_dm_connect_delivery import relay_push_peer_urls_for_payload
|
||||||
|
|
||||||
|
replication_urls = relay_push_peer_urls_for_payload(dict(payload))
|
||||||
|
except Exception:
|
||||||
|
replication_urls = []
|
||||||
try:
|
try:
|
||||||
result = dm_relay.deposit(
|
result = dm_relay.deposit(
|
||||||
sender_id=str(canonical.get("node_id", "") or ""),
|
sender_id=str(canonical.get("node_id", "") or ""),
|
||||||
@@ -1637,6 +1647,7 @@ def _hydrate_dm_relay_from_chain(events: list[dict]) -> int:
|
|||||||
sender_token_hash=sender_token_hash,
|
sender_token_hash=sender_token_hash,
|
||||||
payload_format=str(payload.get("format", "dm1") or "dm1"),
|
payload_format=str(payload.get("format", "dm1") or "dm1"),
|
||||||
session_welcome=str(payload.get("session_welcome", "") or ""),
|
session_welcome=str(payload.get("session_welcome", "") or ""),
|
||||||
|
replication_peer_urls=replication_urls,
|
||||||
)
|
)
|
||||||
if result.get("ok"):
|
if result.get("ok"):
|
||||||
count += 1
|
count += 1
|
||||||
@@ -7192,6 +7203,10 @@ async def _dm_send_from_signed_request(request: Request):
|
|||||||
"format": payload_format,
|
"format": payload_format,
|
||||||
}
|
}
|
||||||
chain_payload["transport_lock"] = "private_strong"
|
chain_payload["transport_lock"] = "private_strong"
|
||||||
|
if connect_intent:
|
||||||
|
chain_payload["connect_intent"] = connect_intent
|
||||||
|
if lookup_peer_url:
|
||||||
|
chain_payload["lookup_peer_url"] = lookup_peer_url
|
||||||
chain_event = infonet.append_private_dm_message(
|
chain_event = infonet.append_private_dm_message(
|
||||||
node_id=sender_id,
|
node_id=sender_id,
|
||||||
payload=chain_payload,
|
payload=chain_payload,
|
||||||
@@ -7207,7 +7222,8 @@ async def _dm_send_from_signed_request(request: Request):
|
|||||||
or PROTOCOL_VERSION,
|
or PROTOCOL_VERSION,
|
||||||
timestamp=float(timestamp or time.time()),
|
timestamp=float(timestamp or time.time()),
|
||||||
)
|
)
|
||||||
_hydrate_dm_relay_from_chain([chain_event])
|
# Relay deposit is deferred to the private release worker so scoped
|
||||||
|
# connect traffic can synchronously replicate to lookup_peer_url once.
|
||||||
hashchain_spool = {
|
hashchain_spool = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"event_id": str(chain_event.get("event_id", "") or ""),
|
"event_id": str(chain_event.get("event_id", "") or ""),
|
||||||
@@ -9663,6 +9679,43 @@ def _get_contact_trust_level(peer_id: str) -> str:
|
|||||||
return "unpinned"
|
return "unpinned"
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_bundle_matches_invite_pin(peer_id: str, bundle: dict[str, Any]) -> bool:
|
||||||
|
"""True when an invite-pinned contact already matches the supplied bundle."""
|
||||||
|
try:
|
||||||
|
from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts
|
||||||
|
from services.mesh.mesh_wormhole_prekey import trust_fingerprint_for_bundle_record
|
||||||
|
|
||||||
|
contact = dict(list_wormhole_dm_contacts().get(str(peer_id or "").strip()) or {})
|
||||||
|
if str(contact.get("trust_level", "") or "") != "invite_pinned":
|
||||||
|
return False
|
||||||
|
pinned = str(
|
||||||
|
contact.get("remotePrekeyFingerprint", "")
|
||||||
|
or contact.get("invitePinnedTrustFingerprint", "")
|
||||||
|
or ""
|
||||||
|
).strip().lower()
|
||||||
|
if not pinned:
|
||||||
|
return False
|
||||||
|
bundle_record = dict(bundle or {})
|
||||||
|
bundle_payload = dict(bundle_record.get("bundle") or bundle_record)
|
||||||
|
candidate = str(bundle_record.get("trust_fingerprint", "") or "").strip().lower()
|
||||||
|
if not candidate:
|
||||||
|
candidate = str(
|
||||||
|
trust_fingerprint_for_bundle_record(
|
||||||
|
{
|
||||||
|
"agent_id": str(peer_id or "").strip(),
|
||||||
|
"bundle": bundle_payload,
|
||||||
|
"public_key": str(bundle_record.get("public_key", "") or ""),
|
||||||
|
"public_key_algo": str(bundle_record.get("public_key_algo", "") or "Ed25519"),
|
||||||
|
"protocol_version": str(bundle_record.get("protocol_version", "") or ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
or ""
|
||||||
|
).strip().lower()
|
||||||
|
return bool(candidate and pinned == candidate)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def compose_wormhole_dm(
|
def compose_wormhole_dm(
|
||||||
*,
|
*,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
@@ -9727,8 +9780,11 @@ def compose_wormhole_dm(
|
|||||||
bundle = fetched_bundle
|
bundle = fetched_bundle
|
||||||
if bundle and str(peer_id or "").strip():
|
if bundle and str(peer_id or "").strip():
|
||||||
try:
|
try:
|
||||||
trust_state = observe_remote_prekey_bundle(str(peer_id or "").strip(), bundle)
|
if _compose_bundle_matches_invite_pin(str(peer_id or "").strip(), bundle):
|
||||||
_compose_trust_level = str(trust_state.get("trust_level", "") or "")
|
_compose_trust_level = "invite_pinned"
|
||||||
|
else:
|
||||||
|
trust_state = observe_remote_prekey_bundle(str(peer_id or "").strip(), bundle)
|
||||||
|
_compose_trust_level = str(trust_state.get("trust_level", "") or "")
|
||||||
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
|
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
|
||||||
|
|
||||||
verified_first_contact = verified_first_contact_requirement(
|
verified_first_contact = verified_first_contact_requirement(
|
||||||
@@ -9909,21 +9965,11 @@ def decrypt_wormhole_dm_envelope(
|
|||||||
if not has_session.get("ok"):
|
if not has_session.get("ok"):
|
||||||
return has_session
|
return has_session
|
||||||
if not has_session.get("exists"):
|
if not has_session.get("exists"):
|
||||||
local_dh_secret = ""
|
|
||||||
local_identity_alias = ""
|
|
||||||
try:
|
|
||||||
local_identity = read_wormhole_identity()
|
|
||||||
local_dh_secret = str(local_identity.get("dh_private_key", "") or "")
|
|
||||||
local_identity_alias = str(local_identity.get("node_id", "") or "")
|
|
||||||
except Exception:
|
|
||||||
local_dh_secret = ""
|
|
||||||
local_identity_alias = ""
|
|
||||||
ensured = ensure_mls_dm_session(
|
ensured = ensure_mls_dm_session(
|
||||||
resolved_local,
|
resolved_local,
|
||||||
resolved_remote,
|
resolved_remote,
|
||||||
str(session_welcome or ""),
|
str(session_welcome or ""),
|
||||||
local_dh_secret=local_dh_secret,
|
identity_alias=resolved_local,
|
||||||
identity_alias=local_identity_alias,
|
|
||||||
)
|
)
|
||||||
if not ensured.get("ok"):
|
if not ensured.get("ok"):
|
||||||
return ensured
|
return ensured
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ def _hydrate_dm_relay_from_chain(events: list) -> int:
|
|||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def infonet_peer_push(request: Request):
|
async def infonet_peer_push(request: Request):
|
||||||
"""Accept pushed Infonet events from relay peers (HMAC-authenticated)."""
|
"""Accept pushed Infonet events from relay peers (HMAC-authenticated)."""
|
||||||
|
from services.mesh.mesh_fleet_defaults import infonet_fleet_join_enabled
|
||||||
|
|
||||||
|
if not infonet_fleet_join_enabled():
|
||||||
|
return {"ok": True, "accepted": 0, "duplicates": 0, "rejected": [], "skipped": "fleet_join_disabled"}
|
||||||
content_length = request.headers.get("content-length")
|
content_length = request.headers.get("content-length")
|
||||||
if content_length:
|
if content_length:
|
||||||
try:
|
try:
|
||||||
@@ -154,6 +158,10 @@ async def dm_replicate_envelope(request: Request):
|
|||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def gate_peer_push(request: Request):
|
async def gate_peer_push(request: Request):
|
||||||
"""Accept pushed gate events from relay peers (private plane)."""
|
"""Accept pushed gate events from relay peers (private plane)."""
|
||||||
|
from services.mesh.mesh_fleet_defaults import infonet_fleet_join_enabled
|
||||||
|
|
||||||
|
if not infonet_fleet_join_enabled():
|
||||||
|
return {"ok": True, "accepted": 0, "duplicates": 0, "skipped": "fleet_join_disabled"}
|
||||||
content_length = request.headers.get("content-length")
|
content_length = request.headers.get("content-length")
|
||||||
if content_length:
|
if content_length:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -308,6 +308,10 @@ class WormholeDmDecryptRequest(BaseModel):
|
|||||||
session_welcome: str | None = None
|
session_welcome: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WormholeDmMlsKeyPackageRequest(BaseModel):
|
||||||
|
alias: str
|
||||||
|
|
||||||
|
|
||||||
class WormholeDmResetRequest(BaseModel):
|
class WormholeDmResetRequest(BaseModel):
|
||||||
peer_id: str | None = None
|
peer_id: str | None = None
|
||||||
|
|
||||||
@@ -1228,6 +1232,23 @@ async def api_wormhole_dm_decrypt(request: Request, body: WormholeDmDecryptReque
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/wormhole/dm/mls-key-package", dependencies=[Depends(require_admin)])
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
async def api_wormhole_dm_mls_key_package(request: Request, body: WormholeDmMlsKeyPackageRequest):
|
||||||
|
from services.mesh.mesh_dm_mls import export_dm_key_package_for_alias
|
||||||
|
|
||||||
|
return export_dm_key_package_for_alias(str(body.alias or "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/wormhole/dm/mls-reset", dependencies=[Depends(require_admin)])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def api_wormhole_dm_mls_reset(request: Request):
|
||||||
|
from services.mesh.mesh_dm_mls import reset_dm_mls_state
|
||||||
|
|
||||||
|
reset_dm_mls_state(clear_privacy_core=True, clear_persistence=True)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/wormhole/dm/reset", dependencies=[Depends(require_admin)])
|
@router.post("/api/wormhole/dm/reset", dependencies=[Depends(require_admin)])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def api_wormhole_dm_reset(request: Request, body: WormholeDmResetRequest):
|
async def api_wormhole_dm_reset(request: Request, body: WormholeDmResetRequest):
|
||||||
@@ -1326,6 +1347,25 @@ async def api_wormhole_status(request: Request):
|
|||||||
return await _m.api_wormhole_status(request)
|
return await _m.api_wormhole_status(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/api/wormhole/private-delivery/{item_id}",
|
||||||
|
dependencies=[Depends(require_local_operator)],
|
||||||
|
)
|
||||||
|
@limiter.limit("120/minute")
|
||||||
|
async def api_wormhole_private_delivery_item(request: Request, item_id: str):
|
||||||
|
from services.mesh.mesh_metadata_exposure import metadata_exposure_for_request
|
||||||
|
from services.mesh.mesh_private_outbox import private_delivery_outbox
|
||||||
|
|
||||||
|
exposure = metadata_exposure_for_request(
|
||||||
|
request,
|
||||||
|
authenticated=True,
|
||||||
|
)
|
||||||
|
item = private_delivery_outbox.get_item(item_id, exposure=exposure)
|
||||||
|
if item is None:
|
||||||
|
raise HTTPException(status_code=404, detail="private_delivery_item_not_found")
|
||||||
|
return {"ok": True, "item": item}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/wormhole/private-delivery/{item_id}/action", dependencies=[Depends(require_local_operator)])
|
@router.post("/api/wormhole/private-delivery/{item_id}/action", dependencies=[Depends(require_local_operator)])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def api_wormhole_private_delivery_action(
|
async def api_wormhole_private_delivery_action(
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class Settings(BaseSettings):
|
|||||||
MESH_MQTT_INCLUDE_DEFAULT_ROOTS: bool = True
|
MESH_MQTT_INCLUDE_DEFAULT_ROOTS: bool = True
|
||||||
MESH_RNS_ENABLED: bool = False
|
MESH_RNS_ENABLED: bool = False
|
||||||
MESH_ARTI_ENABLED: bool = False
|
MESH_ARTI_ENABLED: bool = False
|
||||||
|
# When true, trust wormhole_status.json ready bit if the child process is
|
||||||
|
# alive — avoids transport-tier flapping when /api/health probes time out
|
||||||
|
# under Tor load (common during live DM E2E).
|
||||||
|
MESH_WORMHOLE_TRUST_FILE_READY: bool = False
|
||||||
MESH_ARTI_SOCKS_PORT: int = 9050
|
MESH_ARTI_SOCKS_PORT: int = 9050
|
||||||
MESH_RELAY_PEERS: str = ""
|
MESH_RELAY_PEERS: str = ""
|
||||||
MESH_PUBLIC_PEER_URL: str = ""
|
MESH_PUBLIC_PEER_URL: str = ""
|
||||||
|
|||||||
@@ -1574,47 +1574,214 @@ class DMRelay:
|
|||||||
}
|
}
|
||||||
if not msg_id:
|
if not msg_id:
|
||||||
msg_id = f"dm_{int(time.time() * 1000)}_{secrets.token_hex(6)}"
|
msg_id = f"dm_{int(time.time() * 1000)}_{secrets.token_hex(6)}"
|
||||||
elif any(m.msg_id == msg_id for m in self._mailboxes[mailbox_key]):
|
duplicate_hit = any(m.msg_id == msg_id for m in self._mailboxes[mailbox_key])
|
||||||
return {"ok": True, "msg_id": msg_id}
|
if not duplicate_hit:
|
||||||
relay_sender_id = (
|
relay_sender_id = (
|
||||||
f"sender_token:{sender_token_hash}"
|
f"sender_token:{sender_token_hash}"
|
||||||
if sender_token_hash
|
if sender_token_hash
|
||||||
else sender_id
|
else sender_id
|
||||||
)
|
|
||||||
self._mailboxes[mailbox_key].append(
|
|
||||||
DMMessage(
|
|
||||||
sender_id=relay_sender_id,
|
|
||||||
ciphertext=ciphertext,
|
|
||||||
timestamp=time.time(),
|
|
||||||
msg_id=msg_id,
|
|
||||||
delivery_class=delivery_class,
|
|
||||||
sender_seal=sender_seal,
|
|
||||||
sender_block_ref=sender_block_ref,
|
|
||||||
payload_format=str(payload_format or "dm1"),
|
|
||||||
session_welcome=str(session_welcome or ""),
|
|
||||||
)
|
)
|
||||||
)
|
self._mailboxes[mailbox_key].append(
|
||||||
self._stats["messages_in_memory"] = sum(len(v) for v in self._mailboxes.values())
|
DMMessage(
|
||||||
self._save()
|
sender_id=relay_sender_id,
|
||||||
# Cross-node mailbox replication: push the freshly-stored
|
ciphertext=ciphertext,
|
||||||
# envelope to every authenticated relay peer so the recipient
|
timestamp=time.time(),
|
||||||
# can log into ANY node and find their messages. The push is
|
msg_id=msg_id,
|
||||||
# async (fire-and-forget thread) so deposit() returns
|
delivery_class=delivery_class,
|
||||||
# immediately — slow Tor peers can't block the sender's UX.
|
sender_seal=sender_seal,
|
||||||
# Each receiving peer re-enforces the per-sender cap on
|
sender_block_ref=sender_block_ref,
|
||||||
# acceptance, so hostile relays can't widen the cap.
|
payload_format=str(payload_format or "dm1"),
|
||||||
|
session_welcome=str(session_welcome or ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._stats["messages_in_memory"] = sum(len(v) for v in self._mailboxes.values())
|
||||||
|
self._save()
|
||||||
|
preferred_urls = list(replication_peer_urls or [])
|
||||||
|
envelope_for_push: dict[str, Any] | None = None
|
||||||
try:
|
try:
|
||||||
envelope_for_push = self.envelope_for_replication(
|
envelope_for_push = self.envelope_for_replication(
|
||||||
mailbox_key=mailbox_key, msg_id=msg_id,
|
mailbox_key=mailbox_key,
|
||||||
|
msg_id=msg_id,
|
||||||
|
recipient_id=recipient_id,
|
||||||
|
recipient_token=recipient_token,
|
||||||
)
|
)
|
||||||
if envelope_for_push:
|
|
||||||
self._replicate_envelope_to_peers_async(
|
|
||||||
envelope=envelope_for_push,
|
|
||||||
preferred_peer_urls=list(replication_peer_urls or []),
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
metrics_inc("dm_replication_push_error")
|
metrics_inc("dm_replication_push_error")
|
||||||
return {"ok": True, "msg_id": msg_id}
|
deposit_result = {"ok": True, "msg_id": msg_id}
|
||||||
|
if duplicate_hit:
|
||||||
|
deposit_result["duplicate"] = True
|
||||||
|
|
||||||
|
if envelope_for_push:
|
||||||
|
# Invite-scoped connect traffic names an explicit recipient relay
|
||||||
|
# (lookup_peer_url). Block until that push completes so the
|
||||||
|
# recipient can poll their own node; fleet-wide fan-out stays
|
||||||
|
# async so dead manifest peers cannot wedge deposit().
|
||||||
|
if preferred_urls:
|
||||||
|
logger.info(
|
||||||
|
"DM deposit awaiting scoped replicate to %d peer(s)",
|
||||||
|
len(preferred_urls),
|
||||||
|
)
|
||||||
|
deposit_result["replicate"] = self._replicate_envelope_to_peers(
|
||||||
|
envelope=envelope_for_push,
|
||||||
|
preferred_peer_urls=preferred_urls,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._replicate_envelope_to_peers_async(
|
||||||
|
envelope=envelope_for_push,
|
||||||
|
preferred_peer_urls=[],
|
||||||
|
)
|
||||||
|
elif preferred_urls:
|
||||||
|
logger.warning(
|
||||||
|
"DM deposit skipped scoped replicate: envelope missing for msg_id=%s",
|
||||||
|
msg_id,
|
||||||
|
)
|
||||||
|
return deposit_result
|
||||||
|
|
||||||
|
def _replicate_envelope_to_peers(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
envelope: dict[str, Any],
|
||||||
|
preferred_peer_urls: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Push an envelope to relay peers. Returns per-peer results."""
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import requests as _requests
|
||||||
|
|
||||||
|
from services.mesh.mesh_crypto import (
|
||||||
|
normalize_peer_url,
|
||||||
|
resolve_peer_key_for_url,
|
||||||
|
)
|
||||||
|
from services.mesh.mesh_router import authenticated_push_peer_urls
|
||||||
|
|
||||||
|
peers: list[str] = []
|
||||||
|
for raw_url in list(preferred_peer_urls or []):
|
||||||
|
normalized_preferred = normalize_peer_url(str(raw_url or "").strip())
|
||||||
|
if normalized_preferred and normalized_preferred not in peers:
|
||||||
|
peers.append(normalized_preferred)
|
||||||
|
if not peers:
|
||||||
|
for peer_url in authenticated_push_peer_urls():
|
||||||
|
normalized_peer = normalize_peer_url(str(peer_url or "").strip())
|
||||||
|
if normalized_peer and normalized_peer not in peers:
|
||||||
|
peers.append(normalized_peer)
|
||||||
|
if not peers:
|
||||||
|
return {"ok": False, "detail": "no_relay_peers", "pushed": [], "failed": []}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"DM replicate push starting for %d peer(s): %s",
|
||||||
|
len(peers),
|
||||||
|
", ".join(peers[:3]) + ("..." if len(peers) > 3 else ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = json.dumps(
|
||||||
|
{"envelope": envelope},
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
base_timeout = max(
|
||||||
|
1,
|
||||||
|
int(getattr(self._settings(), "MESH_RELAY_PUSH_TIMEOUT_S", 10) or 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
from main import _infonet_peer_requests_proxies
|
||||||
|
|
||||||
|
preferred_set = {
|
||||||
|
normalize_peer_url(str(raw_url or "").strip())
|
||||||
|
for raw_url in list(preferred_peer_urls or [])
|
||||||
|
}
|
||||||
|
preferred_set.discard("")
|
||||||
|
|
||||||
|
pushed: list[str] = []
|
||||||
|
failed: list[dict[str, str]] = []
|
||||||
|
for peer_url in peers:
|
||||||
|
try:
|
||||||
|
normalized = normalize_peer_url(peer_url)
|
||||||
|
timeout = max(180 if ".onion" in normalized else 1, base_timeout)
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
peer_key = resolve_peer_key_for_url(normalized)
|
||||||
|
if peer_key:
|
||||||
|
headers["X-Peer-Url"] = normalized
|
||||||
|
headers["X-Peer-HMAC"] = hmac.new(
|
||||||
|
peer_key, payload, hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
url = f"{peer_url}/api/mesh/dm/replicate-envelope"
|
||||||
|
request_kwargs: dict[str, Any] = {
|
||||||
|
"data": payload,
|
||||||
|
"timeout": timeout,
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
proxies = _infonet_peer_requests_proxies(normalized)
|
||||||
|
if proxies:
|
||||||
|
request_kwargs["proxies"] = proxies
|
||||||
|
resp = None
|
||||||
|
max_attempts = 3 if normalized in preferred_set else 2
|
||||||
|
last_exc = ""
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
resp = _requests.post(url, **request_kwargs)
|
||||||
|
break
|
||||||
|
except Exception as exc:
|
||||||
|
last_exc = str(exc) or type(exc).__name__
|
||||||
|
if attempt + 1 < max_attempts:
|
||||||
|
time.sleep(5.0 * (attempt + 1))
|
||||||
|
continue
|
||||||
|
logger.warning(
|
||||||
|
"DM replicate push to %s failed: %s",
|
||||||
|
peer_url,
|
||||||
|
last_exc,
|
||||||
|
)
|
||||||
|
metrics_inc("dm_replication_push_error")
|
||||||
|
resp = None
|
||||||
|
break
|
||||||
|
if resp is None:
|
||||||
|
failed.append({"url": peer_url, "detail": last_exc or "request_failed"})
|
||||||
|
continue
|
||||||
|
if resp.status_code == 200:
|
||||||
|
body_ok = True
|
||||||
|
detail = ""
|
||||||
|
try:
|
||||||
|
body = resp.json()
|
||||||
|
if isinstance(body, dict) and body.get("ok") is False:
|
||||||
|
body_ok = False
|
||||||
|
detail = str(body.get("detail", "") or "replicate rejected")[:200]
|
||||||
|
except Exception:
|
||||||
|
body_ok = True
|
||||||
|
if body_ok:
|
||||||
|
logger.info("DM replicate push to %s succeeded", peer_url)
|
||||||
|
metrics_inc("dm_replication_push_ok")
|
||||||
|
pushed.append(peer_url)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"DM replicate push to %s rejected: %s",
|
||||||
|
peer_url,
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
metrics_inc("dm_replication_push_rejected")
|
||||||
|
failed.append({"url": peer_url, "detail": detail or "replicate_rejected"})
|
||||||
|
else:
|
||||||
|
detail = (resp.text or "")[:200]
|
||||||
|
logger.warning(
|
||||||
|
"DM replicate push to %s -> %s: %s",
|
||||||
|
peer_url,
|
||||||
|
resp.status_code,
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
metrics_inc("dm_replication_push_rejected")
|
||||||
|
failed.append({"url": peer_url, "detail": f"http_{resp.status_code}: {detail}"})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("DM replicate push outer failure for %s: %s", peer_url, exc)
|
||||||
|
metrics_inc("dm_replication_push_error")
|
||||||
|
failed.append({"url": peer_url, "detail": str(exc) or type(exc).__name__})
|
||||||
|
|
||||||
|
scoped = bool(preferred_set)
|
||||||
|
ok = bool(pushed) if scoped else bool(pushed) or not failed
|
||||||
|
return {
|
||||||
|
"ok": ok,
|
||||||
|
"scoped": scoped,
|
||||||
|
"pushed": pushed,
|
||||||
|
"failed": failed,
|
||||||
|
}
|
||||||
|
|
||||||
def accept_replica(
|
def accept_replica(
|
||||||
self,
|
self,
|
||||||
@@ -1647,6 +1814,33 @@ class DMRelay:
|
|||||||
mailbox_key = str(envelope.get("mailbox_key", "") or "").strip()
|
mailbox_key = str(envelope.get("mailbox_key", "") or "").strip()
|
||||||
sender_block_ref = str(envelope.get("sender_block_ref", "") or "").strip()
|
sender_block_ref = str(envelope.get("sender_block_ref", "") or "").strip()
|
||||||
ciphertext = str(envelope.get("ciphertext", "") or "")
|
ciphertext = str(envelope.get("ciphertext", "") or "")
|
||||||
|
delivery_class = str(envelope.get("delivery_class", "") or "").strip().lower()
|
||||||
|
recipient_id = str(envelope.get("recipient_id", "") or "").strip()
|
||||||
|
recipient_token = str(envelope.get("recipient_token", "") or "").strip()
|
||||||
|
if delivery_class not in ("request", "shared", "self"):
|
||||||
|
if recipient_id and not recipient_token:
|
||||||
|
delivery_class = "request"
|
||||||
|
elif recipient_token:
|
||||||
|
delivery_class = "shared"
|
||||||
|
if delivery_class == "request":
|
||||||
|
if not recipient_id:
|
||||||
|
try:
|
||||||
|
from services.mesh.mesh_wormhole_persona import get_dm_identity
|
||||||
|
|
||||||
|
recipient_id = str((get_dm_identity() or {}).get("node_id") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
recipient_id = ""
|
||||||
|
if recipient_id:
|
||||||
|
mailbox_key = self.mailbox_key_for_delivery(
|
||||||
|
recipient_id=recipient_id,
|
||||||
|
delivery_class="request",
|
||||||
|
)
|
||||||
|
elif delivery_class == "shared" and recipient_token:
|
||||||
|
mailbox_key = self.mailbox_key_for_delivery(
|
||||||
|
recipient_id=recipient_id,
|
||||||
|
delivery_class="shared",
|
||||||
|
recipient_token=recipient_token,
|
||||||
|
)
|
||||||
if not msg_id or not mailbox_key or not sender_block_ref or not ciphertext:
|
if not msg_id or not mailbox_key or not sender_block_ref or not ciphertext:
|
||||||
return {"ok": False, "detail": "envelope missing required fields"}
|
return {"ok": False, "detail": "envelope missing required fields"}
|
||||||
|
|
||||||
@@ -1664,7 +1858,6 @@ class DMRelay:
|
|||||||
# Same per-class cap as the deposit path — defense in depth
|
# Same per-class cap as the deposit path — defense in depth
|
||||||
# against a peer that wraps a "deposit" as a "replica" to
|
# against a peer that wraps a "deposit" as a "replica" to
|
||||||
# bypass the class limit.
|
# bypass the class limit.
|
||||||
delivery_class = str(envelope.get("delivery_class", "") or "")
|
|
||||||
if delivery_class in ("request", "shared", "self"):
|
if delivery_class in ("request", "shared", "self"):
|
||||||
class_limit = self._mailbox_limit_for_class(delivery_class)
|
class_limit = self._mailbox_limit_for_class(delivery_class)
|
||||||
else:
|
else:
|
||||||
@@ -1720,89 +1913,16 @@ class DMRelay:
|
|||||||
envelope: dict[str, Any],
|
envelope: dict[str, Any],
|
||||||
preferred_peer_urls: list[str] | None = None,
|
preferred_peer_urls: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Push an outbound DM envelope to every authenticated relay peer.
|
"""Fire-and-forget fleet-wide replicate push (non-scoped traffic)."""
|
||||||
|
|
||||||
Fire-and-forget: spawned in a background thread so ``deposit``
|
|
||||||
returns to the caller immediately. Per-peer errors are logged
|
|
||||||
and swallowed — the sender's UX must not block on slow Tor
|
|
||||||
peers, and a peer that's down today gets the next message
|
|
||||||
whenever it comes back. Inbound recipient polling from a healthy
|
|
||||||
peer keeps the system functional during peer failures.
|
|
||||||
|
|
||||||
Each peer is authed with the existing per-peer HMAC pattern
|
|
||||||
(#256) — same headers and key resolver gate-message replication
|
|
||||||
uses, so a hostile node that doesn't know any peer's HMAC key
|
|
||||||
can't impersonate a legitimate relay.
|
|
||||||
"""
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
def _do_push():
|
def _do_push() -> None:
|
||||||
try:
|
try:
|
||||||
import hashlib
|
self._replicate_envelope_to_peers(
|
||||||
import hmac
|
envelope=envelope,
|
||||||
import requests as _requests
|
preferred_peer_urls=preferred_peer_urls,
|
||||||
|
|
||||||
from services.mesh.mesh_crypto import (
|
|
||||||
normalize_peer_url,
|
|
||||||
resolve_peer_key_for_url,
|
|
||||||
)
|
)
|
||||||
from services.mesh.mesh_router import (
|
|
||||||
authenticated_push_peer_urls,
|
|
||||||
)
|
|
||||||
|
|
||||||
peers: list[str] = []
|
|
||||||
for raw_url in list(preferred_peer_urls or []):
|
|
||||||
normalized_preferred = normalize_peer_url(str(raw_url or "").strip())
|
|
||||||
if normalized_preferred and normalized_preferred not in peers:
|
|
||||||
peers.append(normalized_preferred)
|
|
||||||
for peer_url in authenticated_push_peer_urls():
|
|
||||||
normalized_peer = normalize_peer_url(str(peer_url or "").strip())
|
|
||||||
if normalized_peer and normalized_peer not in peers:
|
|
||||||
peers.append(normalized_peer)
|
|
||||||
if not peers:
|
|
||||||
return
|
|
||||||
|
|
||||||
payload = json.dumps(
|
|
||||||
{"envelope": envelope},
|
|
||||||
separators=(",", ":"),
|
|
||||||
ensure_ascii=False,
|
|
||||||
).encode("utf-8")
|
|
||||||
|
|
||||||
timeout = max(
|
|
||||||
1,
|
|
||||||
int(getattr(self._settings(), "MESH_RELAY_PUSH_TIMEOUT_S", 10) or 10),
|
|
||||||
)
|
|
||||||
|
|
||||||
for peer_url in peers:
|
|
||||||
try:
|
|
||||||
normalized = normalize_peer_url(peer_url)
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
peer_key = resolve_peer_key_for_url(normalized)
|
|
||||||
if peer_key:
|
|
||||||
headers["X-Peer-Url"] = normalized
|
|
||||||
headers["X-Peer-HMAC"] = hmac.new(
|
|
||||||
peer_key, payload, hashlib.sha256
|
|
||||||
).hexdigest()
|
|
||||||
url = f"{peer_url}/api/mesh/dm/replicate-envelope"
|
|
||||||
resp = _requests.post(
|
|
||||||
url, data=payload, timeout=timeout, headers=headers,
|
|
||||||
)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
metrics_inc("dm_replication_push_ok")
|
|
||||||
else:
|
|
||||||
# 4xx including the structured cap_violation
|
|
||||||
# rejection from accept_replica — sender's
|
|
||||||
# relay learns and stops retrying this msg_id.
|
|
||||||
metrics_inc("dm_replication_push_rejected")
|
|
||||||
except Exception:
|
|
||||||
# Per-peer failure is non-fatal — log to metrics
|
|
||||||
# but don't break the loop. Other peers and a
|
|
||||||
# future retry can still propagate the envelope.
|
|
||||||
metrics_inc("dm_replication_push_error")
|
|
||||||
continue
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Outer guard — never let replication errors propagate
|
|
||||||
# back to the sender's deposit() caller.
|
|
||||||
metrics_inc("dm_replication_push_error")
|
metrics_inc("dm_replication_push_error")
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -1817,6 +1937,8 @@ class DMRelay:
|
|||||||
*,
|
*,
|
||||||
mailbox_key: str,
|
mailbox_key: str,
|
||||||
msg_id: str,
|
msg_id: str,
|
||||||
|
recipient_id: str = "",
|
||||||
|
recipient_token: str | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Return the wire-form envelope for a stored message, suitable
|
"""Return the wire-form envelope for a stored message, suitable
|
||||||
for POSTing to a peer relay's replicate-envelope endpoint.
|
for POSTing to a peer relay's replicate-envelope endpoint.
|
||||||
@@ -1833,6 +1955,8 @@ class DMRelay:
|
|||||||
return {
|
return {
|
||||||
"msg_id": m.msg_id,
|
"msg_id": m.msg_id,
|
||||||
"mailbox_key": mailbox_key,
|
"mailbox_key": mailbox_key,
|
||||||
|
"recipient_id": str(recipient_id or "").strip(),
|
||||||
|
"recipient_token": str(recipient_token or "").strip(),
|
||||||
"sender_id": m.sender_id,
|
"sender_id": m.sender_id,
|
||||||
"sender_block_ref": m.sender_block_ref,
|
"sender_block_ref": m.sender_block_ref,
|
||||||
"sender_seal": m.sender_seal,
|
"sender_seal": m.sender_seal,
|
||||||
|
|||||||
@@ -140,10 +140,24 @@ def transport_tier_from_state(state: dict[str, Any] | None) -> str:
|
|||||||
snapshot = state or {}
|
snapshot = state or {}
|
||||||
if not bool(snapshot.get("configured")):
|
if not bool(snapshot.get("configured")):
|
||||||
return "public_degraded"
|
return "public_degraded"
|
||||||
if not bool(snapshot.get("ready")):
|
|
||||||
return "public_degraded"
|
|
||||||
arti_ready = bool(snapshot.get("arti_ready"))
|
arti_ready = bool(snapshot.get("arti_ready"))
|
||||||
rns_ready = bool(snapshot.get("rns_ready"))
|
rns_ready = bool(snapshot.get("rns_ready"))
|
||||||
|
running = bool(snapshot.get("running"))
|
||||||
|
transport_usable = bool(snapshot.get("ready"))
|
||||||
|
if not transport_usable:
|
||||||
|
try:
|
||||||
|
from services.config import get_settings
|
||||||
|
|
||||||
|
if (
|
||||||
|
bool(getattr(get_settings(), "MESH_WORMHOLE_TRUST_FILE_READY", False))
|
||||||
|
and running
|
||||||
|
and arti_ready
|
||||||
|
):
|
||||||
|
transport_usable = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not transport_usable:
|
||||||
|
return "public_degraded"
|
||||||
if arti_ready and rns_ready:
|
if arti_ready and rns_ready:
|
||||||
return "private_strong"
|
return "private_strong"
|
||||||
if arti_ready or rns_ready:
|
if arti_ready or rns_ready:
|
||||||
|
|||||||
@@ -390,7 +390,13 @@ def _dispatch_dm(
|
|||||||
try:
|
try:
|
||||||
from services.mesh.mesh_dm_connect_delivery import relay_push_peer_urls_for_payload
|
from services.mesh.mesh_dm_connect_delivery import relay_push_peer_urls_for_payload
|
||||||
|
|
||||||
replication_peer_urls = relay_push_peer_urls_for_payload(payload)
|
replication_peer_urls = [
|
||||||
|
str(raw or "").strip().rstrip("/")
|
||||||
|
for raw in list(payload.get("relay_push_peer_urls") or [])
|
||||||
|
if str(raw or "").strip()
|
||||||
|
]
|
||||||
|
if not replication_peer_urls:
|
||||||
|
replication_peer_urls = relay_push_peer_urls_for_payload(payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
replication_peer_urls = []
|
replication_peer_urls = []
|
||||||
|
|
||||||
@@ -409,6 +415,23 @@ def _dispatch_dm(
|
|||||||
session_welcome=session_welcome,
|
session_welcome=session_welcome,
|
||||||
replication_peer_urls=replication_peer_urls,
|
replication_peer_urls=replication_peer_urls,
|
||||||
)
|
)
|
||||||
|
replicate_info = dict(relay_result.get("replicate") or {})
|
||||||
|
if replication_peer_urls and not replicate_info.get("ok"):
|
||||||
|
return _dispatch_result(
|
||||||
|
ok=False,
|
||||||
|
lane="dm",
|
||||||
|
selected_transport="relay",
|
||||||
|
selected_carrier="relay",
|
||||||
|
dispatch_reason="scoped_relay_replicate_failed",
|
||||||
|
hidden_transport_effective=bool(hidden_relay),
|
||||||
|
no_acceptable_path=False,
|
||||||
|
detail=(
|
||||||
|
"Scoped relay replicate did not reach the recipient node: "
|
||||||
|
+ str(replicate_info.get("failed") or replicate_info.get("detail") or "unknown")
|
||||||
|
),
|
||||||
|
msg_id=msg_id,
|
||||||
|
replicate=replicate_info,
|
||||||
|
)
|
||||||
if not relay_result.get("ok"):
|
if not relay_result.get("ok"):
|
||||||
return _dispatch_result(
|
return _dispatch_result(
|
||||||
ok=False,
|
ok=False,
|
||||||
@@ -445,6 +468,7 @@ def _dispatch_dm(
|
|||||||
else str(relay_result.get("detail", "") or "Delivered privately")
|
else str(relay_result.get("detail", "") or "Delivered privately")
|
||||||
),
|
),
|
||||||
msg_id=str(relay_result.get("msg_id", "") or msg_id),
|
msg_id=str(relay_result.get("msg_id", "") or msg_id),
|
||||||
|
replicate=replicate_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -463,8 +463,26 @@ def _apply_content_private_transport_lock_policy(prepared: "PreparedSignedWrite"
|
|||||||
except Exception:
|
except Exception:
|
||||||
current_tier = "public_degraded"
|
current_tier = "public_degraded"
|
||||||
|
|
||||||
|
lock_to_satisfy = normalized
|
||||||
|
if prepared.kind in {
|
||||||
|
SignedWriteKind.DM_POLL,
|
||||||
|
SignedWriteKind.DM_COUNT,
|
||||||
|
SignedWriteKind.DM_SEND,
|
||||||
|
SignedWriteKind.DM_REGISTER,
|
||||||
|
SignedWriteKind.DM_BLOCK,
|
||||||
|
SignedWriteKind.DM_WITNESS,
|
||||||
|
}:
|
||||||
|
from services.mesh.mesh_privacy_policy import release_lane_required_tier
|
||||||
|
|
||||||
|
lane_cap = release_lane_required_tier("dm")
|
||||||
|
# Clients sign private_strong; Tor-only nodes cap DM at
|
||||||
|
# private_transitional. Accept when live transport meets the
|
||||||
|
# strongest tier this node can offer on the DM lane.
|
||||||
|
if not transport_tier_is_sufficient(lane_cap, normalized):
|
||||||
|
lock_to_satisfy = lane_cap
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not transport_tier_is_sufficient(current_tier, normalized)
|
not transport_tier_is_sufficient(current_tier, lock_to_satisfy)
|
||||||
and prepared.kind not in _QUEUEABLE_CONTENT_PRIVATE_KINDS
|
and prepared.kind not in _QUEUEABLE_CONTENT_PRIVATE_KINDS
|
||||||
):
|
):
|
||||||
metrics_inc("signed_write_transport_lock_tier_mismatch")
|
metrics_inc("signed_write_transport_lock_tier_mismatch")
|
||||||
|
|||||||
@@ -424,6 +424,7 @@ def _submit_signed_dm_send(
|
|||||||
session_welcome: str = "",
|
session_welcome: str = "",
|
||||||
connect_intent: str = "",
|
connect_intent: str = "",
|
||||||
lookup_peer_url: str = "",
|
lookup_peer_url: str = "",
|
||||||
|
peer_dh_pub: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
import main as main_mod
|
import main as main_mod
|
||||||
from services.mesh.mesh_protocol import (
|
from services.mesh.mesh_protocol import (
|
||||||
@@ -455,6 +456,26 @@ def _submit_signed_dm_send(
|
|||||||
if session_welcome:
|
if session_welcome:
|
||||||
dm_payload["session_welcome"] = str(session_welcome)
|
dm_payload["session_welcome"] = str(session_welcome)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from services.config import get_settings
|
||||||
|
from services.mesh.mesh_wormhole_seal import build_sender_seal
|
||||||
|
|
||||||
|
if (
|
||||||
|
delivery == "shared"
|
||||||
|
and bool(get_settings().MESH_DM_REQUIRE_SENDER_SEAL_SHARED)
|
||||||
|
and not str(dm_payload.get("sender_seal", "") or "").strip()
|
||||||
|
):
|
||||||
|
seal = build_sender_seal(
|
||||||
|
recipient_id=recipient,
|
||||||
|
recipient_dh_pub=str(peer_dh_pub or ""),
|
||||||
|
msg_id=msg_id,
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
if seal.get("ok"):
|
||||||
|
dm_payload["sender_seal"] = str(seal.get("sender_seal") or "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
ok_payload, reason = validate_event_payload("dm_message", dm_payload)
|
ok_payload, reason = validate_event_payload("dm_message", dm_payload)
|
||||||
if not ok_payload:
|
if not ok_payload:
|
||||||
return {"ok": False, "detail": reason}
|
return {"ok": False, "detail": reason}
|
||||||
@@ -490,6 +511,7 @@ def _submit_signed_dm_send(
|
|||||||
"session_welcome": str(session_welcome or ""),
|
"session_welcome": str(session_welcome or ""),
|
||||||
"msg_id": msg_id,
|
"msg_id": msg_id,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
"sender_seal": str(dm_payload.get("sender_seal") or ""),
|
||||||
"public_key": str(signed.get("public_key") or ""),
|
"public_key": str(signed.get("public_key") or ""),
|
||||||
"public_key_algo": str(signed.get("public_key_algo") or ""),
|
"public_key_algo": str(signed.get("public_key_algo") or ""),
|
||||||
"signature": str(signed.get("signature") or ""),
|
"signature": str(signed.get("signature") or ""),
|
||||||
@@ -618,6 +640,8 @@ def send_contact_accept(
|
|||||||
*,
|
*,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
peer_dh_pub: str = "",
|
peer_dh_pub: str = "",
|
||||||
|
lookup_token: str = "",
|
||||||
|
lookup_peer_url: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Accept a pending contact request and open the shared DM lane."""
|
"""Accept a pending contact request and open the shared DM lane."""
|
||||||
from services.mesh.mesh_wormhole_dead_drop import build_contact_accept, issue_pairwise_dm_alias
|
from services.mesh.mesh_wormhole_dead_drop import build_contact_accept, issue_pairwise_dm_alias
|
||||||
@@ -627,9 +651,15 @@ def send_contact_accept(
|
|||||||
if not peer:
|
if not peer:
|
||||||
return {"ok": False, "detail": "peer_id required"}
|
return {"ok": False, "detail": "peer_id required"}
|
||||||
|
|
||||||
|
token = str(lookup_token or "").strip()
|
||||||
|
preferred_peer = str(lookup_peer_url or "").strip().rstrip("/")
|
||||||
dh_pub = str(peer_dh_pub or "").strip()
|
dh_pub = str(peer_dh_pub or "").strip()
|
||||||
if not dh_pub:
|
if not dh_pub:
|
||||||
bundle = fetch_dm_prekey_bundle(agent_id=peer)
|
bundle = fetch_dm_prekey_bundle(
|
||||||
|
agent_id=peer if not token else "",
|
||||||
|
lookup_token=token,
|
||||||
|
lookup_peer_urls=[preferred_peer] if preferred_peer else None,
|
||||||
|
)
|
||||||
if not bundle.get("ok"):
|
if not bundle.get("ok"):
|
||||||
return bundle
|
return bundle
|
||||||
dh_pub = str(bundle.get("dh_pub_key") or "").strip()
|
dh_pub = str(bundle.get("dh_pub_key") or "").strip()
|
||||||
@@ -644,7 +674,7 @@ def send_contact_accept(
|
|||||||
return {"ok": False, "detail": "shared_alias unavailable"}
|
return {"ok": False, "detail": "shared_alias unavailable"}
|
||||||
|
|
||||||
accept_plain = build_contact_accept(shared_alias=shared_alias)
|
accept_plain = build_contact_accept(shared_alias=shared_alias)
|
||||||
encrypted = bootstrap_encrypt_for_peer(peer, accept_plain)
|
encrypted = bootstrap_encrypt_for_peer(peer, accept_plain, lookup_token=token)
|
||||||
if not encrypted.get("ok"):
|
if not encrypted.get("ok"):
|
||||||
return encrypted
|
return encrypted
|
||||||
|
|
||||||
@@ -655,6 +685,7 @@ def send_contact_accept(
|
|||||||
ciphertext=str(encrypted.get("result") or ""),
|
ciphertext=str(encrypted.get("result") or ""),
|
||||||
payload_format="mls1",
|
payload_format="mls1",
|
||||||
connect_intent="contact_accept",
|
connect_intent="contact_accept",
|
||||||
|
lookup_peer_url=preferred_peer,
|
||||||
)
|
)
|
||||||
if isinstance(sent, dict):
|
if isinstance(sent, dict):
|
||||||
sent.setdefault("shared_alias", shared_alias)
|
sent.setdefault("shared_alias", shared_alias)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ def validate_privacy_core_startup(settings: Any | None = None) -> None:
|
|||||||
|
|
||||||
attestation = privacy_core_attestation(snapshot)
|
attestation = privacy_core_attestation(snapshot)
|
||||||
state = str(attestation.get("attestation_state", "") or "").strip()
|
state = str(attestation.get("attestation_state", "") or "").strip()
|
||||||
if state == "attested_current":
|
if state in {"attested_current", "development_override"}:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.critical(
|
logger.critical(
|
||||||
|
|||||||
@@ -388,11 +388,17 @@ class TorHiddenService:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
from services.config import get_settings
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
socks_port_line = ""
|
||||||
|
if not bool(getattr(settings, "MESH_ARTI_ENABLED", False)):
|
||||||
|
socks_port_line = "SocksPort 9050\n"
|
||||||
torrc_content = (
|
torrc_content = (
|
||||||
f"DataDirectory {TOR_DATA_DIR.as_posix()}\n"
|
f"DataDirectory {TOR_DATA_DIR.as_posix()}\n"
|
||||||
f"HiddenServiceDir {hidden_service_dir.as_posix()}\n"
|
f"HiddenServiceDir {hidden_service_dir.as_posix()}\n"
|
||||||
f"HiddenServicePort {target_port} 127.0.0.1:{target_port}\n"
|
f"HiddenServicePort {target_port} 127.0.0.1:{target_port}\n"
|
||||||
"SocksPort 9050\n"
|
f"{socks_port_line}"
|
||||||
"Log notice stderr\n"
|
"Log notice stderr\n"
|
||||||
)
|
)
|
||||||
TORRC_PATH.write_text(torrc_content, encoding="utf-8")
|
TORRC_PATH.write_text(torrc_content, encoding="utf-8")
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ _WORMHOLE_ENV_EXPLICIT = {
|
|||||||
"CORS_ORIGINS",
|
"CORS_ORIGINS",
|
||||||
"PUBLIC_API_KEY",
|
"PUBLIC_API_KEY",
|
||||||
"PRIVACY_CORE_ALLOWED_SHA256",
|
"PRIVACY_CORE_ALLOWED_SHA256",
|
||||||
|
"PRIVACY_CORE_DEV_OVERRIDE",
|
||||||
"PRIVACY_CORE_LIB",
|
"PRIVACY_CORE_LIB",
|
||||||
"PRIVACY_CORE_MIN_VERSION",
|
"PRIVACY_CORE_MIN_VERSION",
|
||||||
}
|
}
|
||||||
@@ -289,6 +290,23 @@ def _terminate_pid(pid: int, *, timeout_s: float = 5.0) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _trust_wormhole_file_ready(status: dict[str, Any] | None = None) -> bool:
|
||||||
|
try:
|
||||||
|
from services.config import get_settings
|
||||||
|
|
||||||
|
if not bool(getattr(get_settings(), "MESH_WORMHOLE_TRUST_FILE_READY", False)):
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
snapshot = status if status is not None else read_wormhole_status()
|
||||||
|
if not bool(snapshot.get("ready")):
|
||||||
|
return False
|
||||||
|
started_at = int(snapshot.get("started_at", 0) or 0)
|
||||||
|
if started_at <= 0:
|
||||||
|
return False
|
||||||
|
return (time.time() - started_at) < 3600
|
||||||
|
|
||||||
|
|
||||||
def _probe_ready(timeout_s: float = 1.5) -> bool:
|
def _probe_ready(timeout_s: float = 1.5) -> bool:
|
||||||
try:
|
try:
|
||||||
with urlopen(f"http://{WORMHOLE_HOST}:{WORMHOLE_PORT}/api/health", timeout=timeout_s) as resp:
|
with urlopen(f"http://{WORMHOLE_HOST}:{WORMHOLE_PORT}/api/health", timeout=timeout_s) as resp:
|
||||||
@@ -337,7 +355,10 @@ def _current_runtime_state() -> dict[str, Any]:
|
|||||||
if not running and _probe_ready(timeout_s=0.35):
|
if not running and _probe_ready(timeout_s=0.35):
|
||||||
running = True
|
running = True
|
||||||
pid = 0
|
pid = 0
|
||||||
ready = running and _probe_ready()
|
if running and _trust_wormhole_file_ready(status):
|
||||||
|
ready = True
|
||||||
|
else:
|
||||||
|
ready = running and _probe_ready()
|
||||||
if not running:
|
if not running:
|
||||||
pid = 0
|
pid = 0
|
||||||
transport_active = status.get("transport_active", "") if ready else ""
|
transport_active = status.get("transport_active", "") if ready else ""
|
||||||
@@ -518,7 +539,8 @@ def connect_wormhole(*, reason: str = "connect") -> dict[str, Any]:
|
|||||||
proxy=str(settings.get("socks_proxy", "")),
|
proxy=str(settings.get("socks_proxy", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
deadline = time.monotonic() + 20.0
|
startup_deadline_s = float(os.environ.get("WORMHOLE_STARTUP_DEADLINE_S", "60") or 60)
|
||||||
|
deadline = time.monotonic() + max(20.0, startup_deadline_s)
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
if process.poll() is not None:
|
if process.poll() is not None:
|
||||||
err = f"Wormhole exited with code {process.returncode}."
|
err = f"Wormhole exited with code {process.returncode}."
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Lean local backend for live DM E2E — Infonet swarm + wormhole without OSINT fetchers.
|
||||||
|
# Pair with docker-compose.override.yml (local build + fleet secrets).
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
environment:
|
||||||
|
MESH_ONLY: "true"
|
||||||
|
# DM E2E uses direct onion relay_push_peer_urls — skip fleet hashchain sync
|
||||||
|
# (hundreds of dead manifest peers wedge /api/wormhole/status during Tor warmup).
|
||||||
|
SHADOWBROKER_MESH_NODE_RUNTIME: "false"
|
||||||
|
MESH_ARTI_ENABLED: "true"
|
||||||
|
MESH_INFONET_FLEET_JOIN: "false"
|
||||||
|
MESH_INFONET_FLEET_JOIN_DISABLED: "true"
|
||||||
|
PRIVACY_CORE_DEV_OVERRIDE: "true"
|
||||||
|
MESH_RELAY_PUSH_TIMEOUT_S: "300"
|
||||||
|
MESH_RELAY_MAX_FAILURES: "12"
|
||||||
|
MESH_DM_PENDING_PER_SENDER_LIMIT: "8"
|
||||||
|
MESH_DM_PERSIST_SPOOL: "true"
|
||||||
|
WORMHOLE_STARTUP_DEADLINE_S: "90"
|
||||||
|
MESH_WORMHOLE_TRUST_FILE_READY: "true"
|
||||||
@@ -21,6 +21,8 @@ services:
|
|||||||
MESH_SWARM_MANIFEST_PULL_INTERVAL_S: "300"
|
MESH_SWARM_MANIFEST_PULL_INTERVAL_S: "300"
|
||||||
# Fleet testnet HMAC — overrides stale per-node .env so announce/push auth matches seed.
|
# Fleet testnet HMAC — overrides stale per-node .env so announce/push auth matches seed.
|
||||||
MESH_PEER_PUSH_SECRET: "b7GoqsvoUD9MV7tyt0ZOzMptLA84QG6KCfaV9nDqz5Y"
|
MESH_PEER_PUSH_SECRET: "b7GoqsvoUD9MV7tyt0ZOzMptLA84QG6KCfaV9nDqz5Y"
|
||||||
|
# Dev fleet: allow wormhole child agent to start without release attestation.
|
||||||
|
PRIVACY_CORE_DEV_OVERRIDE: "true"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Lean fleet participant — Infonet swarm + DM relay without global OSINT fetchers.
|
||||||
|
# Use on secondary nodes (e.g. Pete) that should stay responsive for Tor DM tests.
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
environment:
|
||||||
|
MESH_ONLY: "true"
|
||||||
|
SHADOWBROKER_MESH_NODE_RUNTIME: "true"
|
||||||
|
MESH_ARTI_ENABLED: "true"
|
||||||
|
MESH_INFONET_FLEET_JOIN: "true"
|
||||||
|
MESH_WORMHOLE_TRUST_FILE_READY: "true"
|
||||||
|
PRIVACY_CORE_ALLOWED_SHA256: "5dd4b65a317277917842b12d7b430d49913789982ba906bd9a0ea6006d40e28a"
|
||||||
|
MESH_RELAY_PUSH_TIMEOUT_S: "300"
|
||||||
|
MESH_RELAY_MAX_FAILURES: "12"
|
||||||
|
MESH_DM_PENDING_PER_SENDER_LIMIT: "8"
|
||||||
|
MESH_DM_PERSIST_SPOOL: "true"
|
||||||
|
WORMHOLE_STARTUP_DEADLINE_S: "90"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1"
|
||||||
|
memory: 2G
|
||||||
@@ -41,6 +41,9 @@ services:
|
|||||||
# Tor/Arti SOCKS transport for private .onion Infonet sync.
|
# Tor/Arti SOCKS transport for private .onion Infonet sync.
|
||||||
- MESH_ARTI_ENABLED=${MESH_ARTI_ENABLED:-false}
|
- MESH_ARTI_ENABLED=${MESH_ARTI_ENABLED:-false}
|
||||||
- MESH_ARTI_SOCKS_PORT=${MESH_ARTI_SOCKS_PORT:-9050}
|
- MESH_ARTI_SOCKS_PORT=${MESH_ARTI_SOCKS_PORT:-9050}
|
||||||
|
# Lean Infonet participant (meshnode.sh equivalent). Skips global OSINT fetchers.
|
||||||
|
- MESH_ONLY=${MESH_ONLY:-}
|
||||||
|
- SHADOWBROKER_MESH_NODE_RUNTIME=${SHADOWBROKER_MESH_NODE_RUNTIME:-}
|
||||||
# Operator-trusted sync/push peers. Leave empty unless you control the peer secret on both sides.
|
# Operator-trusted sync/push peers. Leave empty unless you control the peer secret on both sides.
|
||||||
- MESH_RELAY_PEERS=${MESH_RELAY_PEERS:-}
|
- MESH_RELAY_PEERS=${MESH_RELAY_PEERS:-}
|
||||||
- MESH_PUBLIC_PEER_URL=${MESH_PUBLIC_PEER_URL:-}
|
- MESH_PUBLIC_PEER_URL=${MESH_PUBLIC_PEER_URL:-}
|
||||||
|
|||||||
+2931
-447
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user