/* * SysDeck - Network Monitor Panel (v0.0.43) * Author: Jeremy Anderson (https://dcos.net) * * v0.0.43 REWRITE — IPTRAF-NG STYLE. * * The v0.0.10-v0.0.43 panel used `ss -tulpn` for a static socket list. * v0.0.43 recreates the iptraf-ng UI by reading the same kernel sources * iptraf-ng reads from directly — no iptraf-ng binary dependency, no * ncurses parsing. * * The panel has three sections (matching iptraf-ng's views): * 1. Interface Overview — live RX/TX rate cards (bytes/s, packets/s) * per interface. Auto-refreshes every 3s. The traffic subcommand * samples /proc/net/dev twice (1s apart) to compute live rates. * 2. IP Traffic Monitor — active TCP/UDP connections table (proto, * state, local addr:port, remote addr:port, TX/RX queue). Reads * /proc/net/tcp + /proc/net/udp directly. * 3. Protocol Statistics — IP/TCP/UDP/ICMP counters from * /proc/net/snmp (InReceives, OutRequests, InDiscards, etc.). * * Data sources (same as iptraf-ng): * /proc/net/dev per-interface RX/TX byte + packet counters * /proc/net/snmp IP/TCP/UDP/ICMP protocol counters * /proc/net/tcp active TCP connections (state, local, remote) * /proc/net/udp active UDP sockets * * The traffic subcommand takes 1 second (samples /proc/net/dev twice, * 1s apart) — the panel shows a loading indicator during that window. */ export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); // Load summary + connections + protocols in parallel (traffic is // slow — 1s — so we load it separately and show it when ready). const [summary, connections, protocols] = await Promise.all([ safe(bridge.netsec.summary(), { total_interfaces: 0, total_connections: 0 }), safe(bridge.netsec.connections(), []), safe(bridge.netsec.protocols(), {}), ]); panel.innerHTML = `

Network Monitor

${summary.total_interfaces || 0} interfaces (${summary.interfaces_up || 0} active) · ${summary.total_connections || 0} connections · ESTABLISHED: ${summary.tcp_established || 0} · LISTEN: ${summary.tcp_listen || 0} ·

Interface Overview (live traffic)

sampling…

Sampling /proc/net/dev (1s window)…

IP Traffic Monitor (${connections.length} connections)

${connections.slice(0, 100).map((c) => ` `).join('') || ''}
ProtoStateLocalRemoteTxQRxQ
${escapeHtml(c.proto)} ${escapeHtml(c.state)} ${escapeHtml(c.local_ip)}:${c.local_port} ${escapeHtml(c.remote_ip)}:${c.remote_port} ${c.tx_queue} ${c.rx_queue}
No active connections.
${connections.length > 100 ? `

Showing first 100 of ${connections.length} connections.

` : ''}

Protocol Statistics

${renderProtocolStats(protocols)}
`; // Load live traffic data asynchronously (1s sample window). loadTrafficData(panel, bridge); wireEvents(panel, { bridge, EventBus }); EventBus.emit('netsec.loaded', { totalInterfaces: summary.total_interfaces, totalConnections: summary.total_connections, }); } // ── Traffic data loader (async, 1s sample) ───────────────────────── async function loadTrafficData(panel, bridge) { const host = panel.querySelector('#netsec-traffic-host'); const status = panel.querySelector('#netsec-traffic-status'); if (!host) return; try { const traffic = await bridge.netsec.traffic(); status.textContent = `updated ${new Date().toLocaleTimeString()}`; host.innerHTML = renderTrafficCards(traffic); } catch (err) { status.textContent = 'error'; host.innerHTML = `

Traffic sample failed: ${escapeHtml(err.message || err)}

`; } } function renderTrafficCards(traffic) { if (!traffic || !traffic.length) { return '

No network interfaces found.

'; } return `
${traffic.map((t) => { const rxKbps = (t.rx_bps / 1024).toFixed(1); const txKbps = (t.tx_bps / 1024).toFixed(1); const rxMbps = (t.rx_bps / (1024 * 1024)).toFixed(2); const txMbps = (t.tx_bps / (1024 * 1024)).toFixed(2); const rxRate = t.rx_bps > 1024 * 1024 ? `${rxMbps} Mbps` : `${rxKbps} KB/s`; const txRate = t.tx_bps > 1024 * 1024 ? `${txMbps} Mbps` : `${txKbps} KB/s`; const rxTotal = t.rx_bytes_total > 1024 * 1024 * 1024 ? `${(t.rx_bytes_total / (1024**3)).toFixed(1)} GB` : t.rx_bytes_total > 1024 * 1024 ? `${(t.rx_bytes_total / (1024**2)).toFixed(1)} MB` : `${(t.rx_bytes_total / 1024).toFixed(0)} KB`; const txTotal = t.tx_bytes_total > 1024 * 1024 * 1024 ? `${(t.tx_bytes_total / (1024**3)).toFixed(1)} GB` : t.tx_bytes_total > 1024 * 1024 ? `${(t.tx_bytes_total / (1024**2)).toFixed(1)} MB` : `${(t.tx_bytes_total / 1024).toFixed(0)} KB`; // Bar width relative to 1 Mbps max for visual scaling. const maxBps = 1024 * 1024; const rxBarW = Math.min(100, (t.rx_bps / maxBps) * 100); const txBarW = Math.min(100, (t.tx_bps / maxBps) * 100); return `
${escapeHtml(t.iface)} ${(t.rx_errs_total + t.tx_errs_total + t.rx_drop_total + t.tx_drop_total) > 0 ? 'errors' : 'clean'}
↓ RX ${rxRate} · ${t.rx_pps} pps · total ${rxTotal}
↑ TX ${txRate} · ${t.tx_pps} pps · total ${txTotal}
`; }).join('')}
`; } function renderProtocolStats(protocols) { if (!protocols || protocols.error) { return '

Protocol statistics unavailable.

'; } const protos = ['ip', 'tcp', 'udp', 'icmp', 'icmp6']; const cards = protos.filter((p) => protocols[p]).map((p) => { const stats = protocols[p]; const fields = Object.entries(stats).slice(0, 12).map(([k, v]) => `${escapeHtml(k)}${escapeHtml(String(v))}` ).join(''); return `

${escapeHtml(p)}

${fields}
`; }).join(''); return cards || '

No protocol data.

'; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { panel.querySelector('#btn-netsec-refresh')?.addEventListener('click', () => { mount(panel, { bridge, EventBus }); }); // Auto-refresh traffic every 5 seconds (the traffic subcommand // itself takes 1s to sample, so 5s gives a good balance between // freshness and load). if (panel._netsecTrafficInterval) clearInterval(panel._netsecTrafficInterval); panel._netsecTrafficInterval = setInterval(() => { loadTrafficData(panel, bridge); }, 5000); // Clean up interval when the panel is unmounted. // Cockpit doesn't have a native unmount hook, but we can detect // when the panel's DOM is removed via MutationObserver. const observer = new MutationObserver(() => { if (!document.body.contains(panel)) { clearInterval(panel._netsecTrafficInterval); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ── Utilities ─────────────────────────────────────────────────────── async function safe(p, fallback) { try { const v = await p; return v ?? fallback; } catch { return fallback; } } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderSkeleton() { return `
`; }