Linux Network Namespace & veth Visualizer

function parseNetns(text) { const ns = ['root']; text.split('\n').forEach(line => { const m = line.match(/^(\S+)/); if (m && m[1]) ns.push(m[1]); }); return [...new Set(ns)]; } function parseLinks(text) { const links = []; text.split('\n').forEach(line => { // Match: N: name@peer: or N: name: const m = line.match(/^\d+:\s+([a-zA-Z0-9_.-]+)(?:@([a-zA-Z0-9_.-]+))?:/); if (m) { const [, name, peer] = m; const type = name.startsWith('br') ? 'bridge' : peer ? 'veth' : name === 'lo' ? 'lo' : 'eth'; links.push({ name, peer: peer || null, type }); } }); return links; } function parseAddrs(text) { const addrs = {}; let current = null; text.split('\n').forEach(line => { const ifMatch = line.match(/^\d+:\s+([a-zA-Z0-9_.-]+)(?:@\S+)?:/); if (ifMatch) { current = ifMatch[1]; addrs[current] = addrs[current] || []; } const ipMatch = line.match(/inet\s+(\d+\.\d+\.\d+\.\d+\/\d+)/); if (ipMatch && current) addrs[current].push(ipMatch[1]); }); return addrs; } function render() { const nsText = document.getElementById('nsInput').value; const linkText = document.getElementById('linkInput').value; const addrText = document.getElementById('addrInput').value; const namespaces = parseNetns(nsText); const links = parseLinks(linkText); const addrs = parseAddrs(addrText); const elements = []; // Namespace nodes namespaces.forEach(ns => { elements.push({ data: { id: `ns_${ns}`, label: ns === 'root' ? 'root\nnamespace' : ns, type: 'ns', ns } }); }); // Interface nodes and veth pair edges const seenPairs = new Set(); links.filter(l => l.type !== 'lo').forEach(l => { const iface = l.name; const ip = (addrs[iface] || []).join('\n') || ''; const type = l.type; elements.push({ data: { id: `if_${iface}`, label: iface + (ip ? '\n' + ip : ''), type, ip } }); // Connect interface to root namespace elements.push({ data: { id: `ns_root-${iface}`, source: 'ns_root', target: `if_${iface}` } }); // veth pair edge if (l.peer && !seenPairs.has(`${l.peer}-${iface}`)) { seenPairs.add(`${iface}-${l.peer}`); elements.push({ data: { id: `veth_${iface}_${l.peer}`, source: `if_${iface}`, target: `if_${l.peer}`, type: 'veth-pair' } }); } }); document.getElementById('cySection').style.display = 'block'; if (cyInst) { cyInst.destroy(); cyInst = null; } cyInst = cytoscape({ container: document.getElementById('cy'), elements, style: [ { selector: 'node[type="ns"]', style: { 'label': 'data(label)', 'text-wrap': 'wrap', 'shape': 'roundrectangle', 'width': 120, 'height': 50, 'background-color': ele => ele.data('ns') === 'root' ? '#28a745' : '#0a3d62', 'color': '#fff', 'font-size': 12, 'font-weight': 'bold', 'text-valign': 'center', 'text-halign': 'center', }}, { selector: 'node[type="veth"]', style: { 'label': 'data(label)', 'text-wrap': 'wrap', 'shape': 'ellipse', 'width': 100, 'height': 50, 'background-color': '#4facfe', 'color': '#fff', 'font-size': 10, 'text-valign': 'center', 'text-halign': 'center', }}, { selector: 'node[type="bridge"]', style: { 'label': 'data(label)', 'shape': 'diamond', 'width': 90, 'height': 50, 'background-color': '#f7971e', 'color': '#fff', 'font-size': 11, 'text-valign': 'center', 'text-halign': 'center', }}, { selector: 'node[type="eth"]', style: { 'label': 'data(label)', 'text-wrap': 'wrap', 'shape': 'roundrectangle', 'width': 100, 'height': 40, 'background-color': '#6c757d', 'color': '#fff', 'font-size': 10, 'text-valign': 'center', 'text-halign': 'center', }}, { selector: 'edge', style: { 'curve-style': 'bezier', 'line-color': '#555', 'width': 2, 'target-arrow-shape': 'none', }}, { selector: 'edge[type="veth-pair"]', style: { 'line-color': '#4facfe', 'width': 3, 'line-style': 'dashed', }}, { selector: 'node:selected', style: { 'border-width': 3, 'border-color': '#ffd200' }}, ], layout: { name: 'cose', padding: 30 }, userZoomingEnabled: true, }); cyInst.on('tap', 'node', evt => { const d = evt.target.data(); let html = `${d.label.replace(/\n/g, ' / ')}
Type: ${d.type}`; if (d.ip) html += `
IPs: ${d.ip}`; document.getElementById('detailPanel').innerHTML = html; }); } function loadSample() { document.getElementById('nsInput').value = 'red (id: 0)\nblue (id: 1)'; document.getElementById('linkInput').value = `1: lo: mtu 65536 2: eth0: mtu 1500 3: veth-red@veth-red-peer: mtu 1500 4: veth-red-peer@veth-red: mtu 1500 5: veth-blue@veth-blue-peer: mtu 1500 6: veth-blue-peer@veth-blue: mtu 1500 7: br0: mtu 1500`; document.getElementById('addrInput').value = `3: veth-red@veth-red-peer: inet 10.0.1.1/24 scope global veth-red 4: veth-red-peer@veth-red: inet 10.0.1.2/24 scope global veth-red-peer 5: veth-blue@veth-blue-peer: inet 10.0.2.1/24 scope global veth-blue 6: veth-blue-peer@veth-blue: inet 10.0.2.2/24 scope global veth-blue-peer`; render(); } function clearAll() { document.getElementById('nsInput').value = ''; document.getElementById('linkInput').value = ''; document.getElementById('addrInput').value = ''; document.getElementById('cySection').style.display = 'none'; if (cyInst) { cyInst.destroy(); cyInst = null; } }