diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 23:11:36 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 23:11:36 +0200 |
| commit | 35130e5528a52161630fd1c93572e1b2b7cd911b (patch) | |
| tree | 50954e2da56e186e2651ad838093eb6c09c259a8 /packages/meshbay-node/src/meshbay_node/transport | |
| parent | c66ee41d8476461939c5f4e7fdc71c5d7fb4a85c (diff) | |
| download | meshbay-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.py | 59 |
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") |