feat(us-017): P2P gossip, NAT-traversal relay node, and TLS
- packages/p2p: identity (peer_id from sha256 of RSA pubkey), TLS cert generation with SHA-256 fingerprint, GossipClient (WSS PubSub with per-topic handlers, dedup by msg_id, auto-reconnect), MdnsDiscovery (zeroconf optional dependency, graceful no-op fallback) - packages/relay: new meshnet-relay package — RelayServer (asyncio + websockets) with gossip fanout hub, circuit relay proxy for NAT traversal, peer registry; meshnet-relay CLI - packages/p2p/relay_bootstrap.json: team relay bootstrap list - Tracker: _NodeEntry gains relay_addr, cert_fingerprint, peer_id; both register and heartbeat handlers read and store these optional fields - docs/adr/0010 already written (previous commit) - conftest.py: packages/relay added to sys.path - 18 new tests; 115 passed total, 1 skipped (no regressions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
64
packages/p2p/meshnet_p2p/identity.py
Normal file
64
packages/p2p/meshnet_p2p/identity.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Peer identity — stable peer_id and RSA keypair, persisted to disk."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
_DEFAULT_IDENTITY_PATH = Path.home() / ".config" / "meshnet" / "identity.json"
|
||||
|
||||
|
||||
def _generate_keypair() -> tuple[bytes, bytes]:
|
||||
"""Return (private_key_pem, public_key_pem) for a new RSA-2048 keypair."""
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
priv_pem = key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
)
|
||||
pub_pem = key.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return priv_pem, pub_pem
|
||||
|
||||
|
||||
def _peer_id_from_pubkey(pub_pem: bytes) -> str:
|
||||
return hashlib.sha256(pub_pem).hexdigest()[:16]
|
||||
|
||||
|
||||
def load_or_create_identity(path: Path | None = None) -> dict:
|
||||
"""Return identity dict with peer_id, private_key_pem, public_key_pem.
|
||||
|
||||
Creates and persists a new identity if none exists at path.
|
||||
"""
|
||||
p = path or _DEFAULT_IDENTITY_PATH
|
||||
if p.exists():
|
||||
try:
|
||||
data = json.loads(p.read_text())
|
||||
if "peer_id" in data and "public_key_pem" in data:
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
priv_pem, pub_pem = _generate_keypair()
|
||||
identity = {
|
||||
"peer_id": _peer_id_from_pubkey(pub_pem),
|
||||
"private_key_pem": priv_pem.decode(),
|
||||
"public_key_pem": pub_pem.decode(),
|
||||
}
|
||||
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(identity, indent=2))
|
||||
try:
|
||||
os.chmod(p, stat.S_IRUSR | stat.S_IWUSR)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return identity
|
||||
Reference in New Issue
Block a user