app proto flow determination improvement
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 11s

This commit is contained in:
2026-03-30 22:06:41 +02:00
parent dbd052a7ac
commit de6edd6b68
4 changed files with 111 additions and 50 deletions

View File

@@ -47,6 +47,7 @@ type TopologyLink = {
source: string;
target: string;
value: number;
packetCount: number;
label: string;
};
@@ -124,6 +125,14 @@ function protocolColor(protocol: string) {
return palette[hash % palette.length];
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function sankeyVisualWeight(packetCount: number) {
return Math.max(1, Math.sqrt(Math.max(0, packetCount)));
}
function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyData {
const nodes = new Map<string, TopologyNode>();
const links = new Map<string, TopologyLink>();
@@ -157,7 +166,8 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyD
links.set(interfaceHostLinkId, {
source: interfaceNodeId,
target: hostId,
value: host.packet_count,
value: sankeyVisualWeight(host.packet_count),
packetCount: host.packet_count,
label: `${entry.interface} -> ${host.ip_address ?? host.mac_address ?? 'host'} (${host.packet_count})`,
});
@@ -187,7 +197,8 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyD
links.set(hostProtocolLinkId, {
source: hostId,
target: protocolId,
value: protocol.packet_count,
value: sankeyVisualWeight(protocol.packet_count),
packetCount: protocol.packet_count,
label: `${host.ip_address ?? host.mac_address ?? 'host'} -> ${protocol.protocol} (${protocol.packet_count})`,
});
@@ -229,7 +240,13 @@ function SankeyTopology({ data }: { data: TopologyData }) {
if (!svgRef.current) return;
const width = 1160;
const height = Math.max(420, data.nodes.length * 26);
const nodesPerLayer = {
interface: data.nodes.filter((node) => node.kind === 'interface').length,
host: data.nodes.filter((node) => node.kind === 'host').length,
protocol: data.nodes.filter((node) => node.kind === 'protocol').length,
};
const maxNodesInLayer = Math.max(nodesPerLayer.interface, nodesPerLayer.host, nodesPerLayer.protocol, 1);
const height = clamp(maxNodesInLayer * 56 + 120, 260, 760);
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
svg.attr('viewBox', `0 0 ${width} ${height}`);
@@ -246,7 +263,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
const sankeyLayout = d3Sankey<TopologyNode, TopologyLink>()
.nodeId((node) => node.id)
.nodeWidth(18)
.nodePadding(16)
.nodePadding(maxNodesInLayer <= 4 ? 28 : maxNodesInLayer <= 8 ? 22 : 16)
.extent([
[24, 28],
[width - 24, height - 28],
@@ -254,22 +271,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
const layout = sankeyLayout(graph);
const defs = svg.append('defs');
defs
.append('marker')
.attr('id', 'sankey-arrow')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('refX', 8)
.attr('refY', 5)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L10,5 L0,10 z')
.attr('fill', '#8394a7');
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#f8fbff');
const linkLayer = svg.append('g').attr('fill', 'none').attr('stroke-opacity', 0.35);
const linkLayer = svg.append('g').attr('fill', 'none').attr('stroke-opacity', 0.4);
linkLayer
.selectAll('path')
.data(layout.links as SankeyLinkDatum[])
@@ -280,12 +282,11 @@ function SankeyTopology({ data }: { data: TopologyData }) {
if (target.kind === 'protocol' && target.protocol) {
return protocolColor(target.protocol);
}
return '#7f8ea3';
return '#9aa7b5';
})
.attr('stroke-width', (link) => Math.max(1, link.width || 1))
.attr('marker-end', 'url(#sankey-arrow)')
.append('title')
.text((link) => `${link.label}\nPackets: ${link.value}`);
.text((link) => `${link.label}\nPackets: ${link.packetCount}`);
const nodeLayer = svg.append('g');
const node = nodeLayer
@@ -299,7 +300,6 @@ function SankeyTopology({ data }: { data: TopologyData }) {
.attr('y', (d) => d.y0 ?? 0)
.attr('width', (d) => (d.x1 ?? 0) - (d.x0 ?? 0))
.attr('height', (d) => Math.max(8, (d.y1 ?? 0) - (d.y0 ?? 0)))
.attr('rx', 8)
.attr('fill', (d) => {
if (d.kind === 'interface') return '#20405d';
if (d.kind === 'host') return '#d7e7f5';
@@ -307,7 +307,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
})
.attr('stroke', (d) => (d.kind === 'host' ? '#9bb8d6' : 'none'))
.append('title')
.text((d) => `${d.label}\nPackets: ${d.value ?? d.packetCount}`);
.text((d) => `${d.label}\nPackets: ${d.packetCount}`);
node
.append('text')
@@ -338,8 +338,8 @@ function ForceTopology({ data }: { data: TopologyData }) {
useEffect(() => {
if (!svgRef.current) return;
const width = 1160;
const height = 720;
const width = 1320;
const height = 860;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
svg.attr('viewBox', `0 0 ${width} ${height}`);
@@ -350,6 +350,32 @@ function ForceTopology({ data }: { data: TopologyData }) {
const nodes: ForceNode[] = data.nodes.map((node) => ({ ...node }));
const links: ForceLink[] = data.links.map((link) => ({ ...link }));
const groupedNodes = {
interface: nodes.filter((node) => node.kind === 'interface').sort((left, right) => left.label.localeCompare(right.label)),
host: nodes.filter((node) => node.kind === 'host').sort((left, right) => (left.ipAddress ?? left.macAddress ?? left.label).localeCompare(right.ipAddress ?? right.macAddress ?? right.label)),
protocol: nodes.filter((node) => node.kind === 'protocol').sort((left, right) => left.label.localeCompare(right.label)),
};
const distributedY = (group: ForceNode[], top: number, bottom: number) => {
const targets = new Map<string, number>();
if (group.length === 0) {
return targets;
}
if (group.length === 1) {
targets.set(group[0].id, (top + bottom) / 2);
return targets;
}
const step = (bottom - top) / (group.length - 1);
group.forEach((node, index) => {
targets.set(node.id, top + step * index);
});
return targets;
};
const interfaceY = distributedY(groupedNodes.interface, 120, height - 120);
const hostY = distributedY(groupedNodes.host, 90, height - 90);
const protocolY = distributedY(groupedNodes.protocol, 120, height - 120);
const targetY = (node: ForceNode) => interfaceY.get(node.id) ?? hostY.get(node.id) ?? protocolY.get(node.id) ?? height / 2;
const simulation = d3
.forceSimulation<ForceNode>(nodes)
@@ -360,20 +386,29 @@ function ForceTopology({ data }: { data: TopologyData }) {
.id((node) => node.id)
.distance((link) => {
const source = link.source as ForceNode;
return source.kind === 'interface' ? 140 : 120;
return source.kind === 'interface' ? 250 : 200;
})
.strength((link) => {
const source = link.source as ForceNode;
return source.kind === 'interface' ? 0.45 : 0.35;
}),
)
.force('charge', d3.forceManyBody().strength(-280))
.force('collision', d3.forceCollide<ForceNode>().radius((node) => (node.kind === 'host' ? 38 : 28)))
.force('charge', d3.forceManyBody().strength(-720))
.force('collision', d3.forceCollide<ForceNode>().radius((node) => {
if (node.kind === 'interface') return 52;
if (node.kind === 'host') return 44;
return 34;
}))
.force(
'x',
d3.forceX<ForceNode>().x((node) => {
if (node.kind === 'interface') return 150;
if (node.kind === 'interface') return 180;
if (node.kind === 'host') return width / 2;
return width - 180;
}).strength(0.32),
return width - 200;
}).strength(0.42),
)
.force('y', d3.forceY<ForceNode>(height / 2).strength(0.08));
.force('y', d3.forceY<ForceNode>().y((node) => targetY(node)).strength(0.22))
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.06));
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#fbfcfe');
@@ -445,7 +480,7 @@ function ForceTopology({ data }: { data: TopologyData }) {
return <Empty description="No graph data available yet" />;
}
return <svg ref={svgRef} style={{ width: '100%', minWidth: 960 }} />;
return <svg ref={svgRef} style={{ width: '100%', minWidth: 1120 }} />;
}
function ProtocolHeatmap({ data }: { data: TopologyData }) {