summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-11 23:11:36 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-11 23:11:36 +0200
commit35130e5528a52161630fd1c93572e1b2b7cd911b (patch)
tree50954e2da56e186e2651ad838093eb6c09c259a8 /packages/meshbay-node/src/meshbay_node/transport
parentc66ee41d8476461939c5f4e7fdc71c5d7fb4a85c (diff)
downloadmeshbay-35130e5528a52161630fd1c93572e1b2b7cd911b.tar.gz
feat(node): audit logging + local admin UI rewrite
Add SQLite audit store for legal compliance (LCEN/DSA): logs user IP, actions (handshake, file download/upload/delete, stream, chat), and timestamps. Retention: 1 year, with cleanup method. WebRTC transport now logs all user actions to the audit store with remote IP extraction from the ICE transport. Local web UI rewritten as a proper admin dashboard: - Stats cards (groups, files, peers) - Connected peers table with IP, username, group, state - Group cards with file listings and shared directory info - Audit log page with event/user filtering - Dark theme, responsive, auto-refresh - JSON API: /api/status, /api/groups, /api/peers, /api/audit, /api/config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py59
1 files changed, 57 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index ab95550..e692c80 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -124,16 +124,35 @@ class _DataChannelBuffer:
yield msgpack.unpackb(msg_bytes, raw=False)
+def _get_remote_ip(pc: RTCPeerConnection) -> str:
+ """Best-effort extraction of the remote peer IP from the ICE transport."""
+ try:
+ dtls = pc.sctp and pc.sctp.transport
+ ice = dtls and dtls.transport
+ conn = ice and ice._connection
+ if conn and hasattr(conn, '_nominated') and conn._nominated:
+ for pair in conn._nominated.values():
+ return pair.remote_candidate.host
+ if conn and conn.remote_candidates:
+ return conn.remote_candidates[0].host
+ except Exception:
+ pass
+ return ""
+
+
class WebRTCPeerSession:
"""One WebRTC peer connection, handling MNP over a DataChannel."""
- def __init__(self, pc: RTCPeerConnection, node_ctx: dict):
+ def __init__(self, pc: RTCPeerConnection, node_ctx: dict, peer_id: str = ""):
self._pc = pc
self._ctx = node_ctx
self._channel: RTCDataChannel | None = None
self._buffer = _DataChannelBuffer()
self._user_id: str | None = None
self._group_id: str | None = None
+ self._peer_id: str = peer_id
+ self._remote_ip: str = ""
+ self._username: str = ""
def _setup_channel(self, channel: RTCDataChannel) -> None:
self._channel = channel
@@ -178,6 +197,20 @@ class WebRTCPeerSession:
log.error("Error handling %s on DataChannel: %s", mtype, e)
self._send({"type": "error", "detail": str(e)})
+ def _audit(self, event: str, detail: str = "") -> None:
+ audit = self._ctx.get("audit_store")
+ if audit and self._user_id:
+ if not self._remote_ip:
+ self._remote_ip = _get_remote_ip(self._pc)
+ asyncio.ensure_future(audit.log_event(
+ user_id=self._user_id,
+ event=event,
+ ip=self._remote_ip,
+ username=self._username,
+ group_id=self._group_id or "",
+ detail=detail,
+ ))
+
def _do_handshake(self, msg: dict) -> None:
token = msg.get("token", "")
group_id = msg.get("group_id", "")
@@ -185,6 +218,7 @@ class WebRTCPeerSession:
decoded = jwt.decode(token, self._ctx["hub_pk_pem"], algorithms=["EdDSA"])
except Exception as e:
self._send({"type": "error", "detail": f"Invalid JWT: {e}"})
+ self._audit_auth_failed(group_id, str(e))
return
denylist = self._ctx.get("denylist")
@@ -202,6 +236,7 @@ class WebRTCPeerSession:
self._user_id = decoded["sub"]
self._group_id = group_id
+ self._username = decoded.get("username", "")
peers = self._ctx.get("_peers")
if peers is not None:
@@ -214,6 +249,19 @@ class WebRTCPeerSession:
"v": MNP_VERSION,
"node_pk": pk_to_b64(self._ctx["sk_node"].public_key()),
})
+ self._audit("handshake")
+
+ def _audit_auth_failed(self, group_id: str, reason: str) -> None:
+ audit = self._ctx.get("audit_store")
+ if audit:
+ self._remote_ip = _get_remote_ip(self._pc)
+ asyncio.ensure_future(audit.log_event(
+ user_id="unknown",
+ event="auth_failed",
+ ip=self._remote_ip,
+ group_id=group_id,
+ detail=reason,
+ ))
def _group_ctx(self) -> dict:
if "groups" in self._ctx and self._group_id:
@@ -274,6 +322,8 @@ class WebRTCPeerSession:
file_hash,
)
self._send(chunk_data)
+ if chunk_index == 0:
+ self._audit("file_download", entry.name)
def _do_stream_segment(self, msg: dict) -> None:
ctx = self._group_ctx()
@@ -365,6 +415,7 @@ class WebRTCPeerSession:
pass
self._send({"type": "ack", "v": MNP_VERSION})
+ self._audit("chat_message")
def _do_chat_history(self, msg: dict) -> None:
chat_store = self._ctx.get("chat_store")
@@ -441,6 +492,7 @@ class WebRTCPeerSession:
final_path = shared_root / safe_name
tmp_path.rename(final_path)
log.info("Upload complete: %s (%d chunks)", safe_name, total_chunks)
+ self._audit("file_upload", safe_name)
def _do_file_delete(self, msg: dict) -> None:
ctx = self._group_ctx()
@@ -463,6 +515,7 @@ class WebRTCPeerSession:
if file_path.exists():
file_path.unlink()
log.info("File deleted: %s", entry.name)
+ self._audit("file_delete", entry.name)
ctx["index"].remove_entry(file_id)
self._send({
@@ -549,6 +602,7 @@ class WebRTCPeerSession:
"file_id": file_id,
})
log.info("Streamed %s: %d segments", entry.name, index)
+ self._audit("stream_video", entry.name)
def _send(self, obj: dict) -> None:
if self._channel and self._channel.readyState == "open":
@@ -558,6 +612,7 @@ class WebRTCPeerSession:
self._channel.readyState if self._channel else "none")
async def close(self) -> None:
+ self._audit("disconnect")
peers = self._ctx.get("_peers")
if peers and self._user_id:
peers.pop(self._user_id, None)
@@ -639,7 +694,7 @@ class WebRTCTransport:
iceServers=[RTCIceServer(urls=s) for s in self._stun] if self._stun else []
)
pc = RTCPeerConnection(configuration=config)
- session = WebRTCPeerSession(pc, self._ctx)
+ session = WebRTCPeerSession(pc, self._ctx, peer_id=peer_id)
self._sessions[peer_id] = session
@pc.on("datachannel")