From 1645d09ebaf9695b58ee6fe8d79c478b4a87314f Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Sun, 26 Jul 2026 07:49:00 +0000 Subject: [PATCH] network subsystem --- index.html | 2 +- kernel_ai/services/network.py | 150 +- static/js/active-connections.js | 14 + static/js/network-stack.js | 1807 +++++++++++++++++++++++- templates/linux-network-subsystem.html | 27 +- 5 files changed, 1929 insertions(+), 71 deletions(-) diff --git a/index.html b/index.html index 7223bb5..8e164b9 100755 --- a/index.html +++ b/index.html @@ -95,7 +95,7 @@

Linux Kernel Ring 0 Visualization

- + diff --git a/kernel_ai/services/network.py b/kernel_ai/services/network.py index 1f6325c..6753f32 100644 --- a/kernel_ai/services/network.py +++ b/kernel_ai/services/network.py @@ -4,6 +4,7 @@ import ipaddress import logging +import os import re import subprocess import time @@ -55,7 +56,7 @@ def get_active_connections(): lines = f.readlines()[1:] for line in lines: parts = line.strip().split() - if len(parts) < 4: + if len(parts) < 10: continue local_addr = parts[1] remote_addr = parts[2] @@ -68,6 +69,14 @@ def hex_to_ip(hex_str): local_ip = hex_to_ip(local_addr.split(":")[0]) local_port = int(local_addr.split(":")[1], 16) + try: + uid = int(parts[7]) + except ValueError: + uid = 0 + try: + inode = int(parts[9]) + except ValueError: + inode = 0 if remote_addr != "00000000:0000": remote_ip = hex_to_ip(remote_addr.split(":")[0]) @@ -78,6 +87,8 @@ def hex_to_ip(hex_str): "remote": f"{remote_ip}:{remote_port}", "state": state, "type": "TCP", + "uid": uid, + "inode": inode, } ) return connections[:20] @@ -97,14 +108,78 @@ def hex_to_ip(hex_str): def get_mock_active_connections(): return [ - {"local": "127.0.0.1:22", "remote": "192.168.1.100:54321", "state": "01", "type": "TCP"}, - {"local": "0.0.0.0:80", "remote": "10.0.0.50:12345", "state": "01", "type": "TCP"}, - {"local": "127.0.0.1:3306", "remote": "172.16.0.10:65432", "state": "01", "type": "TCP"}, - {"local": "0.0.0.0:443", "remote": "203.0.113.0:54321", "state": "01", "type": "TCP"}, - {"local": "127.0.0.1:5001", "remote": "192.168.1.101:12345", "state": "01", "type": "TCP"}, + {"local": "127.0.0.1:22", "remote": "192.168.1.100:54321", "state": "01", "type": "TCP", "uid": 0, "inode": 12345}, + {"local": "0.0.0.0:80", "remote": "10.0.0.50:12345", "state": "01", "type": "TCP", "uid": 0, "inode": 12346}, + {"local": "127.0.0.1:3306", "remote": "172.16.0.10:65432", "state": "01", "type": "TCP", "uid": 0, "inode": 12347}, + {"local": "0.0.0.0:443", "remote": "203.0.113.0:54321", "state": "01", "type": "TCP", "uid": 0, "inode": 12348}, + {"local": "127.0.0.1:5001", "remote": "192.168.1.101:12345", "state": "01", "type": "TCP", "uid": 0, "inode": 12349}, ] +def _addr_key(addr) -> str: + if not addr: + return "" + if isinstance(addr, (tuple, list)) and len(addr) >= 2: + return f"{addr[0]}:{addr[1]}" + return str(addr) + + +def _get_socket_example(all_connections: list) -> dict | None: + """Pick a live flow and attach fd/pid via psutil when possible (for Socket morph).""" + interesting = [ + c + for c in all_connections + if not str(c.get("remote", "")).startswith("127.0.0.1") + and not str(c.get("remote", "")).startswith("0.0.0.0") + ] + # Prefer ESTABLISHED with a real inode — better for fd→sock* morph. + established = [ + c for c in interesting + if str(c.get("state", "")).upper() == "01" and int(c.get("inode") or 0) > 0 + ] + with_inode = [c for c in interesting if int(c.get("inode") or 0) > 0] + base = ( + established[0] + if established + else (with_inode[0] if with_inode else (interesting[0] if interesting else (all_connections[0] if all_connections else None))) + ) + if not base: + return None + example = { + "local": base.get("local"), + "remote": base.get("remote"), + "type": str(base.get("type", "TCP")).upper(), + "state_code": base.get("state", "00"), + "state_name": _tcp_state_name(base.get("state", "00")), + "inode": int(base.get("inode") or 0), + "uid": int(base.get("uid") or 0), + "fd": None, + "pid": None, + "process": None, + } + want_local = str(example["local"] or "") + want_remote = str(example["remote"] or "") + try: + for conn in psutil.net_connections(kind="inet"): + if conn.status and str(conn.status).upper() not in ("ESTABLISHED", "LISTEN", "CLOSE_WAIT", "TIME_WAIT"): + # Still allow match by address even for other states. + pass + local = _addr_key(conn.laddr) + remote = _addr_key(conn.raddr) + if local == want_local and (not want_remote or remote == want_remote or not remote): + example["fd"] = int(conn.fd) if conn.fd is not None and conn.fd >= 0 else None + example["pid"] = int(conn.pid) if conn.pid is not None else None + if example["pid"]: + try: + example["process"] = psutil.Process(example["pid"]).name() + except (psutil.Error, OSError): + example["process"] = None + break + except (psutil.Error, OSError, AttributeError): + pass + return example + + def _tcp_state_name(code): states = { "01": "ESTABLISHED", @@ -282,6 +357,44 @@ def _rate_to_mbps(value, unit): return v * scale +def _read_sysctl_int(path: str, default: int = 0) -> int: + try: + with open(path, "r", encoding="utf-8", errors="ignore") as fh: + return int(fh.read().strip()) + except (OSError, ValueError): + return default + + +def _get_conntrack_stats() -> dict: + """Best-effort nf_conntrack occupancy for the Netfilter morph view.""" + count = _read_sysctl_int("/proc/sys/net/netfilter/nf_conntrack_count", 0) + maximum = _read_sysctl_int("/proc/sys/net/netfilter/nf_conntrack_max", 0) + available = count > 0 or maximum > 0 + # Also true when the sysctl node exists even at zero. + try: + available = available or os.path.exists("/proc/sys/net/netfilter/nf_conntrack_count") + except OSError: + pass + usage = round(count / maximum, 5) if maximum > 0 else 0.0 + nf_conntrack = False + nft = False + try: + with open("/proc/modules", "r", encoding="utf-8", errors="ignore") as fh: + mods = fh.read() + nf_conntrack = "nf_conntrack" in mods + nft = "nf_tables" in mods + except OSError: + pass + return { + "available": bool(available), + "count": int(count), + "max": int(maximum), + "usage": usage, + "nf_conntrack": nf_conntrack, + "nft": nft, + } + + def _hex_le_ipv4(hex_str: str) -> str: raw = (hex_str or "").strip() if len(raw) != 8: @@ -408,6 +521,7 @@ def get_network_stack_realtime(network_stack_prev=None): all_connections = get_active_connections() interesting = [c for c in all_connections if not c["remote"].startswith("127.0.0.1") and not c["remote"].startswith("0.0.0.0")] flow = interesting[0] if interesting else (all_connections[0] if all_connections else None) + socket_example = _get_socket_example(all_connections) if flow: flow = { "local": flow.get("local"), @@ -415,12 +529,18 @@ def get_network_stack_realtime(network_stack_prev=None): "type": str(flow.get("type", "TCP")).upper(), "state_code": flow.get("state", "00"), "state_name": _tcp_state_name(flow.get("state", "00")), + "inode": int(flow.get("inode") or 0), + "uid": int(flow.get("uid") or 0), + "fd": (socket_example or {}).get("fd"), + "pid": (socket_example or {}).get("pid"), + "process": (socket_example or {}).get("process"), } tcpext = _parse_netstat_tcpext() ip_stats = _parse_snmp_section("Ip") tcp_stats = _parse_snmp_section("Tcp") ss_metrics = _get_ss_tcp_metrics() + conntrack = _get_conntrack_stats() retrans_total = tcpext.get("RetransSegs", 0) ip_in_total = ip_stats.get("InReceives", 0) @@ -492,7 +612,12 @@ def rate(curr, prev): "flow": flow, "layer_metrics": { "userspace": {"active_processes": len(psutil.pids())}, - "socket_api": {"active_sockets": len(all_connections), "established": established, "retransmits_per_sec": round(retrans_per_sec, 2)}, + "socket_api": { + "active_sockets": len(all_connections), + "established": established, + "retransmits_per_sec": round(retrans_per_sec, 2), + "example": socket_example, + }, "tcp_udp": { "established": established, "retrans_per_sec": round(retrans_per_sec, 2), @@ -509,7 +634,16 @@ def rate(curr, prev): "drop_per_sec": round(ip_drop_per_sec, 3), "drop_ratio": round(drop_ratio, 5), }, - "netfilter": {"drop_per_sec": round(ip_drop_per_sec, 3), "drop_ratio": round(drop_ratio, 5)}, + "netfilter": { + "drop_per_sec": round(ip_drop_per_sec, 3), + "drop_ratio": round(drop_ratio, 5), + "conntrack_count": int(conntrack.get("count", 0)), + "conntrack_max": int(conntrack.get("max", 0)), + "conntrack_usage": float(conntrack.get("usage", 0.0)), + "conntrack_available": bool(conntrack.get("available")), + "nft": bool(conntrack.get("nft")), + "nf_conntrack": bool(conntrack.get("nf_conntrack")), + }, "driver": { "iface": iface, "rx_mb_s": round(rx_per_sec / (1024 * 1024), 3), diff --git a/static/js/active-connections.js b/static/js/active-connections.js index 85cabf0..1a69944 100755 --- a/static/js/active-connections.js +++ b/static/js/active-connections.js @@ -117,6 +117,20 @@ class ActiveConnectionsManager { .attr('font-family', 'monospace'); this.attachSocketTooltipHandlers(row, connection); + + // Flow follow: open Network stack focused on this remote in FIB/neigh. + row.on('click', (event) => { + if (event && typeof event.stopPropagation === 'function') event.stopPropagation(); + const remote = String(connection.remote || '').trim(); + if (!remote) return; + const local = String(connection.local || '').trim(); + const type = String(connection.type || 'TCP').toUpperCase(); + const params = new URLSearchParams(); + params.set('remote', remote); + if (local) params.set('local', local); + if (type) params.set('proto', type); + window.location.assign(`/linux-network-subsystem?${params.toString()}`); + }); }); } diff --git a/static/js/network-stack.js b/static/js/network-stack.js index 59df9b1..baf7e04 100644 --- a/static/js/network-stack.js +++ b/static/js/network-stack.js @@ -1,7 +1,7 @@ // Network Stack Visualization - vertical packet flow through Linux networking layers -// Version: 6 — L04 IP drill-in replaces puzzle with FIB/route/neigh/ICMP map +// Version: 11 — Netfilter morph (syntax-safe) + hooks/conntrack/verdict -debugLog('🌐 network-stack.js v6: Script loading...'); +debugLog('🌐 network-stack.js v16: Script loading...'); class NetworkStackVisualization { constructor() { @@ -96,8 +96,31 @@ class NetworkStackVisualization { this.galaxyStateData = null; this.lifecyclePanelNode = null; this.ipMapPinned = false; + this.ipMorphTarget = null; // { kind:'route'|'neigh'|'flow', ... } + this.ipMapFrozen = null; // snapshot while morph/IP drill is being read + this.tcpMapPinned = false; + this.tcpMorphTarget = null; // { focus:'flow'|'cwnd'|'rtt'|'cc'|... } + this.tcpFrozen = null; // { tcp_udp, bbr, flow } + this.nfMapPinned = false; + this.nfMorphTarget = null; // { focus:'hook'|'ct'|'verdict'|... } + this.nfFrozen = null; // { netfilter, flow } + this.sockMapPinned = false; + this.sockMorphTarget = null; // { focus:'fd'|'inode'|'sk'|'queue'|... } + this.sockFrozen = null; // { socket_api, flow } + this.flowFollow = null; // { remote, local, proto, ip, port } from ?remote= + this._flowFollowApplied = false; + this._ghostPending = false; + this._ghostEl = null; + this._ghostTimer = null; + this._ghostFadeTimer = null; this.hideOsiTiles = true; + // Keep the particle swarm and hero packet off — morph stays as panel ribbons. this.hideVerticalOrbs = true; + this.packetMorphEnabled = false; + this.packetMorphPhase = -1; + this.packetMorphCtx = null; + this.packetMorphTag = null; + this.packetMorphPulse = 0; this.packetLifecycleStages = [ 'NIC RX', 'IRQ', @@ -897,12 +920,210 @@ class NetworkStackVisualization { this.scene.add(trail); this.packetTrail.push(trail); } - if (this.hideVerticalOrbs) { + if (this.hideVerticalOrbs && !this.packetMorphEnabled) { this.packet.visible = false; this.packetGlow.visible = false; this.packetTrail.forEach((trail) => { if (trail) trail.visible = false; }); + } else if (this.packetMorphEnabled) { + this.packet.visible = true; + this.packetGlow.visible = true; + this.packetTrail.forEach((trail) => { + if (trail) trail.visible = true; + }); + this.ensurePacketMorphTag(); + } + } + + ensurePacketMorphTag() { + if (this.packetMorphTag || !this.scene) return; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + canvas.width = 380; + canvas.height = 88; + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + const material = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + depthTest: false, + depthWrite: false, + opacity: 0, + }); + const sprite = new THREE.Sprite(material); + sprite.scale.set(1.85, 0.42, 1); + sprite.renderOrder = 4; + sprite.visible = false; + this.scene.add(sprite); + this.packetMorphTag = { + sprite, + canvas, + ctx, + texture, + lastText: '', + lastIntensity: -1, + }; + } + + paintPacketMorphTag(text, intensity = 0.75) { + const tag = this.packetMorphTag; + if (!tag || !tag.ctx) return; + const safeText = String(text || ''); + const safeIntensity = Math.max(0, Math.min(1, Number(intensity) || 0)); + if (safeText === tag.lastText && Math.abs(safeIntensity - (tag.lastIntensity ?? -1)) < 0.04) return; + tag.lastText = safeText; + tag.lastIntensity = safeIntensity; + + const ctx = tag.ctx; + const w = tag.canvas.width; + const h = tag.canvas.height; + ctx.clearRect(0, 0, w, h); + + ctx.fillStyle = `rgba(8, 12, 20, ${0.55 + safeIntensity * 0.28})`; + ctx.strokeStyle = `rgba(230, 193, 90, ${0.35 + safeIntensity * 0.45})`; + ctx.lineWidth = 1.4; + if (typeof ctx.roundRect === 'function') { + ctx.beginPath(); + ctx.roundRect(0.5, 0.5, w - 1, h - 1, 7); + ctx.fill(); + ctx.stroke(); + } else { + ctx.fillRect(0, 0, w, h); + ctx.strokeRect(0.5, 0.5, w - 1, h - 1); + } + + const lines = safeText.split('\n').slice(0, 2); + ctx.textBaseline = 'middle'; + ctx.shadowColor = `rgba(230, 193, 90, ${0.18 + safeIntensity * 0.35})`; + ctx.shadowBlur = 7; + if (lines[0]) { + ctx.font = '15px "Share Tech Mono", monospace'; + ctx.fillStyle = `rgba(236, 244, 250, ${0.92 + safeIntensity * 0.08})`; + ctx.strokeStyle = 'rgba(10, 14, 20, 0.85)'; + ctx.lineWidth = 2.4; + ctx.strokeText(lines[0], 14, h * 0.34); + ctx.fillText(lines[0], 14, h * 0.34); + } + if (lines[1]) { + ctx.font = '13px "Share Tech Mono", monospace'; + ctx.fillStyle = `rgba(169, 212, 232, ${0.88 + safeIntensity * 0.1})`; + ctx.strokeStyle = 'rgba(10, 14, 20, 0.8)'; + ctx.lineWidth = 2.2; + ctx.strokeText(lines[1], 14, h * 0.72); + ctx.fillText(lines[1], 14, h * 0.72); + } + ctx.shadowBlur = 0; + tag.texture.needsUpdate = true; + } + + resolvePacketMorphPhase(y) { + if (!this.layerMap) return -1; + const top = (this.layerMap.tcp + this.layerMap.ip) / 2; + const bot = (this.layerMap.ip + this.layerMap.netfilter) / 2; + if (!(y <= top && y >= bot)) return -1; + const span = Math.max(0.001, top - bot); + const t = (top - y) / span; + return Math.min(3, Math.floor(t * 4)); + } + + refreshPacketMorphContext() { + // Snapshot into packetMorphCtx only — do not touch ipMapFrozen (panel freeze). + this.packetMorphCtx = this.resolveMorphContext({ kind: 'flow' }); + return this.packetMorphCtx; + } + + buildPacketMorphCaption(phase, ctx) { + const c = ctx || {}; + const addr = this.ipv4ToBe32(c.ip); + const port = this.portToBe16(c.port); + const route = c.route || {}; + const ha = this.macToHa(c.hop?.mac); + const ttl = Number(this.getIpMapData()?.ip?.default_ttl) || 64; + const dest = route.default ? 'default' : (route.destination || c.ip || '?'); + const gw = route.gateway && route.gateway !== '*' ? route.gateway : 'on-link'; + const iface = c.iface || '?'; + const nh = c.nexthopIp || gw; + const macShort = (ha.mac && ha.mac !== '??') ? ha.mac : 'ha[] unfinished'; + + if (phase === 0) { + return 'IP hdr ' + (c.ip || '?') + ':' + port.port + ' ttl ' + ttl + '\nip_queue_xmit'; + } + if (phase === 1) { + return 'route ' + dest + ' → ' + gw + ' oif ' + iface + '\nfib_table_lookup'; + } + if (phase === 2) { + return 'neigh ' + nh + ' ' + macShort + '\nneigh_resolve_output'; + } + return 'eth hdr → ' + iface + ' ' + (addr?.hex || 'be32') + '\ndev_queue_xmit'; + } + + applyPacketMorphPhase(phase) { + if (!this.packet) return; + const colors = [0xE6C15A, 0xE6C15A, 0x96FFBE, 0x67BEE0]; + const scales = [1.0, 0.92, 0.86, 1.08]; + const color = colors[phase] != null ? colors[phase] : 0xE6C15A; + const scale = scales[phase] != null ? scales[phase] : 1; + this.packet.material.color.setHex(color); + if (this.packetGlow?.material) this.packetGlow.material.color.setHex(color); + this.packet.scale.setScalar(scale); + if (this.packetGlow) this.packetGlow.scale.setScalar(scale * (1.15 + this.packetMorphPulse * 0.2)); + this.packetTrail.forEach((trail, i) => { + if (trail?.material?.color) trail.material.color.setHex(color); + if (trail) trail.scale.setScalar(Math.max(0.55, scale * (1 - i * 0.08))); + }); + } + + setPacketMorphVisible(on) { + const tag = this.packetMorphTag; + if (!tag?.sprite) return; + tag.sprite.visible = !!on; + if (tag.sprite.material) { + tag.sprite.material.opacity = on ? 1 : 0; + } + } + + updatePacketMorph() { + if (!this.packetMorphEnabled || !this.packet) return; + this.ensurePacketMorphTag(); + const y = this.packet.position.y; + const phase = this.resolvePacketMorphPhase(y); + + if (phase < 0) { + if (this.packetMorphPhase !== -1) { + this.packetMorphPhase = -1; + this.packetMorphCtx = null; + this.setPacketMorphVisible(false); + this.packet.scale.setScalar(1); + if (this.packetGlow) this.packetGlow.scale.setScalar(1); + this.updatePacketColorByFlow(); + } + return; + } + + if (this.packetMorphPhase < 0 || !this.packetMorphCtx) { + this.refreshPacketMorphContext(); + } + if (phase !== this.packetMorphPhase) { + this.packetMorphPhase = phase; + this.packetMorphPulse = 1; + const caption = this.buildPacketMorphCaption(phase, this.packetMorphCtx); + this.paintPacketMorphTag(caption, 0.9); + this.applyPacketMorphPhase(phase); + this.setPacketMorphVisible(true); + } else if (this.packetMorphPulse > 0) { + this.packetMorphPulse = Math.max(0, this.packetMorphPulse - 0.04); + this.applyPacketMorphPhase(phase); + } + + const tag = this.packetMorphTag; + if (tag?.sprite) { + tag.sprite.position.set( + this.packet.position.x + 1.55, + this.packet.position.y + 0.18, + this.packet.position.z + ); } } @@ -1947,8 +2168,13 @@ class NetworkStackVisualization { updatePacketLifecycleUI() { if (!this.lifecyclePanelNode) return; - if (this.ipMapPinned || this.drillLayerId === 'ip') { - this.renderIpLayerMapPanel(); + // While IP/TCP map or morph is open, keep the right panel frozen — no live rewrites. + if ( + this.ipMapPinned || this.tcpMapPinned || this.nfMapPinned || this.sockMapPinned + || this.drillLayerId === 'ip' || this.drillLayerId === 'tcp' || this.drillLayerId === 'netfilter' + || this.drillLayerId === 'socket' + || this.ipMorphTarget || this.tcpMorphTarget || this.nfMorphTarget || this.sockMorphTarget + ) { return; } const idx = this.getPacketLifecycleIndex(); @@ -2234,12 +2460,28 @@ class NetworkStackVisualization { }); } + resetPacketLoop() { + if (!this.packet || !this.layerMap) return; + this.packet.position.y = this.layerMap.userspace + 0.5; + this.packetMorphPhase = -1; + this.packetMorphCtx = null; + this.packetMorphPulse = 0; + this.setPacketMorphVisible(false); + this.packet.scale.setScalar(1); + if (this.packetGlow) this.packetGlow.scale.setScalar(1); + this.updatePacketColorByFlow(); + } + updatePacket(dt) { if (!this.packet) return; this.packet.position.y -= this.packetSpeed * dt; - this.packetGlow.position.copy(this.packet.position); - this.packetGlow.scale.setScalar(1 + Math.sin(performance.now() * 0.008) * 0.1); + if (this.packetGlow) { + this.packetGlow.position.copy(this.packet.position); + if (this.packetMorphPhase < 0) { + this.packetGlow.scale.setScalar(1 + Math.sin(performance.now() * 0.008) * 0.1); + } + } // Retransmit visual on TCP/UDP layer. this.retransmitCooldown -= dt; @@ -2255,7 +2497,7 @@ class NetworkStackVisualization { if (this.dropCooldown <= 0 && Math.abs(this.packet.position.y - this.layerMap.netfilter) < 0.08) { if (Math.random() < this.dropProbability) { this.triggerDrop(); - this.packet.position.y = this.layerMap.userspace + 0.5; + this.resetPacketLoop(); this.dropCooldown = 1.8; return; } @@ -2263,14 +2505,18 @@ class NetworkStackVisualization { // NIC -> wire -> remote reached; restart packet loop. if (this.packet.position.y < this.layerMap.nic - 0.75) { - this.packet.position.y = this.layerMap.userspace + 0.5; + this.resetPacketLoop(); } // Trail follows packet. for (let i = this.packetTrail.length - 1; i > 0; i--) { this.packetTrail[i].position.lerp(this.packetTrail[i - 1].position, 0.65); } - this.packetTrail[0].position.lerp(this.packet.position, 0.65); + if (this.packetTrail[0]) { + this.packetTrail[0].position.lerp(this.packet.position, 0.65); + } + + this.updatePacketMorph(); } updatePacketColorByFlow() { @@ -2359,7 +2605,25 @@ class NetworkStackVisualization { if (this.flowNode) { if (flow) { - this.flowNode.textContent = `process -> syscall -> socket -> ${flowType} ${flowState} -> IP -> NIC -> wire -> ${flow.remote || 'remote'}`; + const remote = flow.remote || 'remote'; + this.flowNode.innerHTML = ''; + const prefix = document.createTextNode( + `process -> syscall -> socket -> ${flowType} ${flowState} -> IP -> NIC -> wire -> ` + ); + const remoteSpan = document.createElement('span'); + remoteSpan.textContent = remote; + if (this.flowFollow?.remote) { + remoteSpan.style.color = '#e6c15a'; + remoteSpan.style.textShadow = '0 0 8px rgba(230,193,90,0.35)'; + this.flowNode.style.borderColor = 'rgba(230,193,90,0.55)'; + this.flowNode.style.boxShadow = '0 0 14px rgba(230,193,90,0.12)'; + } else { + remoteSpan.style.color = ''; + this.flowNode.style.borderColor = 'rgba(90, 104, 120, 0.32)'; + this.flowNode.style.boxShadow = 'none'; + } + this.flowNode.appendChild(prefix); + this.flowNode.appendChild(remoteSpan); } else { this.flowNode.textContent = 'process -> syscall -> socket -> TCP -> IP -> NIC -> wire -> remote (no active flow)'; } @@ -2580,11 +2844,25 @@ class NetworkStackVisualization { this.dropProbability = Math.max(0.03, Math.min(0.75, Number(data.signals?.drop_probability ?? 0.2))); this.retransmitProbability = Math.max(0.04, Math.min(0.75, Number(data.signals?.retransmit_probability ?? 0.28))); this.packetSpeed = Math.max(1.1, Math.min(5.2, Number(data.signals?.packet_speed ?? 2.2))); + this.applyFlowFollowOverride(); this.updatePacketColorByFlow(); this.updateTelemetryUI(); this.recordBbrSample(); - if (this.drillLayerId) this.openLayerDrilldown(this.drillLayerId); - if (this.bbrOpen) this.openBbrOverlay(); + // Do not rebuild drill/morph overlays on every poll — user needs time to read. + const freezeDrill = ( + this.drillLayerId === 'ip' || this.drillLayerId === 'tcp' || this.drillLayerId === 'netfilter' + || this.drillLayerId === 'socket' + || this.ipMorphTarget || this.tcpMorphTarget || this.nfMorphTarget || this.sockMorphTarget + ); + if (freezeDrill) { + // freeze: keep current overlay DOM + frozen snapshots + } else if (this.drillLayerId) { + this.openLayerDrilldown(this.drillLayerId); + } + if (this.bbrOpen && !freezeDrill) { + this.openBbrOverlay(); + } + this.maybeApplyFlowFollowDeepLink(); if (this.telemetryErrorNode) { this.telemetryErrorNode.textContent = ''; } @@ -2931,7 +3209,9 @@ class NetworkStackVisualization { ], netfilter: () => [ ['drop/s', n(m.netfilter?.drop_per_sec)], - ['drop ratio', `${(n(m.netfilter?.drop_ratio) * 100).toFixed(2)}%`] + ['drop ratio', `${(n(m.netfilter?.drop_ratio) * 100).toFixed(2)}%`], + ['conntrack', `${n(m.netfilter?.conntrack_count)} / ${n(m.netfilter?.conntrack_max) || '—'}`], + ['ct usage', `${(n(m.netfilter?.conntrack_usage) * 100).toFixed(2)}%`] ], driver: () => [ ['iface', m.driver?.iface ?? 'n/a'], @@ -2951,9 +3231,502 @@ class NetworkStackVisualization { } getIpMapData() { + if (this.ipMapFrozen) return this.ipMapFrozen; return this.telemetryData?.ip_map || { routes: [], neigh: [], icmp: {}, ip: {} }; } + freezeIpMapSnapshot() { + const live = this.telemetryData?.ip_map || { routes: [], neigh: [], icmp: {}, ip: {} }; + try { + this.ipMapFrozen = JSON.parse(JSON.stringify(live)); + } catch (_) { + this.ipMapFrozen = live; + } + } + + escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + ipv4ToBe32(ip) { + const parts = String(ip || '').split('.').map((x) => Number(x)); + if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) { + return null; + } + const host = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0; + // Network byte order (big-endian) as stored in __be32 / sockaddr_in. + const be = host; + return { + dotted: parts.join('.'), + host, + be, + hex: `0x${be.toString(16).padStart(8, '0')}`, + bytes: parts.map((n) => `0x${n.toString(16).padStart(2, '0')}`).join(' '), + }; + } + + portToBe16(port) { + const p = Number(port); + if (!Number.isFinite(p) || p < 0 || p > 65535) return { port: 0, hex: '0x0000' }; + return { port: p, hex: `0x${p.toString(16).padStart(4, '0')}` }; + } + + macToHa(mac) { + const parts = String(mac || '').toLowerCase().split(':').filter(Boolean); + if (parts.length !== 6) return { mac: String(mac || '??'), ha: 'ha[] unfinished', bytes: [] }; + const bytes = parts.map((b) => `0x${b.padStart(2, '0')}`); + return { mac: parts.join(':'), ha: `{ ${bytes.join(', ')} }`, bytes }; + } + + parseEndpoint(endpoint) { + const value = String(endpoint || '').trim(); + if (!value) return { ip: '', port: null }; + const sep = value.lastIndexOf(':'); + if (sep <= 0) return { ip: value, port: null }; + const ip = value.slice(0, sep); + const portNum = Number(value.slice(sep + 1)); + return { ip, port: Number.isFinite(portNum) ? portNum : null }; + } + + parseFlowFollowFromUrl() { + try { + const params = new URLSearchParams(window.location.search || ''); + const remote = String(params.get('remote') || '').trim(); + if (!remote) return null; + const local = String(params.get('local') || '').trim(); + const proto = String(params.get('proto') || 'TCP').toUpperCase(); + const { ip, port } = this.parseEndpoint(remote); + if (!ip) return null; + return { remote, local, proto, ip, port: port != null ? port : 443 }; + } catch (_) { + return null; + } + } + + applyFlowFollowOverride() { + if (!this.flowFollow) { + this.flowFollow = this.parseFlowFollowFromUrl(); + } + if (!this.flowFollow || !this.telemetryData) return; + const ff = this.flowFollow; + const prev = this.telemetryData.flow || {}; + this.telemetryData.flow = { + ...prev, + remote: ff.remote, + local: ff.local || prev.local || null, + type: ff.proto || prev.type || 'TCP', + state_name: prev.state_name || 'ESTABLISHED', + state_code: prev.state_code || '01', + }; + } + + maybeApplyFlowFollowDeepLink() { + if (this._flowFollowApplied) return; + if (!this.flowFollow) this.flowFollow = this.parseFlowFollowFromUrl(); + if (!this.flowFollow || !this.telemetryData?.ip_map) return; + this._flowFollowApplied = true; + this.applyFlowFollowOverride(); + this.updateTelemetryUI(); + + const map = this.getIpMapData(); + const routes = Array.isArray(map.routes) ? map.routes : []; + const neigh = Array.isArray(map.neigh) ? map.neigh : []; + const routeIndex = routes.findIndex((r) => r.default); + const resolvedRoute = routeIndex >= 0 ? routes[routeIndex] : (routes[0] || null); + const nexthop = (resolvedRoute?.gateway && resolvedRoute.gateway !== '*') + ? resolvedRoute.gateway + : this.flowFollow.ip; + let neighIndex = neigh.findIndex((n) => n.ip === this.flowFollow.ip); + if (neighIndex < 0) neighIndex = neigh.findIndex((n) => n.ip === nexthop); + + const target = { + kind: 'flow', + ip: this.flowFollow.ip, + port: this.flowFollow.port, + routeIndex: routeIndex >= 0 ? routeIndex : (routes.length ? 0 : -1), + neighIndex, + follow: true, + }; + // Defer one frame so overlay UI nodes exist after activate/init. + requestAnimationFrame(() => this.openIpKernelMorph(target)); + } + + resolveMorphContext(target) { + const map = this.getIpMapData(); + const routes = Array.isArray(map.routes) ? map.routes : []; + const neigh = Array.isArray(map.neigh) ? map.neigh : []; + const flow = this.telemetryData?.flow || {}; + const flowRemote = String(flow.remote || ''); + const parsedFlow = this.parseEndpoint(flowRemote); + const flowIp = parsedFlow.ip; + const flowPortRaw = parsedFlow.port; + + let kind = target?.kind || 'route'; + let ip = target?.ip || ''; + let port = target?.port != null ? target.port : (Number(flowPortRaw) || 443); + let route = null; + let hop = null; + + if (kind === 'route' && Number.isFinite(Number(target?.index))) { + route = routes[Number(target.index)] || null; + } else if (kind === 'neigh' && Number.isFinite(Number(target?.index))) { + hop = neigh[Number(target.index)] || null; + ip = hop?.ip || ip; + } else if (kind === 'flow') { + ip = ip || flowIp; + } + + if (Number.isFinite(Number(target?.routeIndex)) && Number(target.routeIndex) >= 0) { + route = routes[Number(target.routeIndex)] || route; + } + if (Number.isFinite(Number(target?.neighIndex)) && Number(target.neighIndex) >= 0) { + hop = neigh[Number(target.neighIndex)] || hop; + } + + if (!route && ip) { + // Prefer default route for remote destinations; else first matching iface route. + route = routes.find((r) => r.default) || routes[0] || null; + } + if (!ip) { + if (route?.gateway && route.gateway !== '*') ip = route.gateway; + else if (hop?.ip) ip = hop.ip; + else if (flowIp) ip = flowIp; + } + if (!hop && ip) { + hop = neigh.find((n) => n.ip === ip) || null; + } + // For gateway routes, neigh is usually the gateway, not the remote. + const nexthopIp = (route?.gateway && route.gateway !== '*') ? route.gateway : ip; + if (!hop && nexthopIp) { + hop = neigh.find((n) => n.ip === nexthopIp) || null; + } + + const routeIndex = route ? routes.indexOf(route) : -1; + const neighIndex = hop ? neigh.indexOf(hop) : -1; + + return { + kind, + ip: ip || nexthopIp || '0.0.0.0', + port, + route, + hop, + nexthopIp: nexthopIp || ip || '0.0.0.0', + iface: hop?.iface || route?.iface || '?', + flow, + routeIndex, + neighIndex, + }; + } + + buildIpKernelMorphHtml(target) { + const ctx = this.resolveMorphContext(target); + const addr = this.ipv4ToBe32(ctx.ip); + const nhAddr = this.ipv4ToBe32(ctx.nexthopIp); + const port = this.portToBe16(ctx.port); + const ha = this.macToHa(ctx.hop?.mac); + const route = ctx.route || {}; + const ttl = Number(this.getIpMapData()?.ip?.default_ttl) || 64; + const destLabel = route.default + ? 'default' + : (route.destination || ctx.ip); + const gwLabel = route.gateway && route.gateway !== '*' ? route.gateway : 'on-link'; + const fibType = route.default ? 'RTN_UNICAST (default)' : 'RTN_UNICAST'; + const neighState = ctx.hop?.state || 'INCOMPLETE'; + const esc = (v) => this.escapeHtml(v); + + const step = (sym, title, body, accent) => ` +
+
${esc(title)}
+
${body}
+
${esc(sym)}
+
`; + + const arrow = `
`; + + return ` +
+
+
+
IP → KERNEL TRANSLATION
+
address is not a string in the kernel — it becomes integers, fib_result, neighbour, dst
+
+ +
+
+ ${step('inet_pton / sockaddr_in', '1 · SOCKET ADDR', ` + ${esc(ctx.ip)}:${esc(port.port)}
+ sin_addr / sin_port + `, 'rgba(212,221,231,0.28)')} + ${arrow} + ${step('__be32 / htons', '2 · WIRE INTEGERS', ` + ${esc(addr?.hex || '0x????????')} + : + ${esc(port.hex)}
+ ${esc(addr?.bytes || '')} · port be16 + `, 'rgba(103,190,224,0.4)')} + ${arrow} + ${step('fib_table_lookup', '3 · fib_result', ` + dest ${esc(destLabel)}
+ nh/gw ${esc(gwLabel)} + · oif ${esc(ctx.iface)}
+ ${esc(fibType)} · metric ${esc(route.metric ?? '—')} · nh ${esc(nhAddr?.hex || '—')} + `, 'rgba(230,193,90,0.45)')} + ${arrow} + ${step('neigh_resolve_output', '4 · neighbour / dst', ` + nexthop ${esc(ctx.nexthopIp)}
+ ha[] ${esc(ha.ha)}
+ nud=${esc(neighState)} · dst_entry → ha + `, 'rgba(150,255,190,0.35)')} + ${arrow} + ${step('dev_queue_xmit', '5 · sk_buff → NIC', ` + skb → dev ${esc(ctx.iface)} ring
+ eth hdr · TTL ${esc(ttl)} · qdisc / NAPI path + `, 'rgba(103,190,224,0.35)')} +
+
+ symbols: ip_route_output_key → fib_table_lookup → neigh_lookup → neigh_resolve_output → dev_queue_xmit +
+
`; + } + + animateIpMorph(root) { + if (!root) return; + const steps = [...root.querySelectorAll('.ns-ip-morph-step')]; + const arrows = [...root.querySelectorAll('.ns-ip-morph-arrow')]; + steps.forEach((el, i) => { + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + if (arrows[i]) arrows[i].style.opacity = '1'; + }, 90 + i * 160); + }); + } + + clearKernelGhost() { + if (this._ghostTimer) { + clearTimeout(this._ghostTimer); + this._ghostTimer = null; + } + if (this._ghostFadeTimer) { + clearTimeout(this._ghostFadeTimer); + this._ghostFadeTimer = null; + } + if (this._ghostEl) { + this._ghostEl.remove(); + this._ghostEl = null; + } + } + + flashKernelGhost(anchorEl, code = 'fib_lookup(fl4)') { + if (!anchorEl || typeof anchorEl.getBoundingClientRect !== 'function') return; + this.clearKernelGhost(); + const rect = anchorEl.getBoundingClientRect(); + if (!rect || rect.width < 2) return; + + const el = document.createElement('div'); + el.className = 'ns-kernel-ghost'; + el.textContent = String(code || 'fib_lookup(fl4)'); + const preferLeft = rect.right > window.innerWidth - 220; + const left = preferLeft + ? Math.max(8, rect.left - 12) + : Math.min(window.innerWidth - 12, rect.right + 10); + const top = Math.max(8, rect.top + rect.height / 2 - 12); + el.style.cssText = [ + 'position:fixed', + `left:${left}px`, + `top:${top}px`, + preferLeft ? 'transform:translate(-100%,0) translateX(6px)' : 'transform:translateX(-6px)', + 'opacity:0', + 'pointer-events:none', + 'z-index:10050', + 'font:12px "Share Tech Mono", monospace', + 'letter-spacing:0.4px', + 'color:rgba(230,193,90,0.88)', + 'text-shadow:0 0 10px rgba(230,193,90,0.35)', + 'background:rgba(8,12,20,0.55)', + 'border:1px solid rgba(230,193,90,0.28)', + 'border-radius:4px', + 'padding:3px 8px', + 'white-space:nowrap', + 'transition:opacity 220ms ease, transform 220ms ease', + ].join(';'); + document.body.appendChild(el); + this._ghostEl = el; + + const reveal = () => { + if (this._ghostEl !== el) return; + el.style.opacity = '1'; + el.style.transform = preferLeft ? 'translate(-100%,0) translateX(0)' : 'translateX(0)'; + }; + requestAnimationFrame(reveal); + setTimeout(reveal, 32); + + this._ghostTimer = setTimeout(() => { + if (this._ghostEl !== el) return; + el.style.opacity = '0'; + el.style.transform = preferLeft + ? 'translate(-100%,0) translateX(-8px)' + : 'translateX(8px)'; + this._ghostFadeTimer = setTimeout(() => { + if (this._ghostEl === el) { + el.remove(); + this._ghostEl = null; + } + }, 260); + }, 1700); + } + + kernelGhostCodeForIpTarget(target) { + const t = target || this.ipMorphTarget || {}; + if (t.kind === 'neigh') return 'neigh_lookup(&key)'; + if (t.kind === 'route') return 'fib_lookup(fl4)'; + if (t.kind === 'flow') return 'ip_route_output_key()'; + return 'fib_lookup(fl4)'; + } + + flashKernelGhostForIpTarget(root) { + const t = this.ipMorphTarget; + if (!t || !root) return; + let anchor = null; + if (t.kind === 'route' && Number.isFinite(Number(t.index))) { + anchor = root.querySelector(`.ns-ip-route-row[data-route-index="${Number(t.index)}"]`); + } else if (t.kind === 'neigh' && Number.isFinite(Number(t.index))) { + anchor = root.querySelector(`.ns-ip-neigh-row[data-neigh-index="${Number(t.index)}"]`); + } else if (t.kind === 'flow') { + if (Number.isFinite(Number(t.neighIndex)) && Number(t.neighIndex) >= 0) { + anchor = root.querySelector(`.ns-ip-neigh-row[data-neigh-index="${Number(t.neighIndex)}"]`); + } + if (!anchor && Number.isFinite(Number(t.routeIndex)) && Number(t.routeIndex) >= 0) { + anchor = root.querySelector(`.ns-ip-route-row[data-route-index="${Number(t.routeIndex)}"]`); + } + } + if (!anchor) { + anchor = root.querySelector('.ns-ip-route-row, .ns-ip-neigh-row, .ns-ip-morph'); + } + this.flashKernelGhost(anchor, this.kernelGhostCodeForIpTarget(t)); + } + + bindIpMapInteractions(root) { + if (!root) return; + root.querySelectorAll('.ns-ip-route-row').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + const index = Number(el.getAttribute('data-route-index')); + this.openIpKernelMorph({ kind: 'route', index }); + }); + }); + root.querySelectorAll('.ns-ip-neigh-row').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + const index = Number(el.getAttribute('data-neigh-index')); + this.openIpKernelMorph({ kind: 'neigh', index }); + }); + }); + const back = root.querySelector('.ns-ip-back-puzzle'); + if (back) { + back.addEventListener('click', () => { + this.ipMapPinned = false; + this.ipMorphTarget = null; + this.ipMapFrozen = null; + this._ipMapPanelSig = null; + if (this.drillLayerId === 'ip') this.closeLayerDrilldown(); + else this.updatePacketLifecycleUI(); + }); + } + const morphClose = root.querySelector('.ns-ip-morph-close'); + if (morphClose) { + morphClose.addEventListener('click', (e) => { + e.stopPropagation(); + // Close ribbon only: restore right panel to pre-morph IP map (no ribbon). + this.ipMorphTarget = null; + this._ipMapPanelSig = null; + this.renderIpLayerMapPanel(); + this.refreshIpMapViews({ drillOnly: true }); + }); + } + const morphRoot = root.querySelector('.ns-ip-morph'); + if (morphRoot) this.animateIpMorph(morphRoot); + } + + refreshIpMapViews({ drillOnly = false } = {}) { + if (!drillOnly && this.lifecyclePanelNode && (this.ipMapPinned || this.drillLayerId === 'ip')) { + this.renderIpLayerMapPanel(); + } + if (this.drillLayerId === 'ip' && this.drillScrim?.style.display === 'block') { + // Rebuild drill body map section without collapsing overlay. + const info = this.getLayerDrillInfo('ip'); + if (!info || !this.drillPanel) return; + const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.ip ?? 0))) * 100); + const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)'); + const metrics = this.getLayerDrillMetrics('ip'); + const metricCells = metrics.map(([k, v]) => ` +
+
${k}
+
${v}
+
`).join(''); + const morph = this.ipMorphTarget ? this.buildIpKernelMorphHtml(this.ipMorphTarget) : ` +
+ tip: click a route or neigh row to watch IP → kernel translation +
`; + const html = ` +
+
+
STACK LAYER · NETWORK · L04
+
IP · FIB / ROUTE / NEIGH / ICMP
+
+
+
LIVE ACTIVITY
+
${act}%
+
+
+
+
+
${metricCells}
+
${info.what}
+
WATCH · ${info.watch}
+ ${morph} + ${this.buildIpLayerMapHtml({ compact: false })} +
`; + window.setSafeHtml(this.drillPanel, html); + const closeBtn = this.drillPanel.querySelector('.ns-ov-close'); + if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown()); + this.bindIpMapInteractions(this.drillPanel); + if (this._ghostPending) { + this._ghostPending = false; + requestAnimationFrame(() => this.flashKernelGhostForIpTarget(this.drillPanel)); + } + } + } + + openIpKernelMorph(target) { + this.freezeIpMapSnapshot(); + const next = target || { kind: 'flow' }; + // For flow follow, attach route/neigh indexes so the map rows light up. + if (next.kind === 'flow' && (next.routeIndex == null || next.neighIndex == null)) { + const ctx = this.resolveMorphContext(next); + if (next.routeIndex == null) next.routeIndex = ctx.routeIndex; + if (next.neighIndex == null) next.neighIndex = ctx.neighIndex; + if (!next.ip) next.ip = ctx.ip; + if (next.port == null) next.port = ctx.port; + } + this.ipMorphTarget = next; + this.ipMapPinned = true; + this._ipMapPanelSig = null; + this._ghostPending = true; + // Morph lives only in the center overlay — keep the right panel as the + // compact IP map (pre-ribbon layout) so it still fits. + if (this.lifecyclePanelNode) this.renderIpLayerMapPanel(); + if (this.drillLayerId !== 'ip') { + this.openLayerDrilldown('ip'); + return; + } + this.refreshIpMapViews({ drillOnly: true }); + } + buildIpLayerMapHtml({ compact = false } = {}) { const map = this.getIpMapData(); const routes = Array.isArray(map.routes) ? map.routes : []; @@ -2962,31 +3735,43 @@ class NetworkStackVisualization { const ip = map.ip || {}; const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0); const fwd = n(ip.forwarding) === 1 ? 'forwarding ON' : 'host (no forward)'; + let selectedRoute = this.ipMorphTarget?.kind === 'route' ? Number(this.ipMorphTarget.index) : -1; + let selectedNeigh = this.ipMorphTarget?.kind === 'neigh' ? Number(this.ipMorphTarget.index) : -1; + if (this.ipMorphTarget?.kind === 'flow') { + if (Number.isFinite(Number(this.ipMorphTarget.routeIndex))) { + selectedRoute = Number(this.ipMorphTarget.routeIndex); + } + if (Number.isFinite(Number(this.ipMorphTarget.neighIndex))) { + selectedNeigh = Number(this.ipMorphTarget.neighIndex); + } + } const routeRows = routes.length - ? routes.map((r) => { + ? routes.map((r, idx) => { const dest = r.default ? 'default' - : `${r.destination}`; + : `${this.escapeHtml(r.destination)}`; const via = r.gateway && r.gateway !== '*' - ? `via ${r.gateway}` + ? `via ${this.escapeHtml(r.gateway)}` : 'on-link'; - return `
+ const active = idx === selectedRoute; + return `
${dest} ${via} - dev ${r.iface} + dev ${this.escapeHtml(r.iface)} metric ${r.metric ?? 0}
`; }).join('') : '
no routes in /proc/net/route
'; const neighRows = neigh.length - ? neigh.map((h) => { + ? neigh.map((h, idx) => { const st = String(h.state || 'STALE'); const stCol = st === 'REACHABLE' ? '#96ffbe' : (st === 'INCOMPLETE' ? '#e69696' : '#e6c15a'); - return `
- ${h.ip} - ${h.mac} - dev ${h.iface} - ${st} + const active = idx === selectedNeigh; + return `
+ ${this.escapeHtml(h.ip)} + ${this.escapeHtml(h.mac)} + dev ${this.escapeHtml(h.iface)} + ${this.escapeHtml(st)}
`; }).join('') : '
no ARP/neigh entries
'; @@ -2996,6 +3781,10 @@ class NetworkStackVisualization {
${value}
`; const icmpHot = n(icmp.in_errors) + n(icmp.out_errors) + n(icmp.in_dest_unreach) > 0; + // Morph ribbon is center-overlay only — never inject into the right panel. + const tip = compact + ? '
click route/neigh → IP→kernel morph (center)
' + : ''; const flowDiagram = `
LOOKUP @@ -3016,14 +3805,15 @@ class NetworkStackVisualization {
${compact ? `` : ''}
+ ${tip} ${flowDiagram}
-
FIB / ROUTES
+
FIB / ROUTES · click
${routeRows}
-
NEIGH / ARP
+
NEIGH / ARP · click
${neighRows}
@@ -3043,38 +3833,250 @@ class NetworkStackVisualization { renderIpLayerMapPanel() { if (!this.lifecyclePanelNode) return; window.setSafeHtml(this.lifecyclePanelNode, this.buildIpLayerMapHtml({ compact: true })); - const back = this.lifecyclePanelNode.querySelector('.ns-ip-back-puzzle'); + this.bindIpMapInteractions(this.lifecyclePanelNode); + } + + getTcpSnap() { + if (this.tcpFrozen) return this.tcpFrozen; + const m = this.telemetryData?.layer_metrics?.tcp_udp || {}; + const b = this.telemetryData?.bbr || {}; + const flow = this.telemetryData?.flow || {}; + return { tcp_udp: m, bbr: b, flow }; + } + + freezeTcpSnapshot() { + const snap = { + tcp_udp: { ...(this.telemetryData?.layer_metrics?.tcp_udp || {}) }, + bbr: { ...(this.telemetryData?.bbr || {}) }, + flow: { ...(this.telemetryData?.flow || {}) }, + }; + try { + this.tcpFrozen = JSON.parse(JSON.stringify(snap)); + } catch (_) { + this.tcpFrozen = snap; + } + } + + buildTcpKernelMorphHtml(target = {}) { + const snap = this.getTcpSnap(); + const t = snap.tcp_udp || {}; + const b = snap.bbr || {}; + const flow = snap.flow || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const cwnd = n(t.cwnd ?? b.cwnd); + const mss = n(b.mss, 1448); + const rtt = n(t.rtt_ms ?? b.rtt_ms); + const minRtt = n(t.min_rtt_ms ?? b.min_rtt_ms, rtt); + const retrans = n(t.retrans_per_sec); + const cc = String(t.cc || b.cc || 'unknown'); + const pacing = n(b.pacing_rate_mbps ?? t.delivery_rate_mbps); + const delivery = n(t.delivery_rate_mbps ?? b.delivery_rate_mbps); + const inflightBytes = cwnd * mss; + const inflightKb = inflightBytes >= 1024 ? `${(inflightBytes / 1024).toFixed(1)} KiB` : `${inflightBytes} B`; + const local = flow.local || 'local'; + const remote = flow.remote || 'remote'; + const state = flow.state_name || 'ESTABLISHED'; + const focus = target.focus || 'flow'; + const hi = (key, accent) => (focus === key ? accent : 'rgba(96,110,128,0.32)'); + + const step = (sym, title, body, accent) => ` +
+
${esc(title)}
+
${body}
+
${esc(sym)}
+
`; + const arrow = `
`; + + return ` +
+
+
+
TCP → KERNEL TRANSLATION
+
a flow is not just “connected” — it is tcp_sock fields deciding what may leave the stack
+
+ +
+
+ ${step('tcp_v4_connect / tcp_rcv_established', '1 · FLOW', ` + ${esc(local)} + + ${esc(remote)}
+ state ${esc(state)} · sk → tcp_sock + `, hi('flow', 'rgba(212,221,231,0.4)'))} + ${arrow} + ${step('tcp_ack / tcp_rtt_estimator', '2 · tcp_sock', ` + snd_cwnd ${esc(cwnd)} + · srtt ≈ ${esc(rtt.toFixed(1))} ms
+ minrtt ${esc(minRtt.toFixed(2))} · retrans/s ${esc(retrans)} + `, focus === 'rtt' + ? 'rgba(103,190,224,0.5)' + : (focus === 'cwnd' || focus === 'retrans' ? 'rgba(230,193,90,0.5)' : 'rgba(96,110,128,0.32)'))} + ${arrow} + ${step('tcp_snd_wnd / tcp_tso_should_defer', '3 · SEND BUDGET', ` + cwnd×MSS = ${esc(inflightKb)} + (${esc(cwnd)} × ${esc(mss)})
+ bytes allowed in flight before ACK + `, hi('budget', 'rgba(150,255,190,0.4)'))} + ${arrow} + ${step('tcp_cong_control / ca_ops', '4 · CONGESTION CTL', ` + cc ${esc(cc)} + · pace ${esc(pacing.toFixed(3))} Mbps
+ delivery ${esc(delivery.toFixed(3))} Mbps · tp->ca_ops + `, hi('cc', 'rgba(230,193,90,0.45)'))} + ${arrow} + ${step('tcp_transmit_skb → ip_queue_xmit', '5 · sk_buff → IP', ` + skb leaves TCP → IP output
+ then FIB / neigh morph on L04 + `, hi('skb', 'rgba(103,190,224,0.4)'))} +
+
+ symbols: tcp_rcv_established → tcp_ack → tcp_cong_control → tcp_write_xmit → tcp_transmit_skb +
+
`; + } + + animateTcpMorph(root) { + if (!root) return; + const steps = [...root.querySelectorAll('.ns-tcp-morph-step')]; + const arrows = [...root.querySelectorAll('.ns-tcp-morph-arrow')]; + steps.forEach((el, i) => { + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + if (arrows[i]) arrows[i].style.opacity = '1'; + }, 90 + i * 160); + }); + } + + buildTcpLayerMapHtml({ compact = false } = {}) { + const snap = this.getTcpSnap(); + const t = snap.tcp_udp || {}; + const b = snap.bbr || {}; + const flow = snap.flow || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const cwnd = n(t.cwnd ?? b.cwnd); + const rtt = n(t.rtt_ms ?? b.rtt_ms); + const retrans = n(t.retrans_per_sec); + const cc = String(t.cc || b.cc || 'unknown'); + const focus = this.tcpMorphTarget?.focus || ''; + const row = (key, label, value, color) => { + const active = focus === key; + return `
+ ${label} + ${esc(value)} +
`; + }; + return ` +
+
+
TCP LAYER MAP · L05 · ${esc(cc)}
+
ss -tin · tcp_sock · ca_ops
+
+ ${compact ? `` : ''} +
+ ${compact ? '
click a field → TCP→kernel morph (center)
' : ''} +
+
+ ${row('flow', 'FLOW', `${flow.local || '—'} → ${flow.remote || '—'}`, '#d4dde7')} + ${row('cwnd', 'CWND', `${cwnd} segments`, '#e6c15a')} + ${row('rtt', 'RTT', `${rtt.toFixed(1)} ms`, '#a9d4e8')} + ${row('cc', 'CC', cc, '#96ffbe')} + ${row('retrans', 'RETRANS', `${retrans}/s`, retrans > 0 ? '#e69696' : '#8d99a7')} +
+
+
PATH
+
userspace send → tcp_sendmsg
+
→ window / cwnd check → tcp_write_xmit
+
→ skb build → tcp_transmit_skb
+
ip_queue_xmit (L04)
+ ${!compact ? `
+ + +
` : ''} +
+
`; + } + + bindTcpMapInteractions(root) { + if (!root) return; + root.querySelectorAll('.ns-tcp-row').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + const focus = el.getAttribute('data-tcp-focus') || 'flow'; + this.openTcpKernelMorph({ focus }); + }); + }); + const openMorph = root.querySelector('.ns-tcp-open-morph'); + if (openMorph) { + openMorph.addEventListener('click', (e) => { + e.stopPropagation(); + this.openTcpKernelMorph({ focus: 'flow' }); + }); + } + const back = root.querySelector('.ns-tcp-back-puzzle'); if (back) { back.addEventListener('click', () => { - this.ipMapPinned = false; - if (this.drillLayerId === 'ip') this.closeLayerDrilldown(); + this.tcpMapPinned = false; + this.tcpMorphTarget = null; + this.tcpFrozen = null; + if (this.drillLayerId === 'tcp') this.closeLayerDrilldown(); else this.updatePacketLifecycleUI(); }); } + const morphClose = root.querySelector('.ns-tcp-morph-close'); + if (morphClose) { + morphClose.addEventListener('click', (e) => { + e.stopPropagation(); + this.tcpMorphTarget = null; + this.renderTcpLayerMapPanel(); + this.refreshTcpMapViews({ drillOnly: true }); + }); + } + const bbrBtn = root.querySelector('.ns-drill-bbr'); + if (bbrBtn) { + bbrBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.closeLayerDrilldown(); + this.openBbrOverlay(); + }); + } + const morphRoot = root.querySelector('.ns-tcp-morph'); + if (morphRoot) this.animateTcpMorph(morphRoot); } - openLayerDrilldown(layerId) { - if (!this.drillScrim || !this.drillPanel) return; - const info = this.getLayerDrillInfo(layerId); - if (!info) return; - this.drillLayerId = layerId; + renderTcpLayerMapPanel() { + if (!this.lifecyclePanelNode) return; + window.setSafeHtml(this.lifecyclePanelNode, this.buildTcpLayerMapHtml({ compact: true })); + this.bindTcpMapInteractions(this.lifecyclePanelNode); + } - if (layerId === 'ip') { - this.ipMapPinned = true; - this.drillPanel.style.width = 'min(980px, 92vw)'; - const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.ip ?? 0))) * 100); + refreshTcpMapViews({ drillOnly = false } = {}) { + if (!drillOnly && this.lifecyclePanelNode && (this.tcpMapPinned || this.drillLayerId === 'tcp')) { + this.renderTcpLayerMapPanel(); + } + if (this.drillLayerId === 'tcp' && this.drillScrim?.style.display === 'block') { + const info = this.getLayerDrillInfo('tcp'); + if (!info || !this.drillPanel) return; + const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.tcp ?? 0))) * 100); const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)'); - const metrics = this.getLayerDrillMetrics('ip'); + const metrics = this.getLayerDrillMetrics('tcp'); const metricCells = metrics.map(([k, v]) => `
${k}
${v}
`).join(''); + const morph = this.tcpMorphTarget + ? this.buildTcpKernelMorphHtml(this.tcpMorphTarget) + : `
+ tip: click cwnd / rtt / cc — or open the translation ribbon +
`; const html = ` -
+
-
STACK LAYER · NETWORK · L04
-
IP · FIB / ROUTE / NEIGH / ICMP
+
STACK LAYER · TRANSPORT · L05
+
TCP · SOCK / CWND / CC / SKB
LIVE ACTIVITY
@@ -3085,19 +4087,682 @@ class NetworkStackVisualization {
${metricCells}
${info.what}
-
WATCH · ${info.watch}
- ${this.buildIpLayerMapHtml({ compact: false })} +
WATCH · ${info.watch}
+ ${morph} + ${this.buildTcpLayerMapHtml({ compact: false })}
`; window.setSafeHtml(this.drillPanel, html); const closeBtn = this.drillPanel.querySelector('.ns-ov-close'); if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown()); - this.renderIpLayerMapPanel(); + this.bindTcpMapInteractions(this.drillPanel); + } + } + + openTcpKernelMorph(target) { + this.freezeTcpSnapshot(); + this.tcpMorphTarget = target || { focus: 'flow' }; + this.tcpMapPinned = true; + // Clear other pins so the right panel shows TCP map only. + this.ipMapPinned = false; + this.ipMorphTarget = null; + this.nfMapPinned = false; + this.nfMorphTarget = null; + this.sockMapPinned = false; + this.sockMorphTarget = null; + if (this.lifecyclePanelNode) this.renderTcpLayerMapPanel(); + if (this.drillLayerId !== 'tcp') { + this.openLayerDrilldown('tcp'); + return; + } + this.refreshTcpMapViews({ drillOnly: true }); + } + + getNfSnap() { + if (this.nfFrozen) return this.nfFrozen; + return { + netfilter: { ...(this.telemetryData?.layer_metrics?.netfilter || {}) }, + flow: { ...(this.telemetryData?.flow || {}) }, + }; + } + + freezeNfSnapshot() { + const snap = this.getNfSnap(); + try { + this.nfFrozen = JSON.parse(JSON.stringify(snap)); + } catch (_) { + this.nfFrozen = snap; + } + } + + buildNfKernelMorphHtml(target = {}) { + const snap = this.getNfSnap(); + const nf = snap.netfilter || {}; + const flow = snap.flow || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const drops = n(nf.drop_per_sec); + const ratio = n(nf.drop_ratio); + const ct = n(nf.conntrack_count); + const ctMax = n(nf.conntrack_max); + const ctUsage = n(nf.conntrack_usage); + const engine = nf.nft ? 'nftables' : (nf.nf_conntrack ? 'iptables+ct' : 'netfilter'); + const verdict = drops > 0.5 || ratio > 0.01 ? 'NF_DROP pressure' : 'NF_ACCEPT path'; + const verdictCol = drops > 0.5 || ratio > 0.01 ? '#e69696' : '#96ffbe'; + const focus = target.focus || 'hook'; + const hi = (key, accent) => (focus === key ? accent : 'rgba(96,110,128,0.32)'); + const step = (sym, title, body, accent) => ( + '
' + + '
' + esc(title) + '
' + + '
' + body + '
' + + '
' + esc(sym) + '
' + + '
' + ); + const arrow = '
'; + const flowName = esc(flow.state_name || '-'); + const stepsHtml = [ + step( + 'NF_HOOK / nf_hook_slow', + '1 · HOOK', + 'PREROUTING → LOCAL_IN / FORWARD → POSTROUTING
' + + 'engine ' + esc(engine) + ' · flow ' + flowName + '', + hi('hook', 'rgba(232,96,104,0.45)') + ), + arrow, + step( + 'nf_conntrack_in', + '2 · CONNTRACK', + 'ct entries ' + esc(ct) + '' + + ' / ' + esc(ctMax || '-') + '' + + ' · ' + esc((ctUsage * 100).toFixed(2)) + '%
' + + 'tuple → nf_conn · NEW / ESTABLISHED', + hi('ct', 'rgba(103,190,224,0.45)') + ), + arrow, + step( + 'nft_do_chain / iptable_*', + '3 · CHAIN', + 'rules walk the skb at the active hook
' + + 'match → target (filter / nat / mangle)', + hi('chain', 'rgba(230,193,90,0.45)') + ), + arrow, + step( + 'nft_verdict / NF_DROP', + '4 · VERDICT', + '' + esc(verdict) + '
' + + 'drop/s ' + esc(drops) + ' · ratio ' + (ratio * 100).toFixed(2) + '%', + hi('verdict', 'rgba(232,96,104,0.5)') + ), + arrow, + step( + 'okfn / skb continue', + '5 · CONTINUE', + 'ACCEPT → stack continues · DROP → kfree_skb
' + + 'surviving skb → TCP/IP path', + hi('continue', 'rgba(150,255,190,0.35)') + ), + ].join(''); + + return ( + '
' + + '
' + + '
' + + '
NETFILTER → KERNEL TRANSLATION
' + + '
policy is not a black box — hooks, conntrack tuples, then a verdict on the skb
' + + '
' + + '' + + '
' + + '
' + + stepsHtml + + '
' + + '
' + + 'symbols: nf_hook_slow → nf_conntrack_in → nft_do_chain → nft_verdict → okfn' + + '
' + ); + } + + animateNfMorph(root) { + if (!root) return; + const steps = [...root.querySelectorAll('.ns-nf-morph-step')]; + const arrows = [...root.querySelectorAll('.ns-nf-morph-arrow')]; + steps.forEach((el, i) => { + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + if (arrows[i]) arrows[i].style.opacity = '1'; + }, 90 + i * 160); + }); + } + + buildNfLayerMapHtml({ compact = false } = {}) { + const snap = this.getNfSnap(); + const nf = snap.netfilter || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const drops = n(nf.drop_per_sec); + const ratio = n(nf.drop_ratio); + const ct = n(nf.conntrack_count); + const ctMax = n(nf.conntrack_max); + const engine = nf.nft ? 'nftables' : (nf.nf_conntrack ? 'iptables+ct' : 'netfilter'); + const focus = this.nfMorphTarget?.focus || ''; + const row = (key, label, value, color) => { + const active = focus === key; + return `
+ ${label} + ${esc(value)} +
`; + }; + return ` +
+
+
NETFILTER MAP · L03 · ${esc(engine)}
+
hooks · nf_conntrack · verdict
+
+ ${compact ? `` : ''} +
+ ${compact ? '
click a field → Netfilter→kernel morph (center)
' : ''} +
+
+ ${row('hook', 'HOOKS', 'PREROUTING…POSTROUTING', '#e69696')} + ${row('ct', 'CONNTRACK', `${ct} / ${ctMax || '—'}`, '#a9d4e8')} + ${row('verdict', 'DROPS', `${drops}/s · ${(ratio * 100).toFixed(2)}%`, drops > 0 ? '#e69696' : '#8d99a7')} + ${row('chain', 'ENGINE', engine, '#e6c15a')} +
+
+
RX PATH
+
NIC → PREROUTING
+
nf_conntrack_in
+
nft_do_chain
+
→ verdict → TCP/IP or drop
+ ${!compact ? `
+ +
` : ''} +
+
`; + } + + bindNfMapInteractions(root) { + if (!root) return; + root.querySelectorAll('.ns-nf-row').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + this.openNfKernelMorph({ focus: el.getAttribute('data-nf-focus') || 'hook' }); + }); + }); + const openMorph = root.querySelector('.ns-nf-open-morph'); + if (openMorph) { + openMorph.addEventListener('click', (e) => { + e.stopPropagation(); + this.openNfKernelMorph({ focus: 'hook' }); + }); + } + const back = root.querySelector('.ns-nf-back-puzzle'); + if (back) { + back.addEventListener('click', () => { + this.nfMapPinned = false; + this.nfMorphTarget = null; + this.nfFrozen = null; + if (this.drillLayerId === 'netfilter') this.closeLayerDrilldown(); + else this.updatePacketLifecycleUI(); + }); + } + const morphClose = root.querySelector('.ns-nf-morph-close'); + if (morphClose) { + morphClose.addEventListener('click', (e) => { + e.stopPropagation(); + this.nfMorphTarget = null; + this.renderNfLayerMapPanel(); + this.refreshNfMapViews({ drillOnly: true }); + }); + } + const morphRoot = root.querySelector('.ns-nf-morph'); + if (morphRoot) this.animateNfMorph(morphRoot); + } + + renderNfLayerMapPanel() { + if (!this.lifecyclePanelNode) return; + window.setSafeHtml(this.lifecyclePanelNode, this.buildNfLayerMapHtml({ compact: true })); + this.bindNfMapInteractions(this.lifecyclePanelNode); + } + + refreshNfMapViews({ drillOnly = false } = {}) { + if (!drillOnly && this.lifecyclePanelNode && (this.nfMapPinned || this.drillLayerId === 'netfilter')) { + this.renderNfLayerMapPanel(); + } + if (this.drillLayerId === 'netfilter' && this.drillScrim?.style.display === 'block') { + const info = this.getLayerDrillInfo('netfilter'); + if (!info || !this.drillPanel) return; + const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.netfilter ?? 0))) * 100); + const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)'); + const metrics = this.getLayerDrillMetrics('netfilter'); + const metricCells = metrics.map(([k, v]) => ` +
+
${k}
+
${v}
+
`).join(''); + const morph = this.nfMorphTarget + ? this.buildNfKernelMorphHtml(this.nfMorphTarget) + : `
+ tip: click hooks / conntrack / verdict +
`; + const html = ` +
+
+
STACK LAYER · FIREWALL · L03
+
NETFILTER · HOOK / CT / VERDICT
+
+
+
LIVE ACTIVITY
+
${act}%
+
+
+
+
+
${metricCells}
+
${info.what}
+
WATCH · ${info.watch}
+ ${morph} + ${this.buildNfLayerMapHtml({ compact: false })} +
`; + window.setSafeHtml(this.drillPanel, html); + const closeBtn = this.drillPanel.querySelector('.ns-ov-close'); + if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown()); + this.bindNfMapInteractions(this.drillPanel); + } + } + + openNfKernelMorph(target) { + this.freezeNfSnapshot(); + this.nfMorphTarget = target || { focus: 'hook' }; + this.nfMapPinned = true; + this.ipMapPinned = false; + this.ipMorphTarget = null; + this.tcpMapPinned = false; + this.tcpMorphTarget = null; + this.sockMapPinned = false; + this.sockMorphTarget = null; + if (this.lifecyclePanelNode) this.renderNfLayerMapPanel(); + if (this.drillLayerId !== 'netfilter') { + this.openLayerDrilldown('netfilter'); + return; + } + this.refreshNfMapViews({ drillOnly: true }); + } + + getSockSnap() { + if (this.sockFrozen) return this.sockFrozen; + return { + socket_api: { ...(this.telemetryData?.layer_metrics?.socket_api || {}) }, + flow: { ...(this.telemetryData?.flow || {}) }, + }; + } + + freezeSockSnapshot() { + const snap = this.getSockSnap(); + try { + this.sockFrozen = JSON.parse(JSON.stringify(snap)); + } catch (_) { + this.sockFrozen = snap; + } + } + + buildSockKernelMorphHtml(target = {}) { + const snap = this.getSockSnap(); + const sa = snap.socket_api || {}; + const flow = snap.flow || {}; + const ex = sa.example || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const fd = ex.fd != null ? ex.fd : (flow.fd != null ? flow.fd : null); + const inode = n(ex.inode != null ? ex.inode : flow.inode); + const pid = ex.pid != null ? ex.pid : (flow.pid != null ? flow.pid : null); + const proc = ex.process || flow.process || 'process'; + const local = ex.local || flow.local || 'local'; + const remote = ex.remote || flow.remote || 'remote'; + const state = ex.state_name || flow.state_name || 'ESTABLISHED'; + const est = n(sa.established); + const active = n(sa.active_sockets); + const focus = target.focus || 'fd'; + const hi = (key, accent) => (focus === key ? accent : 'rgba(96,110,128,0.32)'); + const fdLabel = fd != null ? String(fd) : 'fd?'; + const inodeLabel = inode > 0 ? String(inode) : 'inode?'; + const pidLabel = pid != null ? String(pid) : 'pid?'; + const skHint = inode > 0 ? ('sk@inode:' + inode) : 'struct sock *'; + + const step = (sym, title, body, accent) => ( + '
' + + '
' + esc(title) + '
' + + '
' + body + '
' + + '
' + esc(sym) + '
' + + '
' + ); + const arrow = '
'; + const stepsHtml = [ + step( + 'fget / fdtable', + '1 · FILE DESCRIPTOR', + 'fd ' + esc(fdLabel) + '' + + ' · pid ' + esc(pidLabel) + '' + + ' (' + esc(proc) + ')
' + + 'userspace handle → files_struct', + hi('fd', 'rgba(150,255,190,0.45)') + ), + arrow, + step( + 'SOCKET_I(inode)', + '2 · INODE → socket', + 'inode ' + esc(inodeLabel) + '' + + ' · uid ' + esc(ex.uid != null ? ex.uid : (flow.uid || 0)) + '
' + + 'file → dentry → inode → struct socket', + hi('inode', 'rgba(230,193,90,0.45)') + ), + arrow, + step( + 'sock_alloc / socket->sk', + '3 · struct sock *', + '' + esc(skHint) + '
' + + '' + esc(local) + '' + + '' + + '' + esc(remote) + '
' + + 'state ' + esc(state) + ' · protocol socket object', + hi('sk', 'rgba(103,190,224,0.5)') + ), + arrow, + step( + 'sk_receive_queue / sk_write_queue', + '4 · QUEUES', + 'active ' + esc(active) + '' + + ' · established ' + esc(est) + '
' + + 'recv/send buffers · accept backlog · wakeups', + hi('queue', 'rgba(150,255,190,0.35)') + ), + arrow, + step( + 'sock_sendmsg / sock_recvmsg', + '5 · SYSCALL PATH', + 'sendmsg / recvmsg copy between user pages and sk_buff
' + + 'fd stays the only handle the process ever sees', + hi('syscall', 'rgba(212,221,231,0.35)') + ), + ].join(''); + + return ( + '
' + + '
' + + '
' + + '
SOCKET → KERNEL TRANSLATION
' + + '
an fd is not the socket — it is a path: fd → file → inode → socket → sock*
' + + '
' + + '' + + '
' + + '
' + + stepsHtml + + '
' + + '
' + + 'symbols: fget → SOCKET_I → socket->sk → sk_*_queue → sock_sendmsg/recvmsg' + + '
' + ); + } + + animateSockMorph(root) { + if (!root) return; + const steps = [...root.querySelectorAll('.ns-sock-morph-step')]; + const arrows = [...root.querySelectorAll('.ns-sock-morph-arrow')]; + steps.forEach((el, i) => { + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + if (arrows[i]) arrows[i].style.opacity = '1'; + }, 90 + i * 160); + }); + } + + buildSockLayerMapHtml({ compact = false } = {}) { + const snap = this.getSockSnap(); + const sa = snap.socket_api || {}; + const flow = snap.flow || {}; + const ex = sa.example || {}; + const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d); + const esc = (v) => this.escapeHtml(v); + const fd = ex.fd != null ? ex.fd : flow.fd; + const inode = n(ex.inode != null ? ex.inode : flow.inode); + const pid = ex.pid != null ? ex.pid : flow.pid; + const proc = ex.process || flow.process || '—'; + const local = ex.local || flow.local || '—'; + const remote = ex.remote || flow.remote || '—'; + const state = ex.state_name || flow.state_name || '—'; + const focus = this.sockMorphTarget?.focus || ''; + const row = (key, label, value, color) => { + const active = focus === key; + return ( + '
' + + '' + label + '' + + '' + esc(value) + '' + + '
' + ); + }; + return ( + '
' + + '
' + + '
SOCKET MAP · L06 · fd → sock*
' + + '
files_struct · inode · struct sock
' + + '
' + + (compact + ? '' + : '') + + '
' + + (compact ? '
click a field → Socket→kernel morph (center)
' : '') + + '
' + + '
' + + row('fd', 'FD', fd != null ? (fd + ' · pid ' + (pid != null ? pid : '?')) : 'n/a (no process match)', '#96ffbe') + + row('inode', 'INODE', inode > 0 ? String(inode) : 'n/a', '#e6c15a') + + row('sk', 'SOCK*', local + ' → ' + remote, '#a9d4e8') + + row('queue', 'QUEUES', 'est ' + n(sa.established) + ' / active ' + n(sa.active_sockets), '#96ffbe') + + row('syscall', 'STATE', state + ' · ' + proc, '#d4dde7') + + '
' + + '
' + + '
LOOKUP PATH
' + + '
userspace fd
' + + '
inode / SOCKET_I
' + + '
struct sock *
' + + '
→ queues → TCP/UDP
' + + (!compact + ? '
' + + '' + + '
' + : '') + + '
' + ); + } + + bindSockMapInteractions(root) { + if (!root) return; + root.querySelectorAll('.ns-sock-row').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + this.openSockKernelMorph({ focus: el.getAttribute('data-sock-focus') || 'fd' }); + }); + }); + const openMorph = root.querySelector('.ns-sock-open-morph'); + if (openMorph) { + openMorph.addEventListener('click', (e) => { + e.stopPropagation(); + this.openSockKernelMorph({ focus: 'fd' }); + }); + } + const back = root.querySelector('.ns-sock-back-puzzle'); + if (back) { + back.addEventListener('click', () => { + this.sockMapPinned = false; + this.sockMorphTarget = null; + this.sockFrozen = null; + if (this.drillLayerId === 'socket') this.closeLayerDrilldown(); + else this.updatePacketLifecycleUI(); + }); + } + const morphClose = root.querySelector('.ns-sock-morph-close'); + if (morphClose) { + morphClose.addEventListener('click', (e) => { + e.stopPropagation(); + this.sockMorphTarget = null; + this.renderSockLayerMapPanel(); + this.refreshSockMapViews({ drillOnly: true }); + }); + } + const morphRoot = root.querySelector('.ns-sock-morph'); + if (morphRoot) this.animateSockMorph(morphRoot); + } + + renderSockLayerMapPanel() { + if (!this.lifecyclePanelNode) return; + window.setSafeHtml(this.lifecyclePanelNode, this.buildSockLayerMapHtml({ compact: true })); + this.bindSockMapInteractions(this.lifecyclePanelNode); + } + + refreshSockMapViews({ drillOnly = false } = {}) { + if (!drillOnly && this.lifecyclePanelNode && (this.sockMapPinned || this.drillLayerId === 'socket')) { + this.renderSockLayerMapPanel(); + } + if (this.drillLayerId === 'socket' && this.drillScrim?.style.display === 'block') { + const info = this.getLayerDrillInfo('socket'); + if (!info || !this.drillPanel) return; + const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.socket ?? 0))) * 100); + const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)'); + const metrics = this.getLayerDrillMetrics('socket'); + const metricCells = metrics.map(([k, v]) => ( + '
' + + '
' + this.escapeHtml(k) + '
' + + '
' + this.escapeHtml(v) + '
' + + '
' + )).join(''); + const morph = this.sockMorphTarget + ? this.buildSockKernelMorphHtml(this.sockMorphTarget) + : '
' + + 'tip: click fd / inode / sock*' + + '
'; + const html = ( + '
' + + '
' + + '
STACK LAYER · SOCKET API · L06
' + + '
SOCKET · FD / INODE / SOCK*
' + + '
' + + '
' + + '
LIVE ACTIVITY
' + + '
' + act + '%
' + + '
' + + '
' + + '
' + + '
' + + '
' + metricCells + '
' + + '
' + info.what + '
' + + '
WATCH · ' + info.watch + '
' + + morph + + this.buildSockLayerMapHtml({ compact: false }) + + '
' + ); + window.setSafeHtml(this.drillPanel, html); + const closeBtn = this.drillPanel.querySelector('.ns-ov-close'); + if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown()); + this.bindSockMapInteractions(this.drillPanel); + } + } + + openSockKernelMorph(target) { + this.freezeSockSnapshot(); + this.sockMorphTarget = target || { focus: 'fd' }; + this.sockMapPinned = true; + this.ipMapPinned = false; + this.ipMorphTarget = null; + this.tcpMapPinned = false; + this.tcpMorphTarget = null; + this.nfMapPinned = false; + this.nfMorphTarget = null; + if (this.lifecyclePanelNode) this.renderSockLayerMapPanel(); + if (this.drillLayerId !== 'socket') { + this.openLayerDrilldown('socket'); + return; + } + this.refreshSockMapViews({ drillOnly: true }); + } + + clearLayerPinsExcept(keep) { + if (keep !== 'ip') { + this.ipMapPinned = false; + this.ipMorphTarget = null; + this.ipMapFrozen = null; + } + if (keep !== 'tcp') { + this.tcpMapPinned = false; + this.tcpMorphTarget = null; + this.tcpFrozen = null; + } + if (keep !== 'netfilter') { + this.nfMapPinned = false; + this.nfMorphTarget = null; + this.nfFrozen = null; + } + if (keep !== 'socket') { + this.sockMapPinned = false; + this.sockMorphTarget = null; + this.sockFrozen = null; + } + } + + openLayerDrilldown(layerId) { + if (!this.drillScrim || !this.drillPanel) return; + const info = this.getLayerDrillInfo(layerId); + if (!info) return; + this.drillLayerId = layerId; + + if (layerId === 'ip') { + this.clearLayerPinsExcept('ip'); + this.ipMapPinned = true; + if (!this.ipMapFrozen) this.freezeIpMapSnapshot(); + this.drillPanel.style.width = 'min(980px, 92vw)'; this.drillScrim.style.display = 'block'; if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; + this.refreshIpMapViews(); return; } - this.ipMapPinned = false; + if (layerId === 'tcp') { + this.clearLayerPinsExcept('tcp'); + this.tcpMapPinned = true; + if (!this.tcpFrozen) this.freezeTcpSnapshot(); + this.drillPanel.style.width = 'min(980px, 92vw)'; + this.drillScrim.style.display = 'block'; + if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; + this.refreshTcpMapViews(); + return; + } + + if (layerId === 'netfilter') { + this.clearLayerPinsExcept('netfilter'); + this.nfMapPinned = true; + if (!this.nfFrozen) this.freezeNfSnapshot(); + this.drillPanel.style.width = 'min(980px, 92vw)'; + this.drillScrim.style.display = 'block'; + if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; + this.refreshNfMapViews(); + return; + } + + if (layerId === 'socket') { + this.clearLayerPinsExcept('socket'); + this.sockMapPinned = true; + if (!this.sockFrozen) this.freezeSockSnapshot(); + this.drillPanel.style.width = 'min(980px, 92vw)'; + this.drillScrim.style.display = 'block'; + if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; + this.refreshSockMapViews(); + return; + } + + this.clearLayerPinsExcept(null); this.drillPanel.style.width = 'min(680px, 78vw)'; const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity[layerId] ?? 0))) * 100); const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)'); @@ -3130,25 +4795,59 @@ class NetworkStackVisualization {
KEY SUBSYSTEMS
${subs}
- ${layerId === 'tcp' ? `
▸ OPEN TCP BBR PATH MODEL
` : ''}
`; window.setSafeHtml(this.drillPanel, html); const closeBtn = this.drillPanel.querySelector('.ns-ov-close'); if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown()); - const bbrBtn = this.drillPanel.querySelector('.ns-drill-bbr'); - if (bbrBtn) bbrBtn.addEventListener('click', () => { this.closeLayerDrilldown(); this.openBbrOverlay(); }); this.drillScrim.style.display = 'block'; if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; } closeLayerDrilldown() { const wasIp = this.drillLayerId === 'ip'; + const wasTcp = this.drillLayerId === 'tcp'; + const wasNf = this.drillLayerId === 'netfilter'; + const wasSock = this.drillLayerId === 'socket'; + this.clearKernelGhost(); + this._ghostPending = false; this.drillLayerId = null; + this.ipMorphTarget = null; + this.ipMapFrozen = null; + this.tcpMorphTarget = null; + this.tcpFrozen = null; + this.nfMorphTarget = null; + this.nfFrozen = null; + this.sockMorphTarget = null; + this.sockFrozen = null; if (this.drillScrim) this.drillScrim.style.display = 'none'; if (this.drillPanel) this.drillPanel.style.width = 'min(680px, 78vw)'; - // Keep IP map pinned in the bottom panel after closing the overlay - // until the user hits ← PUZZLE or opens another layer. - if (wasIp) this.renderIpLayerMapPanel(); + // After closing center overlay: keep compact layer map on the right + // (pre-ribbon layout). ← PUZZLE restores the puzzle architecture. + if (wasIp) { + this.ipMapPinned = true; + this.tcpMapPinned = false; + this.nfMapPinned = false; + this.sockMapPinned = false; + this.renderIpLayerMapPanel(); + } else if (wasTcp) { + this.tcpMapPinned = true; + this.ipMapPinned = false; + this.nfMapPinned = false; + this.sockMapPinned = false; + this.renderTcpLayerMapPanel(); + } else if (wasNf) { + this.nfMapPinned = true; + this.ipMapPinned = false; + this.tcpMapPinned = false; + this.sockMapPinned = false; + this.renderNfLayerMapPanel(); + } else if (wasSock) { + this.sockMapPinned = true; + this.ipMapPinned = false; + this.tcpMapPinned = false; + this.nfMapPinned = false; + this.renderSockLayerMapPanel(); + } } // Push the latest BBR sample into a rolling window (used to estimate the @@ -3388,7 +5087,7 @@ class NetworkStackVisualization { const dt = Math.min(0.04, (now - this.lastFrameTime) / 1000); this.lastFrameTime = now; - if (!this.hideVerticalOrbs) { + if (!this.hideVerticalOrbs || this.packetMorphEnabled) { this.updatePacket(dt); } this.updateEffects(dt); diff --git a/templates/linux-network-subsystem.html b/templates/linux-network-subsystem.html index 887d850..af7c04c 100644 --- a/templates/linux-network-subsystem.html +++ b/templates/linux-network-subsystem.html @@ -101,19 +101,30 @@

TCP BBR Path Model

online from the traffic itself.

+
- +