diff --git a/docs/API_NETTOOLS.md b/docs/API_NETTOOLS.md index ba71bbb0..6c4ce031 100755 --- a/docs/API_NETTOOLS.md +++ b/docs/API_NETTOOLS.md @@ -1,6 +1,6 @@ # Net Tools API Endpoints -The Net Tools API provides **network diagnostic utilities**, including Wake-on-LAN, traceroute, speed testing, DNS resolution, nmap scanning, and internet connection information. +The Net Tools API provides **network diagnostic utilities**, including Wake-on-LAN, traceroute, speed testing, DNS resolution, nmap scanning, internet connection information, and network interface info. All endpoints require **authorization** via Bearer token. @@ -190,6 +190,51 @@ All endpoints require **authorization** via Bearer token. --- +### 7. Network Interfaces + +* **GET** `/nettools/interfaces` + Fetches the list of network interfaces on the system, including IPv4/IPv6 addresses, MAC, MTU, state (up/down), and RX/TX byte counters. + +**Response** (success): + +```json +{ + "success": true, + "interfaces": { + "eth0": { + "name": "eth0", + "short": "eth0", + "type": "ethernet", + "state": "up", + "mtu": 1500, + "mac": "00:11:32:EF:A5:6B", + "ipv4": ["192.168.1.82/24"], + "ipv6": ["fe80::211:32ff:feef:a56c/64"], + "rx_bytes": 18488221, + "tx_bytes": 1443944 + }, + "lo": { + "name": "lo", + "short": "lo", + "type": "loopback", + "state": "up", + "mtu": 65536, + "mac": null, + "ipv4": ["127.0.0.1/8"], + "ipv6": ["::1/128"], + "rx_bytes": 123456, + "tx_bytes": 123456 + } + } +} +``` + +**Error Responses**: + +* Command failure or parsing error → HTTP 500 + +--- + ## Example `curl` Requests **Wake-on-LAN**: @@ -242,11 +287,20 @@ curl "http://:/nettools/internetinfo" \ -H "Authorization: Bearer " ``` +**Network Interfaces**: + +```sh +curl "http://:/nettools/interfaces" \ + -H "Authorization: Bearer " +``` + --- ## MCP Tools Network tools are available as **MCP Tools** for AI assistant integration: -- `wol_wake_device`, `trigger_scan`, `get_open_ports` + +* `wol_wake_device`, `trigger_scan`, `get_open_ports` 📖 See [MCP Server Bridge API](API_MCP.md) for AI integration details. + diff --git a/front/systeminfoNetwork.php b/front/systeminfoNetwork.php index dd8b594c..402d3357 100755 --- a/front/systeminfoNetwork.php +++ b/front/systeminfoNetwork.php @@ -31,76 +31,107 @@ function getExternalIp() { // Network // ---------------------------------------------------------- -//Network stats -// Server IP + +// ---------------------------------------------------- +// Network Stats (General) +// ---------------------------------------------------- + +// External IP $externalIp = getExternalIp(); -// Check Server name -if (!empty(gethostname())) { $network_NAME = gethostname(); } else { $network_NAME = lang('Systeminfo_Network_Server_Name_String'); } -// Check HTTPS -if (isset($_SERVER['HTTPS'])) { $network_HTTPS = 'Yes (HTTPS)'; } else { $network_HTTPS = lang('Systeminfo_Network_Secure_Connection_String'); } -// Check Query String -if (empty($_SERVER['QUERY_STRING'])) { $network_QueryString = lang('Systeminfo_Network_Server_Query_String'); } else { $network_QueryString = $_SERVER['QUERY_STRING']; } -// Check HTTP referer -if (empty($_SERVER['HTTP_REFERER'])) { $network_referer = lang('Systeminfo_Network_HTTP_Referer_String'); } else { $network_referer = $_SERVER['HTTP_REFERER']; } -//Network Hardware stat -$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $1}'"); -$net_interfaces = explode("\n", trim($network_result)); -$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $2}'"); -$net_interfaces_rx = explode("\n", trim($network_result)); -$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $10}'"); -$net_interfaces_tx = explode("\n", trim($network_result)); +// Server Name +$network_NAME = gethostname() ?: lang('Systeminfo_Network_Server_Name_String'); + +// HTTPS Check +$network_HTTPS = isset($_SERVER['HTTPS']) ? 'Yes (HTTPS)' : lang('Systeminfo_Network_Secure_Connection_String'); + +// Query String +$network_QueryString = !empty($_SERVER['QUERY_STRING']) + ? $_SERVER['QUERY_STRING'] + : lang('Systeminfo_Network_Server_Query_String'); + +// Referer +$network_referer = !empty($_SERVER['HTTP_REFERER']) + ? $_SERVER['HTTP_REFERER'] + : lang('Systeminfo_Network_HTTP_Referer_String'); -// Network Hardware ---------------------------------------------------------- -echo '
-
-

' . lang('Systeminfo_Network_Hardware') . '

-
-
- - - +// ---------------------------------------------------- +// Network Hardware Stats (FAST VERSION) +// ---------------------------------------------------- + +// ---------------------------------------------------- +// Network Stats (General) +// ---------------------------------------------------- + +// External IP +$externalIp = getExternalIp(); + +// Server Name +$network_NAME = gethostname() ?: lang('Systeminfo_Network_Server_Name_String'); + +// HTTPS Check +$network_HTTPS = isset($_SERVER['HTTPS']) ? 'Yes (HTTPS)' : lang('Systeminfo_Network_Secure_Connection_String'); + +// Query String +$network_QueryString = !empty($_SERVER['QUERY_STRING']) + ? $_SERVER['QUERY_STRING'] + : lang('Systeminfo_Network_Server_Query_String'); + +// Referer +$network_referer = !empty($_SERVER['HTTP_REFERER']) + ? $_SERVER['HTTP_REFERER'] + : lang('Systeminfo_Network_HTTP_Referer_String'); + + + +// ---------------------------------------------------- +// Network Stats (General) +// ---------------------------------------------------- + +// External IP +$externalIp = getExternalIp(); + +// Server Name +$network_NAME = gethostname() ?: lang('Systeminfo_Network_Server_Name_String'); + +// HTTPS Check +$network_HTTPS = isset($_SERVER['HTTPS']) ? 'Yes (HTTPS)' : lang('Systeminfo_Network_Secure_Connection_String'); + +// Query String +$network_QueryString = !empty($_SERVER['QUERY_STRING']) + ? $_SERVER['QUERY_STRING'] + : lang('Systeminfo_Network_Server_Query_String'); + +// Referer +$network_referer = !empty($_SERVER['HTTP_REFERER']) + ? $_SERVER['HTTP_REFERER'] + : lang('Systeminfo_Network_HTTP_Referer_String'); + +echo ' +
+
+

+ ' . lang('Systeminfo_Network_Hardware') .' +

+
+
+
+ + - - - '; - -for ($x = 0; $x < sizeof($net_interfaces); $x++) { - $interface_name = str_replace(':', '', $net_interfaces[$x]); - $interface_ip_temp = exec('ip addr show ' . $interface_name . ' | grep "inet "'); - $interface_ip_arr = explode(' ', trim($interface_ip_temp)); - - if (!isset($interface_ip_arr[1])) { - $interface_ip_arr[1] = '--'; - } - - if ($net_interfaces_rx[$x] == 0) { - $temp_rx = 0; - } else { - $temp_rx = number_format(round(($net_interfaces_rx[$x] / 1024 / 1024), 2), 2, ',', '.'); - } - if ($net_interfaces_tx[$x] == 0) { - $temp_tx = 0; - } else { - $temp_tx = number_format(round(($net_interfaces_tx[$x] / 1024 / 1024), 2), 2, ',', '.'); - } - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; -} - -echo ' -
' . lang('Systeminfo_Network_Hardware_Interface_Name') . ' ' . lang('Systeminfo_Network_Hardware_Interface_Mask') . ' ' . lang('Systeminfo_Network_Hardware_Interface_RX') . ' ' . lang('Systeminfo_Network_Hardware_Interface_TX') . '
' . $interface_name . '' . $interface_ip_arr[1] . '' . $temp_rx . ' MB' . $temp_tx . ' MB
-
-
'; + + + + Loading... + + + +'; // Available IPs ---------------------------------------------------------- echo '
@@ -131,7 +162,7 @@ echo '
' . lang('Systeminfo_Network_IP_Server') . '
' . $_SERVER['SERVER_ADDR'] . '
-
+
' . lang('Systeminfo_Network_Server_Name') . '
' . $network_NAME . '
@@ -139,11 +170,11 @@ echo '
' . lang('Systeminfo_Network_Connection_Port') . '
' . $_SERVER['REMOTE_PORT'] . '
-
+
' . lang('Systeminfo_Network_Secure_Connection') . '
' . $network_HTTPS . '
-
+
' . lang('Systeminfo_Network_Server_Version') . '
' . $_SERVER['SERVER_SOFTWARE'] . '
@@ -151,7 +182,7 @@ echo '
' . lang('Systeminfo_Network_Request_URI') . '
' . $_SERVER['REQUEST_URI'] . '
-
+
' . lang('Systeminfo_Network_Server_Query') . '
' . $network_QueryString . '
@@ -159,11 +190,11 @@ echo '
' . lang('Systeminfo_Network_HTTP_Host') . '
' . $_SERVER['HTTP_HOST'] . '
-
+
' . lang('Systeminfo_Network_HTTP_Referer') . '
' . $network_referer . '
-
+
' . lang('Systeminfo_Network_MIME') . '
' . $_SERVER['HTTP_ACCEPT'] . '
@@ -171,11 +202,11 @@ echo '
' . lang('Systeminfo_Network_Accept_Language') . '
' . $_SERVER['HTTP_ACCEPT_LANGUAGE'] . '
-
+
' . lang('Systeminfo_Network_Accept_Encoding') . '
' . $_SERVER['HTTP_ACCEPT_ENCODING'] . '
-
+
' . lang('Systeminfo_Network_Request_Method') . '
' . $_SERVER['REQUEST_METHOD'] . '
@@ -183,7 +214,7 @@ echo '
' . lang('Systeminfo_Network_Request_Time') . '
' . $_SERVER['REQUEST_TIME'] . '
-
+
'; @@ -241,14 +272,14 @@ function fetchUsedIps(callback) { `, variables: { options: { - status: "all_devices" - } + status: "all_devices" + } } }), success: function(response) { console.log(response); - + const usedIps = (response?.data?.devices?.devices || []) .map(d => d.devLastIP) .filter(ip => ip && ip.includes('.')); @@ -270,12 +301,12 @@ function renderAvailableIpsTable(allIps, usedIps) { destroy: true, data: availableIps, columns: [ - { - title: getString("Gen_Subnet"), - data: "subnet" + { + title: getString("Gen_Subnet"), + data: "subnet" }, - { - title: getString("Systeminfo_AvailableIps"), + { + title: getString("Systeminfo_AvailableIps"), data: "ip", render: function (data, type, row, meta) { return ` @@ -292,6 +323,87 @@ function renderAvailableIpsTable(allIps, usedIps) { } + +// Helper: Convert CIDR to subnet mask +function cidrToMask(cidr) { + return ((0xFFFFFFFF << (32 - cidr)) >>> 0) + .toString(16) + .match(/.{1,2}/g) + .map(h => parseInt(h, 16)) + .join('.'); +} + +function formatDataSize(bytes) { + if (!bytes) bytes = 0; // ensure it's a number + + const mb = bytes / 1024 / 1024; // convert bytes to MB + + // Format number with 2 decimals and thousands separators + return mb.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " MB"; +} + + + +function loadInterfaces() { + const apiToken = getSetting("API_TOKEN"); // replace with dynamic token if available + + const host = window.location.hostname; + const port = getSetting("GRAPHQL_PORT"); + + $.ajax({ + url: "http://" + host + ":" + port + "/nettools/interfaces", + type: "GET", + headers: { + "Authorization": "Bearer " + apiToken, + "Content-Type": "application/json" + }, + success: function(data) { + const tbody = $("#networkTable tbody"); + tbody.empty(); + + console.log(data); + + + if (!data.success || !data.interfaces || Object.keys(data.interfaces).length === 0) { + tbody.append('No interfaces found'); + return; + } + + $.each(data.interfaces, function(iface_name, iface) { + + const rx_mb = formatDataSize(iface.rx_bytes); + const tx_mb = formatDataSize(iface.tx_bytes); + + // const rx_mb = (iface.rx_bytes ?? 0) / 1024 / 1024; + // const tx_mb = (iface.tx_bytes ?? 0) / 1024 / 1024; + + let cidr_display = ""; + if (iface.ipv4 && iface.ipv4.length > 0) { + const ip_info = iface.ipv4[0]; + const ip = ip_info.ip || "--"; + const mask = cidrToMask(ip_info.cidr || 24); + cidr_display = mask + " / " + iface.ipv4; + } + + tbody.append(` + + ${iface_name} + ${cidr_display} + ${rx_mb} + ${tx_mb} + + `); + }); + }, + error: function(xhr) { + const tbody = $("#networkTable tbody"); + tbody.empty(); + tbody.append('Failed to fetch interfaces'); + console.error("Error fetching interfaces:", xhr.responseText); + } + }); +} + // INIT $(document).ready(function() { @@ -301,6 +413,8 @@ $(document).ready(function() { renderAvailableIpsTable(allIps, usedIps); }); + loadInterfaces(); + setTimeout(() => { // Available IPs datatable $('#networkTable').DataTable({ @@ -309,7 +423,7 @@ $(document).ready(function() { initComplete: function(settings, json) { hideSpinner(); // Called after the DataTable is fully initialized } - }); + }); }, 200); }); diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index 184adf93..81c57394 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -58,7 +58,8 @@ from .nettools_endpoint import ( # noqa: E402 [flake8 lint suppression] speedtest, nslookup, nmap_scan, - internet_info + internet_info, + network_interfaces ) from .dbquery_endpoint import read_query, write_query, update_query, delete_query # noqa: E402 [flake8 lint suppression] from .sync_endpoint import handle_sync_post, handle_sync_get # noqa: E402 [flake8 lint suppression] @@ -535,6 +536,13 @@ def api_internet_info(): return internet_info() +@app.route("/nettools/interfaces", methods=["GET"]) +def api_network_interfaces(): + if not is_authorized(): + return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403 + return network_interfaces() + + @app.route('/mcp/sse/nettools/trigger-scan', methods=['POST']) @app.route("/nettools/trigger-scan", methods=["GET"]) def api_trigger_scan(): diff --git a/server/api_server/nettools_endpoint.py b/server/api_server/nettools_endpoint.py index d0cc09bf..c2f2c80e 100755 --- a/server/api_server/nettools_endpoint.py +++ b/server/api_server/nettools_endpoint.py @@ -277,3 +277,90 @@ def internet_info(): "details": str(e), } ), 500 + + +def network_interfaces(): + """ + API endpoint to fetch network interface info using `nmap --iflist`. + Returns JSON with interface info and RX/TX bytes. + """ + try: + # Run Nmap + nmap_output = subprocess.run( + ["nmap", "--iflist"], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + # Read /proc/net/dev for RX/TX + rx_tx = {} + with open("/proc/net/dev") as f: + for line in f.readlines()[2:]: + if ":" not in line: + continue + iface, data = line.split(":") + iface = iface.strip() + cols = data.split() + rx_bytes = int(cols[0]) + tx_bytes = int(cols[8]) + rx_tx[iface] = {"rx": rx_bytes, "tx": tx_bytes} + + interfaces = {} + + for line in nmap_output.splitlines(): + line = line.strip() + if not line: + continue + + # Skip header line + if line.startswith("DEV") or line.startswith("----"): + continue + + # Regex to parse: DEV (SHORT) IP/MASK TYPE UP MTU MAC + match = re.match( + r"^(\S+)\s+\(([^)]*)\)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(\S*)", + line + ) + if not match: + continue + + dev, short, ipmask, type_, state, mtu_str, mac = match.groups() + + # Only parse MTU if it's a number + try: + mtu = int(mtu_str) + except ValueError: + mtu = None + + if dev not in interfaces: + interfaces[dev] = { + "name": dev, + "short": short, + "type": type_, + "state": state.lower(), + "mtu": mtu, + "mac": mac if mac else None, + "ipv4": [], + "ipv6": [], + "rx_bytes": rx_tx.get(dev, {}).get("rx", 0), + "tx_bytes": rx_tx.get(dev, {}).get("tx", 0), + } + + # Parse IP/MASK + if ipmask != "(none)/0": + if ":" in ipmask: + interfaces[dev]["ipv6"].append(ipmask) + else: + interfaces[dev]["ipv4"].append(ipmask) + + return jsonify({"success": True, "interfaces": interfaces}), 200 + + except (subprocess.CalledProcessError, ValueError, FileNotFoundError) as e: + return jsonify( + { + "success": False, + "error": "Failed to fetch network interface info", + "details": str(e), + } + ), 500 diff --git a/test/api_endpoints/test_nettools_endpoints.py b/test/api_endpoints/test_nettools_endpoints.py index 72f16d35..a860636e 100644 --- a/test/api_endpoints/test_nettools_endpoints.py +++ b/test/api_endpoints/test_nettools_endpoints.py @@ -208,3 +208,32 @@ def test_internet_info_endpoint(client, api_token): assert data.get("success") is False assert "error" in data assert "details" in data + + +def test_interfaces_endpoint(client, api_token): + # Call the /nettools/interfaces endpoint + resp = client.get("/nettools/interfaces", headers=auth_headers(api_token)) + data = resp.json + + # Assertions + if resp.status_code == 200: + assert data.get("success") is True + assert "interfaces" in data + interfaces = data["interfaces"] + assert isinstance(interfaces, dict) + for if_name, iface in interfaces.items(): + assert "name" in iface + assert "short" in iface + assert "type" in iface + assert "state" in iface + assert "mtu" in iface + assert "mac" in iface + assert "ipv4" in iface and isinstance(iface["ipv4"], list) + assert "ipv6" in iface and isinstance(iface["ipv6"], list) + assert "rx_bytes" in iface + assert "tx_bytes" in iface + else: + # Handle failure + assert data.get("success") is False + assert "error" in data + assert "details" in data