diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:03 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:18 +0000 |
commit | 5da14042f70711ea5cf66e034699730335462f66 (patch) | |
tree | 0f6354ccac934ed87a2d555f45be4c831cf92f4a /src/collectors/network-viewer.plugin | |
parent | Releasing debian version 1.44.3-2. (diff) | |
download | netdata-5da14042f70711ea5cf66e034699730335462f66.tar.xz netdata-5da14042f70711ea5cf66e034699730335462f66.zip |
Merging upstream version 1.45.3+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/collectors/network-viewer.plugin')
-rw-r--r-- | src/collectors/network-viewer.plugin/network-connections-chart.html | 706 | ||||
-rw-r--r-- | src/collectors/network-viewer.plugin/network-viewer.c | 803 |
2 files changed, 1509 insertions, 0 deletions
diff --git a/src/collectors/network-viewer.plugin/network-connections-chart.html b/src/collectors/network-viewer.plugin/network-connections-chart.html new file mode 100644 index 000000000..4471b2add --- /dev/null +++ b/src/collectors/network-viewer.plugin/network-connections-chart.html @@ -0,0 +1,706 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Network Viewer</title> + <style> + /* Styles to make the canvas full width and height */ + body, html { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; + } + + #d3-canvas { + height: 100%; + width: 100%; + } + </style> + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet"> + <!-- Include D3.js --> + <script src="https://d3js.org/d3.v7.min.js"></script> +</head> +<body> +<div id="d3-canvas"></div> <!-- Div for D3 rendering --> + +<script> + + let config = { + listen: true, + } + + function transformData(dataPayload) { + // console.log("dataPayload", dataPayload); + const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"]; + + const transformedData = []; + const appMap = new Map(); + + console.log(dataPayload); + dataPayload.data.forEach(row => { + const rowData = {}; + desiredColumns.forEach(columnName => { + const columnIndex = dataPayload.columns[columnName].index; + rowData[columnName] = row[columnIndex]; + }); + + const appName = rowData['Process']; + if (!appMap.has(appName)) { + appMap.set(appName, { + counts: { + listen: 0, + inbound: 0, + outbound: 0, + local: 0, + private: 0, + public: 0, + total: 0 + } + }); + } + + const appData = appMap.get(appName); + + if(config.listen || rowData['Direction'] !== 'listen') + appData.counts.total++; + + if (rowData['Direction'] === 'listen') + appData.counts.listen++; + else if (rowData['Direction'] === 'local') + appData.counts.local++; + else if (rowData['Direction'] === 'inbound') + appData.counts.inbound++; + else if (rowData['Direction'] === 'outbound') + appData.counts.outbound++; + + if (rowData['RemoteAddressSpace'] === 'public') + appData.counts.public++; + else if (rowData['RemoteAddressSpace'] === 'private') + appData.counts.private++; + }); + + // Convert the map to an array format + for (let [appName, appData] of appMap) { + transformedData.push({ + name: appName, + ...appData + }); + } + + if(!config.listen) + return transformedData.filter(function(d) { + return d.counts.total > 0; + }); + else + return transformedData; + } + + function normalizeData(data, w, h, borderPadding) { + const cw = w / 2 - borderPadding; + const ch = h / 2 - borderPadding; + + const minSize = 13; + const maxSize = Math.min( + (cw * 2) / 3 - borderPadding, + (ch * 2) / 3 - borderPadding, + Math.max(5, Math.min(w, h) / data.length) + minSize + ) + + const max = { + total: d3.max(data, d => d.counts.total), + local: d3.max(data, d => d.counts.local), + listen: d3.max(data, d => d.counts.listen), + private: d3.max(data, d => d.counts.private), + public: d3.max(data, d => d.counts.public), + inbound: d3.max(data, d => d.counts.inbound), + outbound: d3.max(data, d => d.counts.outbound), + } + + const circleSize = d3.scaleLog() + .domain([1, max.total]) + .range([minSize, maxSize]) + .clamp(true); // Clamps the output so that it stays within the range + + let listenerDelta = 50; + let listenersAdded = 0; + let listenersAddedRight = 0; + let listenersAddedLeft = 0; + let listenersH = h - borderPadding; + + data.forEach((d, i) => { + const logScaleRight = d3.scaleLog().domain([1, d.counts.public + 1]).range([0, cw]); + const logScaleLeft = d3.scaleLog().domain([1, d.counts.private + 1]).range([0, cw]); + const logScaleTop = d3.scaleLog().domain([1, d.counts.outbound + 1]).range([0, ch]); + const logScaleBottom = d3.scaleLog().domain([1, (d.counts.listen + d.counts.inbound) / 2 + 1]).range([0, ch]); + + d.forces = { + total: d.counts.total / max.total, + local: d.counts.local / max.local, + listen: d.counts.listen / max.listen, + private: d.counts.private / max.private, + public: d.counts.public / max.public, + inbound: d.counts.inbound / max.inbound, + outbound: d.counts.outbound / max.outbound, + } + + d.pos = { + // we add 1 to avoid log(0) + right: logScaleRight(d.counts.public + 1), + left: logScaleLeft(d.counts.private + 1), + top: logScaleTop(d.counts.outbound + 1), + bottom: logScaleBottom(((config.listen ? d.counts.listen : 0) + d.counts.inbound) / (config.listen ? 2 : 1) + 1), + }; + + let x = borderPadding + cw + d.pos.right - d.pos.left; + let y = borderPadding + ch + d.pos.bottom - d.pos.top; + let size = circleSize(d.counts.total); + + if(d.counts.listen === d.counts.total) { + // a listener + size = circleSize(1); + + if(listenersAdded * listenerDelta > cw * 2 / 3) { + // too many listeners on this row + // let's start adding on another row above + listenersAdded = 0; + listenersAddedLeft = 0; + listenersAddedRight = 0; + listenersH -= 30; + } + + if(!listenersAdded) { + x = cw; + y = listenersH - size; + } + else { + if(listenersAddedLeft >= listenersAddedRight) { + listenersAddedRight++; + x = cw + listenersAddedRight * listenerDelta; + y = listenersH - size - (listenersAddedRight % 2 === 0 ? 0 : 30); + } + else { + listenersAddedLeft++; + x = cw - listenersAddedLeft * listenerDelta; + y = listenersH - size - (listenersAddedLeft % 2 === 0 ? 0 : 30); + } + } + + listenersAdded++; + } + + const others = d.counts.total - (d.counts.public + d.counts.private + (config.listen ? d.counts.listen : 0) + d.counts.inbound + d.counts.outbound); + d.d3 = { + x: x, + y: y, + size: size, + pie: [ + { value: d.counts.public }, + { value: d.counts.private }, + { value: (config.listen ? d.counts.listen : 0) + d.counts.inbound }, + { value: d.counts.outbound }, + { value: others > 0 ? others : 0 }, + ] + } + + if(d.d3.x - d.d3.size / 2 < borderPadding) + d.d3.x = borderPadding + d.d3.size * 2; + + if(d.d3.x + d.d3.size / 2 > w) + d.d3.x = w - d.d3.size * 2; + + if(d.d3.y - d.d3.size / 2 < borderPadding) + d.d3.y = borderPadding + d.d3.size * 2; + + if(d.d3.y + d.d3.size / 2 > h) + d.d3.y = h - d.d3.size * 2; + + if (d.name === 'upsd') + console.log("object", d, "cw", cw, "ch", ch); + }); + + return data; + } + + const themes = { + dark: { + publicColor: "#bb9900", + privateColor: "#323299", + serverColor: "#008800", + clientColor: "#994433", + otherColor: "#454545", + backgroundColor: "black", + appFontColor: "#bbbbbb", + appFontFamily: 'IBM Plex Sans', + appFontSize: "12px", + appFontWeight: "regular", + borderFontColor: "#aaaaaa", + borderFontFamily: 'IBM Plex Sans', + borderFontSize: "14px", + borderFontWeight: "bold", + }, + light: { + publicColor: "#bbaa00", + privateColor: "#5555ff", + serverColor: "#009900", + clientColor: "#990000", + otherColor: "#666666", + backgroundColor: "white", + appFontColor: "black", + appFontFamily: 'IBM Plex Sans', + appFontSize: "12px", + appFontWeight: "bold", + borderFontColor: "white", + borderFontFamily: 'IBM Plex Sans', + borderFontSize: "14px", + borderFontWeight: "bold", + } + } + + function hexToHalfOpacityRGBA(hex) { + if (hex.length !== 7 || hex[0] !== '#') + throw new Error('Invalid hex color format'); + + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, 0.5)`; + } + + function getRgbColor(hex, opacity = 1) { + if (hex.length !== 7 || hex[0] !== "#") throw new Error("Invalid hex color format"); + + // Parse the hex color components (red, green, blue) + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + + // Ensure opacity is within the valid range (0 to 1) + const validOpacity = Math.min(1, Math.max(0, opacity)); + + // Return the RGBA color + return `rgba(${r}, ${g}, ${b}, ${validOpacity})`; + } + + function drawInitialChart(svg, data, w, h, borderPadding, theme) { + const cw = w / 2; + const ch = h / 2; + + document.body.style.backgroundColor = theme.backgroundColor; + + const clientsGradient = svg.append("defs") + .append("linearGradient") + .attr("id", "clientsGradient") + .attr("x1", "0%") + .attr("y1", "0%") + .attr("x2", "0%") + .attr("y2", "100%"); + + clientsGradient.append("stop") + .attr("offset", "0%") + .style("stop-color", getRgbColor(theme.clientColor, 1)); + + clientsGradient.append("stop") + .attr("offset", "100%") + .style("stop-color", getRgbColor(theme.clientColor, 0)); + + svg.append("rect") + .attr("id", "clientsGradientRect") + .attr("x", 0) + .attr("y", 0) + .attr("width", "100%") + .attr("height", borderPadding / 2) + .style("fill", "url(#clientsGradient)"); + + svg.append("text") + .attr("id", "clientsText") + .text("Clients") + .attr("x", "50%") + .attr("y", borderPadding / 2 - 4) + .attr("text-anchor", "middle") + .style("font-family", theme.borderFontFamily) + .style("font-size", theme.borderFontSize) + .style("font-weight", theme.borderFontWeight) + .style("fill", theme.borderFontColor); + + const serversGradient = svg.append("defs") + .append("linearGradient") + .attr("id", "serversGradient") + .attr("x1", "0%") + .attr("y1", "100%") // Start from the bottom + .attr("x2", "0%") + .attr("y2", "0%") // End at the top + + serversGradient.append("stop") + .attr("offset", "0%") + .style("stop-color", getRgbColor(theme.serverColor, 1)); + + serversGradient.append("stop") + .attr("offset", "100%") + .style("stop-color", getRgbColor(theme.serverColor, 0)); + + svg.append("rect") + .attr("id", "serversGradientRect") + .attr("x", 0) + .attr("y", h - borderPadding / 2) + .attr("width", "100%") + .attr("height", borderPadding / 2) + .style("fill", "url(#serversGradient)"); // Use the reversed gradient fill + + svg.append("text") + .attr("id", "serversText") + .text("Servers") + .attr("x", "50%") + .attr("y", h - borderPadding / 2 + 16) + .attr("text-anchor", "middle") + .style("font-family", theme.borderFontFamily) + .style("font-size", theme.borderFontSize) + .style("font-weight", theme.borderFontWeight) + .style("fill", theme.borderFontColor); + + const publicGradient = svg.append("defs") + .append("linearGradient") + .attr("id", "publicGradient") + .attr("x1", "100%") // Start from the right + .attr("y1", "0%") + .attr("x2", "0%") // End at the left + .attr("y2", "0%"); + + publicGradient.append("stop") + .attr("offset", "0%") + .style("stop-color", getRgbColor(theme.publicColor, 1)); + + publicGradient.append("stop") + .attr("offset", "100%") + .style("stop-color", getRgbColor(theme.publicColor, 0)); + + svg.append("rect") + .attr("id", "publicGradientRect") + .attr("x", w - borderPadding / 2) + .attr("y", 0) + .attr("width", borderPadding / 2) + .attr("height", "100%") + .style("fill", "url(#publicGradient)"); + + svg.append("text") + .attr("id", "publicText") + .text("Public") + .attr("x", w - (borderPadding / 2)) + .attr("y", ch - 10) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`) + .style("font-family", theme.borderFontFamily) + .style("font-size", theme.borderFontSize) + .style("font-weight", theme.borderFontWeight) + .style("fill", theme.borderFontColor); + + const privateGradient = svg.append("defs") + .append("linearGradient") + .attr("id", "privateGradient") + .attr("x1", "0%") // Start from the left + .attr("y1", "0%") + .attr("x2", "100%") // End at the right + .attr("y2", "0%"); + + privateGradient.append("stop") + .attr("offset", "0%") + .style("stop-color", getRgbColor(theme.privateColor, 1)); + + privateGradient.append("stop") + .attr("offset", "100%") + .style("stop-color", getRgbColor(theme.privateColor, 0)); + + svg.append("rect") + .attr("id", "privateGradientRect") + .attr("x", 0) + .attr("y", 0) + .attr("width", borderPadding / 2) + .attr("height", "100%") + .style("fill", "url(#privateGradient)"); + + svg.append("text") + .attr("id", "privateText") + .text("Private") + .attr("x", borderPadding / 2) + .attr("y", ch) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`) + .style("font-family", theme.borderFontFamily) + .style("font-size", theme.borderFontSize) + .style("font-weight", theme.borderFontWeight) + .style("fill", theme.borderFontColor); + } + + function updateGradientBoxes(svg, w, h, borderPadding, theme) { + const cw = w / 2; + const ch = h / 2; + + // Update clientsGradient (top gradient) - mainly adjusting width + svg.select("rect#clientsGradientRect") + .attr("width", w); // Stretch across the new width + + // Update text position if necessary + svg.select("text#clientsText") + .attr("x", w / 2); // Center text + + // Update serversGradient (bottom gradient) - adjust Y position and width + svg.select("rect#serversGradientRect") + .attr("y", h - borderPadding / 2) // Move to the new bottom position + .attr("width", w) // Stretch across the new width + .attr("fill", "red"); + + // Update text position if necessary + svg.select("text#serversText") + .attr("x", w / 2) // Center text + .attr("y", h - borderPadding / 2 + 16); // Adjust based on your specific offset + + // Update publicGradient (right gradient) - adjust X position and height + svg.select("rect#publicGradientRect") + .attr("x", w - borderPadding / 2) // Move to the new right side position + .attr("height", h); // Stretch across the new height + + // Update text position if necessary, adjusting for rotation + svg.select("text#publicText") + .attr("x", w - (borderPadding / 2)) + .attr("y", ch - 10) + .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`); + + // Update privateGradient (left gradient) - height adjustment only as it's already at x = 0 + svg.select("rect#privateGradientRect") + .attr("height", h); // Stretch across the new height + + // Update text position if necessary, adjusting for rotation + svg.select("text#privateText") + .attr("x", borderPadding / 2) + .attr("y", ch) + .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`); + + } + + let positionsMap = new Map(); + function saveCurrentPositions(svg) { + svg.selectAll('.app').each(function(d) { + if (d) { + positionsMap.set(d.name, { x: d.x, y: d.y }); + } + }); + } + + function updateApps(svg, data, w, h, borderPadding, theme) { + const cw = w / 2; + const ch = h / 2; + + saveCurrentPositions(svg); + //svg.selectAll('.app').remove(); + + const pieColors = d3.scaleOrdinal() + .domain(["public", "private", "listenInbound", "outbound", "others"]) + .range([theme.publicColor, theme.privateColor, theme.serverColor, theme.clientColor, theme.otherColor]); + + const pie = d3.pie().value(d => d.value); + const arc = d3.arc(); + + // Binding data with key function + const app = svg.selectAll('.app') + .data(data, d => d.name); + + // Remove any elements that no longer have data associated with them + app.exit() + .transition() + .style("opacity", 0) + .remove(); + + // Enter selection for new data points + const appEnter = app.enter() + .append('g') + .attr('class', 'app') + .attr('transform', `translate(${cw}, ${ch})`); // Start from center + + // Initialize new elements + appEnter.each(function (d) { + const group = d3.select(this); + const pieData = pie(d.d3.pie); + + group.selectAll('path') + .data(pieData) + .enter().append('path') + .transition() + .attr('fill', (d, i) => pieColors(i)); + + group.append('text') + .text(d => d.name) + .attr('text-anchor', 'middle') + .style('font-family', theme.appFontFamily) + .style('font-size', theme.appFontSize) + .style('font-weight', theme.appFontWeight) + .style('fill', theme.appFontColor); + }); + + // Transition for new elements + appEnter.transition() + .attr('transform', d => `translate(${d.d3.x}, ${d.d3.y})`); + + // Merge the enter and update selections + const mergedApp = appEnter.merge(app); + + // Update saved positions + mergedApp.each(function (d) { + const group = d3.select(this); + const oldPos = positionsMap.get(d.name) || { x: cw, y: ch }; + + d.x = oldPos.x; + d.y = oldPos.y; + + group.selectAll('path') + .data(pie(d.d3.pie)) + .transition() + .attr('d', arc.innerRadius(0).outerRadius(d.d3.size)); + + group.select('text') + .transition() + .attr('y', d.d3.size + 10); + }); + + // Apply the drag behavior to the merged selection + mergedApp.call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended)); + + return mergedApp; + } + + let forceInit = true; + let initial = true; + let simulation; + + function dragstarted(event, d) { + if (!event.active) simulation.alphaTarget(1).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + // Function to draw the circles and labels for each application with border forces + function drawApplications(data, w, h, borderPadding, theme) { + let svg = d3.select('#d3-canvas').select('svg'); + + if(svg.empty()) { + svg = d3.select('#d3-canvas').select('svg'); + svg = d3.select('#d3-canvas').append('svg') + .attr('width', '100%') + .attr('height', '100%'); + + drawInitialChart(svg, data, w, h, borderPadding, theme); + forceInit = false; + } + + updateGradientBoxes(svg, w, h, borderPadding, theme); + + const app = updateApps(svg, data, w, h, borderPadding, theme); + + app.transition().duration(5000) + + simulation = d3.forceSimulation(data) + //.force('center', d3.forceCenter(cw, ch).strength(1)) + .force("x", d3.forceX(d => d.d3.x).strength(d => { + if(d.counts.listen === d.counts.total) + return 0.5 + else + return 0.05 + })) + .force("y", d3.forceY(d => d.d3.y).strength(d => { + if(d.counts.listen === d.counts.total) + return 0.5 + else + return 0.05 + })) + //.force("charge", d3.forceManyBody().strength(-0.05)) + .force("collide", d3.forceCollide(d => d.d3.size * 1.1 + 15).strength(1)) + .on('tick', ticked); + + function ticked() { + data.forEach(d => { + if(d.x > w - d.d3.size) + d.x = w - d.d3.size; + else if(d.x < 0) + d.x = 0; + + if(d.y > h - d.d3.size) + d.y = h - d.d3.size; + else if(d.y < 0) + d.y = 0; + }); + + app.attr('transform', d => `translate(${d.x}, ${d.y})`); + } + + initial = false; + } + + let lastPayload = {}; + function redrawChart() { + const transformed = transformData(lastPayload); + console.log(transformed); + + const w = window.innerWidth; + const h = window.innerHeight; + const borderPadding = 40; + + const normalized = normalizeData(transformed, w, h, borderPadding); + drawApplications(normalized, w, h, borderPadding, themes.dark); + + // Update SVG dimensions + const svg = d3.select('#d3-canvas').select('svg') + .attr('width', w) + .attr('height', h); + } + + // Debounce function to optimize performance + function debounce(func, timeout = 50) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { func.apply(this, args); }, timeout); + }; + } + + // Attach the event listener to the window resize event + window.addEventListener('resize', debounce(() => { + forceInit = true; + redrawChart(); + })); + + // Modify your fetchData function to call drawApplications after data transformation + function fetchDataAndUpdateChart() { + fetch('http://localhost:19999/api/v1/function?function=network-connections') + .then(response => response.json()) + .then(data => { + lastPayload = data; + redrawChart(); + }) + .catch(error => console.error('Error fetching data:', error)); + } + + // Initial load + window.onload = () => { + fetchDataAndUpdateChart(); + setInterval(fetchDataAndUpdateChart, 2000); // You may need to adjust this part + }; +</script> +</body> +</html> diff --git a/src/collectors/network-viewer.plugin/network-viewer.c b/src/collectors/network-viewer.plugin/network-viewer.c new file mode 100644 index 000000000..074535a0b --- /dev/null +++ b/src/collectors/network-viewer.plugin/network-viewer.c @@ -0,0 +1,803 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "collectors/all.h" +#include "libnetdata/libnetdata.h" +#include "libnetdata/required_dummies.h" + +#define ENABLE_DETAILED_VIEW + +#define LOCAL_SOCKETS_EXTENDED_MEMBERS struct { \ + size_t count; \ + const char *local_address_space; \ + const char *remote_address_space; \ + } network_viewer; + +#include "libnetdata/maps/local-sockets.h" +#include "libnetdata/maps/system-users.h" + +#define NETWORK_CONNECTIONS_VIEWER_FUNCTION "network-connections" +#define NETWORK_CONNECTIONS_VIEWER_HELP "Network connections explorer" + +#define SIMPLE_HASHTABLE_VALUE_TYPE LOCAL_SOCKET +#define SIMPLE_HASHTABLE_NAME _AGGREGATED_SOCKETS +#include "libnetdata/simple_hashtable.h" + +netdata_mutex_t stdout_mutex = NETDATA_MUTEX_INITIALIZER; +static bool plugin_should_exit = false; +static USERNAMES_CACHE *uc; + +ENUM_STR_MAP_DEFINE(SOCKET_DIRECTION) = { + { .id = SOCKET_DIRECTION_LISTEN, .name = "listen" }, + { .id = SOCKET_DIRECTION_LOCAL_INBOUND, .name = "local" }, + { .id = SOCKET_DIRECTION_LOCAL_OUTBOUND, .name = "local" }, + { .id = SOCKET_DIRECTION_INBOUND, .name = "inbound" }, + { .id = SOCKET_DIRECTION_OUTBOUND, .name = "outbound" }, + + // terminator + { . id = 0, .name = NULL } +}; +ENUM_STR_DEFINE_FUNCTIONS(SOCKET_DIRECTION, SOCKET_DIRECTION_LISTEN, "unknown"); + +typedef int TCP_STATE; +ENUM_STR_MAP_DEFINE(TCP_STATE) = { + { .id = TCP_ESTABLISHED, .name = "established" }, + { .id = TCP_SYN_SENT, .name = "syn-sent" }, + { .id = TCP_SYN_RECV, .name = "syn-received" }, + { .id = TCP_FIN_WAIT1, .name = "fin1-wait1" }, + { .id = TCP_FIN_WAIT2, .name = "fin1-wait2" }, + { .id = TCP_TIME_WAIT, .name = "time-wait" }, + { .id = TCP_CLOSE, .name = "close" }, + { .id = TCP_CLOSE_WAIT, .name = "close-wait" }, + { .id = TCP_LAST_ACK, .name = "last-ack" }, + { .id = TCP_LISTEN, .name = "listen" }, + { .id = TCP_CLOSING, .name = "closing" }, + + // terminator + { . id = 0, .name = NULL } +}; +ENUM_STR_DEFINE_FUNCTIONS(TCP_STATE, 0, "unknown"); + +static void local_socket_to_json_array(BUFFER *wb, LOCAL_SOCKET *n, uint64_t proc_self_net_ns_inode, bool aggregated) { + char local_address[INET6_ADDRSTRLEN]; + char remote_address[INET6_ADDRSTRLEN]; + char *protocol; + + if(n->local.family == AF_INET) { + ipv4_address_to_txt(n->local.ip.ipv4, local_address); + ipv4_address_to_txt(n->remote.ip.ipv4, remote_address); + protocol = n->local.protocol == IPPROTO_TCP ? "tcp4" : "udp4"; + } + else if(n->local.family == AF_INET6) { + ipv6_address_to_txt(&n->local.ip.ipv6, local_address); + ipv6_address_to_txt(&n->remote.ip.ipv6, remote_address); + protocol = n->local.protocol == IPPROTO_TCP ? "tcp6" : "udp6"; + } + else + return; + + const char *type; + if(n->net_ns_inode == proc_self_net_ns_inode) + type = "system"; + else if(n->net_ns_inode == 0) + type = "[unknown]"; + else + type = "container"; + + buffer_json_add_array_item_array(wb); + { + buffer_json_add_array_item_string(wb, SOCKET_DIRECTION_2str(n->direction)); + buffer_json_add_array_item_string(wb, protocol); + buffer_json_add_array_item_string(wb, type); // system or container + if(n->local.protocol == IPPROTO_TCP) + buffer_json_add_array_item_string(wb, TCP_STATE_2str(n->state)); + else + buffer_json_add_array_item_string(wb, "stateless"); + + buffer_json_add_array_item_uint64(wb, n->pid); + + if(!n->comm[0]) + buffer_json_add_array_item_string(wb, "[unknown]"); + else + buffer_json_add_array_item_string(wb, n->comm); + + // buffer_json_add_array_item_string(wb, string2str(n->cmdline)); + + if(n->uid == UID_UNSET) { + // buffer_json_add_array_item_uint64(wb, n->uid); + buffer_json_add_array_item_string(wb, "[unknown]"); + } + else { + // buffer_json_add_array_item_uint64(wb, n->uid); + STRING *u = system_usernames_cache_lookup_uid(uc, n->uid); + buffer_json_add_array_item_string(wb, string2str(u)); + string_freez(u); + } + + if(!aggregated) { + buffer_json_add_array_item_string(wb, local_address); + buffer_json_add_array_item_uint64(wb, n->local.port); + } + buffer_json_add_array_item_string(wb, n->network_viewer.local_address_space); + + if(!aggregated) { + buffer_json_add_array_item_string(wb, remote_address); + buffer_json_add_array_item_uint64(wb, n->remote.port); + } + buffer_json_add_array_item_string(wb, n->network_viewer.remote_address_space); + + uint16_t server_port; + const char *server_address; + const char *client_address_space; + const char *server_address_space; + switch (n->direction) { + case SOCKET_DIRECTION_LISTEN: + case SOCKET_DIRECTION_INBOUND: + case SOCKET_DIRECTION_LOCAL_INBOUND: + server_port = n->local.port; + server_address = local_address; + server_address_space = n->network_viewer.local_address_space; + client_address_space = n->network_viewer.remote_address_space; + break; + + case SOCKET_DIRECTION_OUTBOUND: + case SOCKET_DIRECTION_LOCAL_OUTBOUND: + server_port = n->remote.port; + server_address = remote_address; + server_address_space = n->network_viewer.remote_address_space; + client_address_space = n->network_viewer.local_address_space; + break; + + case SOCKET_DIRECTION_NONE: + break; + } + if(aggregated) + buffer_json_add_array_item_string(wb, server_address); + + buffer_json_add_array_item_uint64(wb, server_port); + + if(aggregated) { + buffer_json_add_array_item_string(wb, client_address_space); + buffer_json_add_array_item_string(wb, server_address_space); + } + + // buffer_json_add_array_item_uint64(wb, n->inode); + // buffer_json_add_array_item_uint64(wb, n->net_ns_inode); + buffer_json_add_array_item_uint64(wb, n->network_viewer.count); + } + buffer_json_array_close(wb); +} + +static void local_sockets_cb_to_json(LS_STATE *ls, LOCAL_SOCKET *n, void *data) { + n->network_viewer.count = 1; + n->network_viewer.local_address_space = local_sockets_address_space(&n->local); + n->network_viewer.remote_address_space = local_sockets_address_space(&n->remote); + local_socket_to_json_array(data, n, ls->proc_self_net_ns_inode, false); +} + +static void local_sockets_cb_to_aggregation(LS_STATE *ls __maybe_unused, LOCAL_SOCKET *n, void *data) { + SIMPLE_HASHTABLE_AGGREGATED_SOCKETS *ht = data; + n->network_viewer.count = 1; + n->network_viewer.local_address_space = local_sockets_address_space(&n->local); + n->network_viewer.remote_address_space = local_sockets_address_space(&n->remote); + + switch(n->direction) { + case SOCKET_DIRECTION_INBOUND: + case SOCKET_DIRECTION_LOCAL_INBOUND: + case SOCKET_DIRECTION_LISTEN: + memset(&n->remote.ip, 0, sizeof(n->remote.ip)); + n->remote.port = 0; + break; + + case SOCKET_DIRECTION_OUTBOUND: + case SOCKET_DIRECTION_LOCAL_OUTBOUND: + memset(&n->local.ip, 0, sizeof(n->local.ip)); + n->local.port = 0; + break; + + case SOCKET_DIRECTION_NONE: + return; + } + + n->inode = 0; + n->local_ip_hash = 0; + n->remote_ip_hash = 0; + n->local_port_hash = 0; + n->timer = 0; + n->retransmits = 0; + n->expires = 0; + n->rqueue = 0; + n->wqueue = 0; + memset(&n->local_port_key, 0, sizeof(n->local_port_key)); + + XXH64_hash_t hash = XXH3_64bits(n, sizeof(*n)); + SIMPLE_HASHTABLE_SLOT_AGGREGATED_SOCKETS *sl = simple_hashtable_get_slot_AGGREGATED_SOCKETS(ht, hash, n, true); + LOCAL_SOCKET *t = SIMPLE_HASHTABLE_SLOT_DATA(sl); + if(t) { + t->network_viewer.count++; + } + else { + t = mallocz(sizeof(*t)); + memcpy(t, n, sizeof(*t)); + t->cmdline = string_dup(t->cmdline); + simple_hashtable_set_slot_AGGREGATED_SOCKETS(ht, sl, hash, t); + } +} + +static int local_sockets_compar(const void *a, const void *b) { + LOCAL_SOCKET *n1 = *(LOCAL_SOCKET **)a, *n2 = *(LOCAL_SOCKET **)b; + return strcmp(n1->comm, n2->comm); +} + +void network_viewer_function(const char *transaction, char *function __maybe_unused, usec_t *stop_monotonic_ut __maybe_unused, + bool *cancelled __maybe_unused, BUFFER *payload __maybe_unused, HTTP_ACCESS access __maybe_unused, + const char *source __maybe_unused, void *data __maybe_unused) { + + time_t now_s = now_realtime_sec(); + bool aggregated = false; + + CLEAN_BUFFER *wb = buffer_create(0, NULL); + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + + buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); + buffer_json_member_add_string(wb, "type", "table"); + buffer_json_member_add_time_t(wb, "update_every", 5); + buffer_json_member_add_boolean(wb, "has_history", false); + buffer_json_member_add_string(wb, "help", NETWORK_CONNECTIONS_VIEWER_HELP); + +#ifdef ENABLE_DETAILED_VIEW + buffer_json_member_add_array(wb, "accepted_params"); + { + buffer_json_add_array_item_string(wb, "sockets"); + } + buffer_json_array_close(wb); // accepted_params + buffer_json_member_add_array(wb, "required_params"); + { + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "id", "sockets"); + buffer_json_member_add_string(wb, "name", "Sockets"); + buffer_json_member_add_string(wb, "help", "Select the source type to query"); + buffer_json_member_add_boolean(wb, "unique_view", true); + buffer_json_member_add_string(wb, "type", "select"); + buffer_json_member_add_array(wb, "options"); + { + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "id", "aggregated"); + buffer_json_member_add_string(wb, "name", "Aggregated view of sockets"); + } + buffer_json_object_close(wb); + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "id", "detailed"); + buffer_json_member_add_string(wb, "name", "Detailed view of all sockets"); + } + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); // options array + } + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); // required_params +#endif + + char function_copy[strlen(function) + 1]; + memcpy(function_copy, function, sizeof(function_copy)); + char *words[1024]; + size_t num_words = quoted_strings_splitter_pluginsd(function_copy, words, 1024); + for(size_t i = 1; i < num_words ;i++) { + char *param = get_word(words, num_words, i); + if(strcmp(param, "sockets:aggregated") == 0) { + aggregated = true; + } + else if(strcmp(param, "sockets:detailed") == 0) { + aggregated = false; + } + else if(strcmp(param, "info") == 0) { + goto close_and_send; + } + } + + if(aggregated) { + buffer_json_member_add_object(wb, "aggregated_view"); + { + buffer_json_member_add_string(wb, "column", "Count"); + buffer_json_member_add_string(wb, "results_label", "unique combinations"); + buffer_json_member_add_string(wb, "aggregated_label", "sockets"); + } + buffer_json_object_close(wb); + } + + { + buffer_json_member_add_array(wb, "data"); + + LS_STATE ls = { + .config = { + .listening = true, + .inbound = true, + .outbound = true, + .local = true, + .tcp4 = true, + .tcp6 = true, + .udp4 = true, + .udp6 = true, + .pid = true, + .uid = true, + .cmdline = true, + .comm = true, + .namespaces = true, + + .max_errors = 10, + }, + .stats = { 0 }, + .sockets_hashtable = { 0 }, + .local_ips_hashtable = { 0 }, + .listening_ports_hashtable = { 0 }, + }; + + SIMPLE_HASHTABLE_AGGREGATED_SOCKETS ht = { 0 }; + if(aggregated) { + simple_hashtable_init_AGGREGATED_SOCKETS(&ht, 1024); + ls.config.cb = local_sockets_cb_to_aggregation; + ls.config.data = &ht; + } + else { + ls.config.cb = local_sockets_cb_to_json; + ls.config.data = wb; + } + + local_sockets_process(&ls); + + if(aggregated) { + LOCAL_SOCKET *array[ht.used]; + size_t added = 0; + uint64_t proc_self_net_ns_inode = ls.proc_self_net_ns_inode; + for(SIMPLE_HASHTABLE_SLOT_AGGREGATED_SOCKETS *sl = simple_hashtable_first_read_only_AGGREGATED_SOCKETS(&ht); + sl; + sl = simple_hashtable_next_read_only_AGGREGATED_SOCKETS(&ht, sl)) { + LOCAL_SOCKET *n = SIMPLE_HASHTABLE_SLOT_DATA(sl); + if(!n || added >= ht.used) continue; + + array[added++] = n; + } + + qsort(array, added, sizeof(LOCAL_SOCKET *), local_sockets_compar); + + for(size_t i = 0; i < added ;i++) { + local_socket_to_json_array(wb, array[i], proc_self_net_ns_inode, true); + string_freez(array[i]->cmdline); + freez(array[i]); + } + + simple_hashtable_destroy_AGGREGATED_SOCKETS(&ht); + } + + buffer_json_array_close(wb); + buffer_json_member_add_object(wb, "columns"); + { + size_t field_id = 0; + + // Direction + buffer_rrdf_table_add_field(wb, field_id++, "Direction", "Socket Direction", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_STICKY, + NULL); + + // Protocol + buffer_rrdf_table_add_field(wb, field_id++, "Protocol", "Socket Protocol", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + // Type + buffer_rrdf_table_add_field(wb, field_id++, "Namespace", "Namespace", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + // State + buffer_rrdf_table_add_field(wb, field_id++, "State", "Socket State", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + // Pid + buffer_rrdf_table_add_field(wb, field_id++, "PID", "Process ID", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + // Comm + buffer_rrdf_table_add_field(wb, field_id++, "Process", "Process Name", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH, + NULL); + +// // Cmdline +// buffer_rrdf_table_add_field(wb, field_id++, "CommandLine", "Command Line", +// RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, +// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, +// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, +// RRDF_FIELD_OPTS_NONE|RRDF_FIELD_OPTS_FULL_WIDTH, +// NULL); + +// // Uid +// buffer_rrdf_table_add_field(wb, field_id++, "UID", "User ID", +// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, +// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, +// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, +// RRDF_FIELD_OPTS_NONE, +// NULL); + + // Username + buffer_rrdf_table_add_field(wb, field_id++, "User", "Username", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + if(!aggregated) { + // Local Address + buffer_rrdf_table_add_field(wb, field_id++, "LocalIP", "Local IP Address", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH, + NULL); + + // Local Port + buffer_rrdf_table_add_field(wb, field_id++, "LocalPort", "Local Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + } + + // Local Address Space + buffer_rrdf_table_add_field(wb, field_id++, "LocalAddressSpace", "Local IP Address Space", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, + NULL); + + if(!aggregated) { + // Remote Address + buffer_rrdf_table_add_field(wb, field_id++, "RemoteIP", "Remote IP Address", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH, + NULL); + + // Remote Port + buffer_rrdf_table_add_field(wb, field_id++, "RemotePort", "Remote Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + } + + // Remote Address Space + buffer_rrdf_table_add_field(wb, field_id++, "RemoteAddressSpace", "Remote IP Address Space", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, + NULL); + + if(aggregated) { + // Server IP + buffer_rrdf_table_add_field(wb, field_id++, "ServerIP", "Server IP Address", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + RRDF_FIELD_OPTS_FULL_WIDTH | (aggregated ? RRDF_FIELD_OPTS_VISIBLE : RRDF_FIELD_OPTS_NONE), + NULL); + } + + // Server Port + buffer_rrdf_table_add_field(wb, field_id++, "ServerPort", "Server Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + aggregated ? RRDF_FIELD_OPTS_VISIBLE : RRDF_FIELD_OPTS_NONE, + NULL); + + if(aggregated) { + // Client Address Space + buffer_rrdf_table_add_field(wb, field_id++, "ClientAddressSpace", "Client IP Address Space", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + + // Server Address Space + buffer_rrdf_table_add_field(wb, field_id++, "ServerAddressSpace", "Server IP Address Space", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, + NULL); + } + +// // inode +// buffer_rrdf_table_add_field(wb, field_id++, "Inode", "Socket Inode", +// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, +// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, +// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, +// RRDF_FIELD_OPTS_NONE, +// NULL); + +// // Namespace inode +// buffer_rrdf_table_add_field(wb, field_id++, "Namespace Inode", "Namespace Inode", +// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, +// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, +// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, +// RRDF_FIELD_OPTS_NONE, +// NULL); + + // Count + buffer_rrdf_table_add_field(wb, field_id++, "Count", "Number of sockets like this", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, + aggregated ? (RRDF_FIELD_OPTS_VISIBLE | RRDF_FIELD_OPTS_STICKY) : RRDF_FIELD_OPTS_NONE, + NULL); + } + buffer_json_object_close(wb); // columns + buffer_json_member_add_string(wb, "default_sort_column", aggregated ? "Count" : "Direction"); + + buffer_json_member_add_object(wb, "custom_charts"); + { + buffer_json_member_add_object(wb, "Network Map"); + { + buffer_json_member_add_string(wb, "type", "network-viewer"); + } + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); // custom_charts + + buffer_json_member_add_object(wb, "charts"); + { + // Data Collection Age chart + buffer_json_member_add_object(wb, "Count"); + { + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Direction"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + // Streaming Age chart + buffer_json_member_add_object(wb, "Count"); + { + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Process"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + // DB Duration + buffer_json_member_add_object(wb, "Count"); + { + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Protocol"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); // charts + + buffer_json_member_add_array(wb, "default_charts"); + { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, "Count"); + buffer_json_add_array_item_string(wb, "Direction"); + buffer_json_array_close(wb); + + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, "Count"); + buffer_json_add_array_item_string(wb, "Process"); + buffer_json_array_close(wb); + } + buffer_json_array_close(wb); + + buffer_json_member_add_object(wb, "group_by"); + { + buffer_json_member_add_object(wb, "Direction"); + { + buffer_json_member_add_string(wb, "name", "Direction"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Direction"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "Protocol"); + { + buffer_json_member_add_string(wb, "name", "Protocol"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Protocol"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "Namespace"); + { + buffer_json_member_add_string(wb, "name", "Namespace"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Namespace"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "Process"); + { + buffer_json_member_add_string(wb, "name", "Process"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Process"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + if(!aggregated) { + buffer_json_member_add_object(wb, "LocalIP"); + { + buffer_json_member_add_string(wb, "name", "Local IP"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "LocalIP"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "LocalPort"); + { + buffer_json_member_add_string(wb, "name", "Local Port"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "LocalPort"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "RemoteIP"); + { + buffer_json_member_add_string(wb, "name", "Remote IP"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "RemoteIP"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "RemotePort"); + { + buffer_json_member_add_string(wb, "name", "Remote Port"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "RemotePort"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + } + } + buffer_json_object_close(wb); // group_by + } + +close_and_send: + buffer_json_member_add_time_t(wb, "expires", now_s + 1); + buffer_json_finalize(wb); + + netdata_mutex_lock(&stdout_mutex); + pluginsd_function_result_to_stdout(transaction, HTTP_RESP_OK, "application/json", now_s + 1, wb); + netdata_mutex_unlock(&stdout_mutex); +} + +// ---------------------------------------------------------------------------------------------------------------- +// main + +int main(int argc __maybe_unused, char **argv __maybe_unused) { + clocks_init(); + netdata_thread_set_tag("NETWORK-VIEWER"); + nd_log_initialize_for_external_plugins("network-viewer.plugin"); + + netdata_configured_host_prefix = getenv("NETDATA_HOST_PREFIX"); + if(verify_netdata_host_prefix(true) == -1) exit(1); + + uc = system_usernames_cache_init(); + + // ---------------------------------------------------------------------------------------------------------------- + + if(argc == 2 && strcmp(argv[1], "debug") == 0) { + bool cancelled = false; + usec_t stop_monotonic_ut = now_monotonic_usec() + 600 * USEC_PER_SEC; + char buf[] = "network-connections sockets:aggregated"; + network_viewer_function("123", buf, &stop_monotonic_ut, &cancelled, + NULL, HTTP_ACCESS_ALL, NULL, NULL); + + char buf2[] = "network-connections sockets:detailed"; + network_viewer_function("123", buf2, &stop_monotonic_ut, &cancelled, + NULL, HTTP_ACCESS_ALL, NULL, NULL); + exit(1); + } + + // ---------------------------------------------------------------------------------------------------------------- + + fprintf(stdout, PLUGINSD_KEYWORD_FUNCTION " GLOBAL \"%s\" %d \"%s\" \"top\" "HTTP_ACCESS_FORMAT" %d\n", + NETWORK_CONNECTIONS_VIEWER_FUNCTION, 60, + NETWORK_CONNECTIONS_VIEWER_HELP, + (HTTP_ACCESS_FORMAT_CAST)(HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_SENSITIVE_DATA), + RRDFUNCTIONS_PRIORITY_DEFAULT); + + // ---------------------------------------------------------------------------------------------------------------- + + struct functions_evloop_globals *wg = + functions_evloop_init(5, "Network-Viewer", &stdout_mutex, &plugin_should_exit); + + functions_evloop_add_function(wg, NETWORK_CONNECTIONS_VIEWER_FUNCTION, + network_viewer_function, + PLUGINS_FUNCTIONS_TIMEOUT_DEFAULT, + NULL); + + // ---------------------------------------------------------------------------------------------------------------- + + usec_t step_ut = 100 * USEC_PER_MS; + usec_t send_newline_ut = 0; + bool tty = isatty(fileno(stdout)) == 1; + + heartbeat_t hb; + heartbeat_init(&hb); + while(!plugin_should_exit) { + + usec_t dt_ut = heartbeat_next(&hb, step_ut); + send_newline_ut += dt_ut; + + if(!tty && send_newline_ut > USEC_PER_SEC) { + send_newline_and_flush(&stdout_mutex); + send_newline_ut = 0; + } + } + + return 0; +} |