diff --git a/kernel_ai/services/network.py b/kernel_ai/services/network.py index f39d6eb..1f6325c 100644 --- a/kernel_ai/services/network.py +++ b/kernel_ai/services/network.py @@ -282,6 +282,123 @@ def _rate_to_mbps(value, unit): return v * scale +def _hex_le_ipv4(hex_str: str) -> str: + raw = (hex_str or "").strip() + if len(raw) != 8: + return "0.0.0.0" + try: + parts = [raw[i : i + 2] for i in range(0, 8, 2)] + parts.reverse() + return ".".join(str(int(b, 16)) for b in parts) + except ValueError: + return "0.0.0.0" + + +def _mask_to_prefix(mask_hex: str) -> int: + try: + value = int(mask_hex, 16) + except ValueError: + return 0 + return bin(value).count("1") + + +def get_ip_layer_map(limit_routes: int = 12, limit_neigh: int = 10) -> dict: + """Live IP-layer map: FIB routes, ARP/neigh, ICMP + IP SNMP counters.""" + routes: list[dict] = [] + try: + with open("/proc/net/route", "r", encoding="utf-8", errors="ignore") as fh: + for line in fh.readlines()[1:]: + parts = line.strip().split() + if len(parts) < 8: + continue + iface, dest_h, gw_h, flags_h = parts[0], parts[1], parts[2], parts[3] + metric = int(parts[6]) if parts[6].isdigit() else 0 + mask_h = parts[7] + try: + flags = int(flags_h, 16) + except ValueError: + flags = 0 + if not (flags & 0x1): # RTF_UP + continue + dest = _hex_le_ipv4(dest_h) + gateway = _hex_le_ipv4(gw_h) + prefix = _mask_to_prefix(mask_h) + is_default = dest_h == "00000000" and (flags & 0x2) + routes.append( + { + "iface": iface, + "destination": "default" if is_default else f"{dest}/{prefix}", + "gateway": gateway if gateway != "0.0.0.0" else "*", + "metric": metric, + "flags": flags, + "default": bool(is_default), + } + ) + routes.sort(key=lambda r: (0 if r.get("default") else 1, r.get("metric", 0), r.get("destination", ""))) + routes = routes[: max(1, int(limit_routes))] + except OSError: + routes = [] + + neigh: list[dict] = [] + try: + with open("/proc/net/arp", "r", encoding="utf-8", errors="ignore") as fh: + for line in fh.readlines()[1:]: + parts = line.split() + if len(parts) < 6: + continue + ip_addr, _hw_type, flags_h, mac, _mask, device = parts[:6] + try: + flags = int(flags_h, 16) + except ValueError: + flags = 0 + if mac in ("00:00:00:00:00:00", "0:0:0:0:0:0"): + continue + state = "REACHABLE" if flags & 0x2 else ("INCOMPLETE" if flags & 0x1 else "STALE") + neigh.append( + { + "ip": ip_addr, + "mac": mac, + "iface": device, + "state": state, + "flags": flags, + } + ) + neigh = neigh[: max(1, int(limit_neigh))] + except OSError: + neigh = [] + + ip_stats = _parse_snmp_section("Ip") + icmp_stats = _parse_snmp_section("Icmp") + return { + "routes": routes, + "neigh": neigh, + "icmp": { + "in_msgs": int(icmp_stats.get("InMsgs", 0)), + "out_msgs": int(icmp_stats.get("OutMsgs", 0)), + "in_errors": int(icmp_stats.get("InErrors", 0)), + "out_errors": int(icmp_stats.get("OutErrors", 0)), + "in_dest_unreach": int(icmp_stats.get("InDestUnreachs", 0)), + "out_dest_unreach": int(icmp_stats.get("OutDestUnreachs", 0)), + "in_time_excds": int(icmp_stats.get("InTimeExcds", 0)), + "in_echo_reps": int(icmp_stats.get("InEchoReps", 0)), + "out_echos": int(icmp_stats.get("OutEchos", 0)), + }, + "ip": { + "forwarding": int(ip_stats.get("Forwarding", 0)), + "default_ttl": int(ip_stats.get("DefaultTTL", 0)), + "in_receives": int(ip_stats.get("InReceives", 0)), + "out_requests": int(ip_stats.get("OutRequests", 0)), + "in_discards": int(ip_stats.get("InDiscards", 0)), + "out_discards": int(ip_stats.get("OutDiscards", 0)), + "in_addr_errors": int(ip_stats.get("InAddrErrors", 0)), + "in_hdr_errors": int(ip_stats.get("InHdrErrors", 0)), + "reasm_reqds": int(ip_stats.get("ReasmReqds", 0)), + "frag_oks": int(ip_stats.get("FragOKs", 0)), + }, + "source": {"routes": "/proc/net/route", "neigh": "/proc/net/arp", "snmp": "/proc/net/snmp"}, + } + + def get_network_stack_realtime(network_stack_prev=None): network_stack_prev = _NETWORK_STACK_PREV_DEFAULT if network_stack_prev is None else network_stack_prev now = time.time() @@ -439,6 +556,7 @@ def rate(curr, prev): "out_segs": int(tcp_stats.get("OutSegs", 0)), "retrans_segs_total": int(retrans_total), }, + "ip_map": get_ip_layer_map(), } diff --git a/static/js/network-stack.js b/static/js/network-stack.js index 6b904fe..59df9b1 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: 5 +// Version: 6 — L04 IP drill-in replaces puzzle with FIB/route/neigh/ICMP map -debugLog('🌐 network-stack.js v5: Script loading...'); +debugLog('🌐 network-stack.js v6: Script loading...'); class NetworkStackVisualization { constructor() { @@ -95,6 +95,7 @@ class NetworkStackVisualization { this.selectedGalaxy = 'state'; this.galaxyStateData = null; this.lifecyclePanelNode = null; + this.ipMapPinned = false; this.hideOsiTiles = true; this.hideVerticalOrbs = true; this.packetLifecycleStages = [ @@ -1946,6 +1947,10 @@ class NetworkStackVisualization { updatePacketLifecycleUI() { if (!this.lifecyclePanelNode) return; + if (this.ipMapPinned || this.drillLayerId === 'ip') { + this.renderIpLayerMapPanel(); + return; + } const idx = this.getPacketLifecycleIndex(); const stage = this.packetLifecycleStages[Math.max(0, Math.min(this.packetLifecycleStages.length - 1, idx))] || 'NIC RX'; const activeCoreNode = this.lifecycleStageToNode[stage] || 'skb'; @@ -2945,11 +2950,155 @@ class NetworkStackVisualization { return (rows[layerId] || (() => []))(); } + getIpMapData() { + return this.telemetryData?.ip_map || { routes: [], neigh: [], icmp: {}, ip: {} }; + } + + buildIpLayerMapHtml({ compact = false } = {}) { + const map = this.getIpMapData(); + const routes = Array.isArray(map.routes) ? map.routes : []; + const neigh = Array.isArray(map.neigh) ? map.neigh : []; + const icmp = map.icmp || {}; + 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)'; + const routeRows = routes.length + ? routes.map((r) => { + const dest = r.default + ? 'default' + : `${r.destination}`; + const via = r.gateway && r.gateway !== '*' + ? `via ${r.gateway}` + : 'on-link'; + return `
+ ${dest} + ${via} + dev ${r.iface} + metric ${r.metric ?? 0} +
`; + }).join('') + : '
no routes in /proc/net/route
'; + const neighRows = neigh.length + ? neigh.map((h) => { + const st = String(h.state || 'STALE'); + const stCol = st === 'REACHABLE' ? '#96ffbe' : (st === 'INCOMPLETE' ? '#e69696' : '#e6c15a'); + return `
+ ${h.ip} + ${h.mac} + dev ${h.iface} + ${st} +
`; + }).join('') + : '
no ARP/neigh entries
'; + const chip = (label, value, hot = false) => ` +
+
${label}
+
${value}
+
`; + const icmpHot = n(icmp.in_errors) + n(icmp.out_errors) + n(icmp.in_dest_unreach) > 0; + const flowDiagram = ` +
+ LOOKUP + FIB + + NEIGH + + L2 / NIC + | + ICMP + control / errors +
`; + return ` +
+
+
IP LAYER MAP · L04 · ${fwd}
+
/proc/net/route · /proc/net/arp · /proc/net/snmp
+
+ ${compact ? `` : ''} +
+ ${flowDiagram} +
+
+
FIB / ROUTES
+
${routeRows}
+
+
+
NEIGH / ARP
+
${neighRows}
+
+
+
ICMP · IP COUNTERS
+
+ ${chip('ICMP in', n(icmp.in_msgs))} + ${chip('ICMP out', n(icmp.out_msgs))} + ${chip('errors', n(icmp.in_errors) + n(icmp.out_errors), icmpHot)} + ${chip('unreach', n(icmp.in_dest_unreach) + n(icmp.out_dest_unreach), n(icmp.in_dest_unreach) > 0)} + ${chip('TTL / echo', `${n(ip.default_ttl) || '—'} / ${n(icmp.out_echos)}`)} + ${chip('IP discards', n(ip.in_discards) + n(ip.out_discards), n(ip.in_discards) + n(ip.out_discards) > 0)} +
+
+
`; + } + + renderIpLayerMapPanel() { + if (!this.lifecyclePanelNode) return; + window.setSafeHtml(this.lifecyclePanelNode, this.buildIpLayerMapHtml({ compact: true })); + const back = this.lifecyclePanelNode.querySelector('.ns-ip-back-puzzle'); + if (back) { + back.addEventListener('click', () => { + this.ipMapPinned = false; + if (this.drillLayerId === 'ip') this.closeLayerDrilldown(); + else this.updatePacketLifecycleUI(); + }); + } + } + openLayerDrilldown(layerId) { if (!this.drillScrim || !this.drillPanel) return; const info = this.getLayerDrillInfo(layerId); if (!info) return; this.drillLayerId = layerId; + + 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); + 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 html = ` +
+
+
STACK LAYER · NETWORK · L04
+
IP · FIB / ROUTE / NEIGH / ICMP
+
+
+
LIVE ACTIVITY
+
${act}%
+
+
+
+
+
${metricCells}
+
${info.what}
+
WATCH · ${info.watch}
+ ${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.renderIpLayerMapPanel(); + this.drillScrim.style.display = 'block'; + if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none'; + return; + } + + this.ipMapPinned = false; + 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)'); const metrics = this.getLayerDrillMetrics(layerId); @@ -2993,8 +3142,13 @@ class NetworkStackVisualization { } closeLayerDrilldown() { + const wasIp = this.drillLayerId === 'ip'; this.drillLayerId = 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(); } // Push the latest BBR sample into a rolling window (used to estimate the diff --git a/templates/linux-network-subsystem.html b/templates/linux-network-subsystem.html index b5b0b0b..887d850 100644 --- a/templates/linux-network-subsystem.html +++ b/templates/linux-network-subsystem.html @@ -103,7 +103,7 @@

TCP BBR Path Model

- +