Merge pull request #1026 from LibreQoE/fix/shaped_devices_ip_repr

Fix/shaped devices IP representation
This commit is contained in:
Robert Chacón authored and GitHub committed 2026-04-03 16:20:44 -06:00
commit fe2b195f00
14 files changed
+335 -2265

No files matched your search

@@ -57,11 +57,13 @@ If the page is served through the templated/authenticated router, use `%CACHEBUS
## Validation
- Run `src/rust/lqosd/src/node_manager/js_build/test-node-manager.sh`
- Run `src/rust/lqosd/src/node_manager/js_build/test-build-contract.sh`
- Run `src/rust/lqosd/src/node_manager/js_build/esbuild.sh`
- For broader local iteration, run `src/rust/lqosd/dev_build.sh`
## Notes
- `test-node-manager.sh` uses Node's built-in test runner only; it must not grow a `node_modules` workflow.
- The build-contract test checks page list alignment, required vendored assets, entrypoint/source alignment, and bundle output presence.
- `copy_files.sh` and `dev_build.sh` should stay aligned with the same node_manager asset pipeline.
+6 -2
View File
@@ -433,7 +433,9 @@ fn detect_from_cpuid(possible: &[u32]) -> Option<ResolvedHybridCpuTopology> {
use std::arch::x86_64::__cpuid_count;
// Query leaf 0 first to determine the maximum supported CPUID leaf before using leaf 0x1A.
if __cpuid_count(0, 0).eax < 0x1a {
// SAFETY: `CPUID` is a CPU identification instruction that does not dereference pointers or
// access memory. We probe leaf 0 first and only use the returned register values.
if unsafe { __cpuid_count(0, 0) }.eax < 0x1a {
return None;
}
@@ -453,7 +455,9 @@ fn detect_from_cpuid(possible: &[u32]) -> Option<ResolvedHybridCpuTopology> {
return None;
}
let leaf = __cpuid_count(0x1a, 0);
// SAFETY: leaf 0x1A is only queried after confirming via leaf 0 that the CPU supports it.
// The intrinsic only executes `CPUID` and reads the returned registers.
let leaf = unsafe { __cpuid_count(0x1a, 0) };
let core_type = (leaf.eax >> 24) & 0xff;
match core_type {
CPUID_CORE_TYPE_CORE => performance.push(*cpu),
+1 -2
View File
@@ -9,8 +9,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[dev_build] Building JS bundles..."
pushd "$SCRIPT_DIR/src/node_manager/js_build" >/dev/null
./esbuild.sh
./test-build-contract.sh
./test-node-manager.sh
popd >/dev/null
echo "[dev_build] Copying static HTML/CSS assets to bin/static2..."
@@ -6,7 +6,6 @@ shaped-devices.js
tree.js
help.js
unknown-ips.js
configuration.js
circuit.js
ethernet_caps.js
site_map.js
@@ -123,46 +123,47 @@ export function saveConfig(onComplete, onError) {
}
export function saveNetworkAndDevices(network_json, shaped_devices, onComplete, onError) {
// Validate network_json structure
if (!network_json || typeof network_json !== 'object') {
if (!network_json || typeof network_json !== "object") {
alert("Invalid network configuration");
return;
}
// Validate shaped_devices structure
if (!Array.isArray(shaped_devices)) {
alert("Invalid shaped devices configuration");
return;
}
// Validate individual shaped devices
const validationErrors = [];
const validNodes = validNodeList(network_json);
console.log(validNodes);
shaped_devices.forEach((device, index) => {
// Required fields
if (!device.circuit_id || device.circuit_id.trim() === "") {
validationErrors.push(`Device ${index + 1}: Circuit ID is required`);
}
if (!device.device_id || device.device_id.trim() === "") {
validationErrors.push(`Device ${index + 1}: Device ID is required`);
}
// Parent node validation
if (device.parent_node && validNodes.length > 0 && !validNodes.includes(device.parent_node)) {
validationErrors.push(`Device ${index + 1}: Parent node '${device.parent_node}' does not exist`);
if (
device.parent_node &&
validNodes.length > 0 &&
!validNodes.includes(device.parent_node)
) {
validationErrors.push(
`Device ${index + 1}: Parent node '${device.parent_node}' does not exist`,
);
}
// Bandwidth validation (supports fractional Mbps)
// Minimums must be >= 0.1 Mbps, maximums must be >= 0.2 Mbps
const dmin = parseFloat(device.download_min_mbps);
const umin = parseFloat(device.upload_min_mbps);
const dmax = parseFloat(device.download_max_mbps);
const umax = parseFloat(device.upload_max_mbps);
if (Number.isNaN(dmin) || Number.isNaN(umin) || Number.isNaN(dmax) || Number.isNaN(umax)) {
validationErrors.push(`Device ${index + 1}: One or more bandwidth fields are not valid numbers`);
if (
Number.isNaN(dmin) ||
Number.isNaN(umin) ||
Number.isNaN(dmax) ||
Number.isNaN(umax)
) {
validationErrors.push(
`Device ${index + 1}: One or more bandwidth fields are not valid numbers`,
);
} else {
if (dmin < 0.1 || umin < 0.1) {
validationErrors.push(`Device ${index + 1}: Min rates must be >= 0.1 Mbps`);
@@ -178,21 +179,14 @@ export function saveNetworkAndDevices(network_json, shaped_devices, onComplete,
return;
}
// Prepare data for submission
const submission = {
network_json,
shaped_devices
};
console.log(submission);
sendWsRequest(
"UpdateNetworkAndDevicesResult",
{ UpdateNetworkAndDevices: submission },
{ UpdateNetworkAndDevices: { network_json, shaped_devices } },
(msg) => {
if (onComplete) onComplete(!!msg.ok, msg.message);
},
(msg) => {
const errorMsg = (msg && msg.message) ? msg.message : "Request failed";
const errorMsg = msg && msg.message ? msg.message : "Request failed";
if (onComplete) onComplete(false, errorMsg);
if (onError) {
onError(msg);
@@ -0,0 +1,88 @@
export function parseIpv4Address(text) {
const parts = text.split(".");
if (parts.length !== 4) return null;
const octets = [];
for (const part of parts) {
if (!/^\d{1,3}$/.test(part)) return null;
const octet = parseInt(part, 10);
if (octet < 0 || octet > 255) return null;
octets.push(octet);
}
return octets;
}
function expandIpv4Tail(text) {
if (!text.includes(".")) return text;
const idx = text.lastIndexOf(":");
if (idx === -1) return null;
const tail = parseIpv4Address(text.slice(idx + 1));
if (!tail) return null;
const high = ((tail[0] << 8) | tail[1]).toString(16);
const low = ((tail[2] << 8) | tail[3]).toString(16);
return text.slice(0, idx) + ":" + high + ":" + low;
}
export function parseIpv6Address(text) {
let input = (text || "").trim().toLowerCase();
if (!input || input.includes("%")) return null;
input = expandIpv4Tail(input);
if (!input) return null;
const doubleColon = input.match(/::/g) || [];
if (doubleColon.length > 1) return null;
let head = [];
let tail = [];
if (input.includes("::")) {
const parts = input.split("::");
head = parts[0] ? parts[0].split(":") : [];
tail = parts[1] ? parts[1].split(":") : [];
} else {
head = input.split(":");
}
const validGroup = (group) => /^[0-9a-f]{1,4}$/.test(group);
if (!head.every(validGroup) || !tail.every(validGroup)) return null;
if (!input.includes("::") && head.length !== 8) return null;
if (input.includes("::") && head.length + tail.length >= 8) return null;
const groups = input.includes("::")
? [
...head,
...new Array(8 - head.length - tail.length).fill("0"),
...tail,
]
: head;
if (groups.length !== 8) return null;
const bytes = [];
for (const group of groups) {
const value = parseInt(group, 16);
if (Number.isNaN(value) || value < 0 || value > 0xffff) return null;
bytes.push((value >> 8) & 0xff, value & 0xff);
}
return bytes;
}
export function parseIpInput(text, family) {
if (!text) return [];
const defaultPrefix = family === 6 ? 128 : 32;
const tokens = text.split(/[\n,]+/);
const result = [];
tokens.forEach((token) => {
const trimmed = token.trim();
if (!trimmed) return;
const parts = trimmed.split("/");
const addr = parts[0].trim();
if (!addr) return;
let prefix = defaultPrefix;
if (parts.length > 1 && parts[1].trim().length > 0) {
const parsed = parseInt(parts[1].trim(), 10);
if (!Number.isNaN(parsed)) prefix = parsed;
}
const encoded = family === 6 ? parseIpv6Address(addr) : parseIpv4Address(addr);
result.push([encoded || addr, prefix]);
});
return result;
}
@@ -0,0 +1,44 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
parseIpInput,
parseIpv4Address,
parseIpv6Address,
} from "./shaped_device_wire.mjs";
test("encodes IPv4 hosts and prefixes into byte tuples", () => {
assert.deepEqual(parseIpv4Address("192.168.1.2"), [192, 168, 1, 2]);
assert.deepEqual(parseIpInput("192.168.1.2,10.0.0.0/24", 4), [
[[192, 168, 1, 2], 32],
[[10, 0, 0, 0], 24],
]);
});
test("encodes IPv6 hosts into 16-byte tuples", () => {
assert.deepEqual(parseIpv6Address("2001:db8::1"), [
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
]);
assert.deepEqual(parseIpInput("2001:db8::1/64", 6), [[
[0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
64,
]]);
});
test("encodes IPv4-mapped IPv6 addresses into 16-byte tuples", () => {
assert.deepEqual(parseIpv6Address("::ffff:192.0.2.1"), [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x02, 0x01,
]);
});
test("preserves invalid input as strings so UI validation can reject it", () => {
assert.equal(parseIpv4Address("300.1.1.1"), null);
assert.equal(parseIpv6Address("2001:::1"), null);
assert.deepEqual(parseIpInput("300.1.1.1\n2001:::1", 4), [
["300.1.1.1", 32],
["2001:::1", 32],
]);
});
@@ -10,6 +10,7 @@ import {
updateShapedDevice,
validNodeList,
} from "./config/config_helper";
import { parseIpInput } from "./config/shaped_device_wire.mjs";
let current_rows = [];
let network_json = null;
@@ -187,27 +188,6 @@ function ipListToText(list, defaultPrefix) {
.join("\n");
}
function parseIpInput(text, family) {
if (!text) return [];
const defaultPrefix = family === 6 ? 128 : 32;
const tokens = text.split(/[\n,]+/);
const result = [];
tokens.forEach((token) => {
const trimmed = token.trim();
if (!trimmed) return;
const parts = trimmed.split("/");
const addr = parts[0].trim();
if (!addr) return;
let prefix = defaultPrefix;
if (parts.length > 1 && parts[1].trim().length > 0) {
const parsed = parseInt(parts[1].trim(), 10);
if (!Number.isNaN(parsed)) prefix = parsed;
}
result.push([addr, prefix]);
});
return result;
}
function parseSqmOverride(raw) {
const normalized = (raw || "").toLowerCase();
let down = "";
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v node >/dev/null 2>&1; then
echo "Missing required dependency: node (used only for built-in node_manager contract tests, no node_modules required)." >&2
exit 1
fi
echo "[test-node-manager] Running frontend contract tests..."
node --test "${SCRIPT_DIR}/src/config/shaped_device_wire.test.mjs"
echo "[test-node-manager] Building bundles..."
"${SCRIPT_DIR}/esbuild.sh"
echo "[test-node-manager] Verifying node_manager build contract..."
"${SCRIPT_DIR}/test-build-contract.sh"
+5
View File
@@ -57,6 +57,7 @@ pub async fn spawn_webserver(
.route("/doLogin", post(auth::try_login))
.route("/firstLogin", post(auth::first_user))
.route("/health", get(health_check))
.route("/configuration.html", get(redirect_configuration_page))
// Backwards compatible aliases for historical misspellings.
.route_service(
"/config_spylnx.js",
@@ -92,6 +93,10 @@ async fn redirect_to_index() -> Redirect {
Redirect::permanent("/index.html")
}
async fn redirect_configuration_page() -> Redirect {
Redirect::permanent("/config_devices.html")
}
/// Provides a simple OK status
async fn health_check() -> (StatusCode, &'static str) {
(StatusCode::OK, "OK")
@@ -1,645 +0,0 @@
<div class="row">
<div class="col-12">
<div class="row mb-2">
<div class="col-2">
<h5><i class="fa fa-users"></i> Configuration</h5>
</div>
<div class="col-10">
<a href="#" class="btn btn-sm btn-secondary" id="btnSaveIspConfig"><i class="fa fa-save"></i> Save
Configuration</a>
<button type="button" class="btn btn-sm btn-secondary" id="btnSaveNetDevices"><i
class="fa fa-save"></i> Save network.json and ShapedDevices.csv
</button>
</div>
</div>
<div id="legacyTopologyEditorLockSummary" class="alert alert-secondary d-none" role="alert"></div>
<!-- Ported -->
<div class="d-flex align-items-start">
<div class="nav flex-column nav-pills me-3 bg-body-tertiary" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<button class="nav-link active" id="v-pills-home-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-home" type="button" role="tab" aria-controls="v-pills-home"
aria-selected="true"><i class="fa fa-server"></i> General
</button>
<button class="nav-link" id="v-pills-anon-tab" data-bs-toggle="pill" data-bs-target="#v-pills-anon"
type="button" role="tab" aria-controls="v-pills-profile" aria-selected="false"><i
class="fa fa-user-secret"></i> Anonymous Usage Stats
</button>
<button class="nav-link" id="v-pills-tuning-tab" data-bs-toggle="pill" data-bs-target="#v-pills-tuning"
type="button" role="tab" aria-controls="v-pills-server" aria-selected="false"><i
class="fa fa-warning"></i> Tuning
</button>
<button class="nav-link" id="v-pills-bridge-tab" data-bs-toggle="pill" data-bs-target="#v-pills-bridge"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-chain"></i> Bridge/On-a-Stick
</button>
<button class="nav-link" id="v-pills-queues-tab" data-bs-toggle="pill" data-bs-target="#v-pills-queues"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-car"></i> Queues
</button>
<button class="nav-link" id="v-pills-lts-tab" data-bs-toggle="pill" data-bs-target="#v-pills-lts"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-line-chart"></i> Long-Term Stats
</button>
<button class="nav-link" id="v-pills-iprange-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-iprange" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-address-card"></i> IP Ranges
</button>
<button class="nav-link" id="v-pills-flows-tab" data-bs-toggle="pill" data-bs-target="#v-pills-flows"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-arrow-circle-down"></i> Flow Tracking
</button>
<button class="nav-link" id="v-pills-icommon-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-icommon" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-link"></i> Integration - Common
</button>
<button class="nav-link" id="v-pills-splynx-tab" data-bs-toggle="pill" data-bs-target="#v-pills-splynx"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-link"></i> Splynx
</button>
<button class="nav-link" id="v-pills-netzur-tab" data-bs-toggle="pill" data-bs-target="#v-pills-netzur"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-link"></i> Netzur
</button>
<button class="nav-link" id="v-pills-uisp-tab" data-bs-toggle="pill" data-bs-target="#v-pills-uisp"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-link"></i> UISP
</button>
<button class="nav-link" id="v-pills-powercode-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-powercode" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-link"></i> Powercode
</button>
<button class="nav-link" id="v-pills-sonar-tab" data-bs-toggle="pill" data-bs-target="#v-pills-sonar"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-link"></i> Sonar
</button>
<button class="nav-link" id="v-pills-influxdb-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-influxdb" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-link"></i> InfluxDB
</button>
<button class="nav-link" id="v-pills-netjson-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-netjson" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-map"></i> Network Layout
</button>
<button class="nav-link" id="v-pills-shapeddevs-tab" data-bs-toggle="pill"
data-bs-target="#v-pills-shapeddevs" type="button" role="tab" aria-controls="v-pills-settings"
aria-selected="false"><i class="fa fa-table"></i> Shaped Devices
</button>
<button class="nav-link" id="v-pills-users-tab" data-bs-toggle="pill" data-bs-target="#v-pills-users"
type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i
class="fa fa-users"></i> LibreQoS Users
</button>
</div>
<div class="tab-content" id="v-pills-tabContent">
<!-- General tab -->
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel"
aria-labelledby="v-pills-home-tab">
<h2><i class="fa fa-server"></i> General Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindVersion">Version: </label></td>
<td><input type="text" id="bindVersion" style="background: #ddd"/></td>
</tr>
<tr>
<td><label for="bindPath">Path to LibreQoS: </label></td>
<td><input type="text" id="bindPath" style="width: 500px"/></td>
</tr>
<tr>
<td><label for="bindNodeId">Node ID: </label></td>
<td><input type="text" id="bindNodeId" style="width: 500px"/></td>
</tr>
<tr>
<td><label for="bindNodeName">Node Name: </label></td>
<td><input type="text" id="bindNodeName" style="width: 500px"/></td>
</tr>
<tr>
<td><label for="bindQooProfile">QoO Profile: </label></td>
<td>
<select id="bindQooProfile" style="width: 500px"></select>
<br/>
<small class="text-muted">Used for QoO/QoQ scoring (defaults to Web browsing).</small>
</td>
</tr>
<tr>
<td><label for="bindPacketCaptureTime">Packet Capture Time: </label></td>
<td><input type="number" min="1" max="300" id="bindPacketCaptureTime"/></td>
</tr>
<tr>
<td><label for="bindQueueCheckPeriodMs">Queue Check Period (ms): </label></td>
<td><input type="number" min="10" max="5000" id="bindQueueCheckPeriodMs"/></td>
</tr>
</table>
</div>
<!-- Tuning tab -->
<div class="tab-pane fade" id="v-pills-tuning" role="tabpanel" aria-labelledby="v-pills-tuning-tab">
<h2><i class="fa fa-warning"></i> Tuning Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindStopIrqBalance">Stop IRQ Balancer: </label></td>
<td><input type="checkbox" id="bindStopIrqBalance"/></td>
</tr>
<tr>
<td><label for="bindNetdevBudgetUs">Netdev Budget (usecs): </label></td>
<td><input type="number" id="bindNetdevBudgetUs"/></td>
</tr>
<tr>
<td><label for="bindNetdevBudgetPackets">Netdev Budget (packets): </label></td>
<td><input type="number" id="bindNetdevBudgetPackets"/></td>
</tr>
<tr>
<td><label for="bindRxUs">Rx Period (usecs): </label></td>
<td><input type="number" id="bindRxUs"/></td>
</tr>
<tr>
<td><label for="bindTxUs">Tx Period (usecs): </label></td>
<td><input type="number" id="bindTxUs"/></td>
</tr>
<tr>
<td><label for="bindDisableRxVlan">Disable RxVlan Offload: </label></td>
<td><input type="checkbox" id="bindDisableRxVlan"/></td>
</tr>
<tr>
<td><label for="bindDisableTxVlan">Disable TxVlan Offload: </label></td>
<td><input type="checkbox" id="bindDisableTxVlan"/></td>
</tr>
<tr>
<td><label for="bindDisableOffload">Offloads to Disable: </label></td>
<td><input type="text" id="bindDisableOffload" style="width: 500px"/></td>
</tr>
</table>
</div>
<!-- Bridge/On-a-Stick tab -->
<div class="tab-pane fade" id="v-pills-bridge" role="tabpanel" aria-labelledby="v-pills-bridge-tab">
<h2><i class="fa fa-chain"></i> Bridge Settings</h2>
<h3>Connection Mode: </h3>
<input type="radio" name="bridgeMode" value="bridge" id="radioBridge" onclick="window.setBridgeMode();">
<label for="radioBridge">Bridge Mode</label>
<br/>
<input type="radio" name="bridgeMode" value="onatick" id="radioOnAStick" onclick="window.setStickMode();">
<label for="radioOnAStick">Single-Interface Mode</label>
<br/>
<hr/>
<div id="bridgeMode">
<h3>Bridge Mode</h3>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindUseXdpBridge">Use XDP Enhanced Bridge: </label></td>
<td><input type="checkbox" id="bindUseXdpBridge"/></td>
</tr>
<tr>
<td><label for="bindBridgeToInternet">Network Interface Facing the Internet: </label>
</td>
<td><select id="bindBridgeToInternet"></select></td>
</tr>
<tr>
<td><label for="bindBridgeToNetwork">Network Interface Facing Your Network: </label>
</td>
<td><select id="bindBridgeToNetwork"></select></td>
</tr>
</table>
</div>
<div id="OnAStickMode">
<h3>Single-Interface ("On a Stick") Mode</h3>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSingleInterfaceNic">Network Interface: </label></td>
<td><select id="bindSingleInterfaceNic"></select></td>
</tr>
<tr>
<td><label for="bindSingleInterfaceInternetVlan">Internet VLAN: </label></td>
<td><input type="number" min="1" max="65535" id="bindSingleInterfaceInternetVlan"/></td>
</tr>
<tr>
<td><label for="bindSingleInterfaceNetworklan">Network VLAN: </label></td>
<td><input type="number" min="1" max="65535" id="bindSingleInterfaceNetworklan"/></td>
</tr>
</table>
</div>
</div>
<!-- Queues Tab -->
<div class="tab-pane fade" id="v-pills-queues" role="tabpanel" aria-labelledby="v-pills-queues-tab">
<h2><i class="fa car"></i> Queue Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSqm">SQM Mode: </label></td>
<td>
<select id="bindSqm">
<option value="cake diffserv4">Cake with Diffserv4</option>
<option value="cake diffserv4 ack-filter">Cake with Diffserv4 + Duplicate ACK
Filtering
</option>
</select>
</td>
</tr>
<tr>
<td><label for="bindQueueMode">Queue Mode: </label></td>
<td>
<select id="bindQueueMode">
<option value="shape">Shape</option>
<option value="observe">Observe</option>
</select>
<br/>
<small class="text-muted">Observe removes the LibreQoS shaping tree so you can gather a true baseline. Changing modes requires a reload and may briefly interrupt traffic.</small>
</td>
</tr>
<tr>
<td><label for="bindUplinkMbps">Uplink Bandwidth (Mbps): </label></td>
<td><input type="number" id="bindUplinkMbps"/></td>
</tr>
<tr>
<td><label for="bindDownlinkMbps">Downlink Bandwidth (Mbps): </label></td>
<td><input type="number" id="bindDownlinkMbps"/></td>
</tr>
<tr>
<td><label for="bindGeneratedDownlinkMbps">Generated PN Download Bandwidth (Mbps): </label>
</td>
<td><input type="number" id="bindGeneratedDownlinkMbps"/></td>
</tr>
<tr>
<td><label for="bindGeneratedUplinkMbps">Generated PN Upload Bandwidth (Mbps): </label></td>
<td><input type="number" id="bindGeneratedUplinkMbps"/></td>
</tr>
<tr>
<td><label for="bindDryRun">Dry Run (Don't Make Queues): </label></td>
<td><input type="checkbox" id="bindDryRun"/></td>
</tr>
<tr>
<td><label for="bindSudo">Use Sudo: </label></td>
<td><input type="checkbox" id="bindSudo"/></td>
</tr>
<tr>
<td><label for="bindBinpack">Use Binpacking to equalize CPU usage: </label></td>
<td><input type="checkbox" id="bindBinpack"/></td>
</tr>
<tr>
<td><label for="bindQueuesAvailableOverride">Queues Available Override: </label></td>
<td><input type="number" id="bindQueuesAvailableOverride"/></td>
</tr>
</table>
</div>
<!-- Long-Term Stats Tab -->
<div class="tab-pane fade" id="v-pills-lts" role="tabpanel" aria-labelledby="v-pills-lts-tab">
<h2><i class="fa fa-line-chart"></i> Insight Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindEnableLTS">Send Statistics to Insight: </label></td>
<td><input type="checkbox" id="bindEnableLTS"/></td>
</tr>
<tr>
<td><label for="bindLtsCollation">Collation Period (seconds): </label></td>
<td><input type="number" id="bindLtsCollation"/></td>
</tr>
<tr>
<td><label for="bindLtsLicense">License Key: </label></td>
<td><input type="text" id="bindLtsLicense" style="width: 500px"/></td>
</tr>
<tr>
<td><label for="bindLtsUispInterval">UISP Reporting Interval (Seconds): </label></td>
<td><input type="number" id="bindLtsUispInterval"/></td>
</tr>
</table>
</div>
<!-- IP Ranges Stats Tab -->
<div class="tab-pane fade" id="v-pills-iprange" role="tabpanel" aria-labelledby="v-pills-iprange-tab">
<h2><i class="fa fa-address-card"></i> IP Ranges Settings</h2>
<table>
<tr>
<td><label for="bindIgnoreRanges">IP Subnets to Ignore</label></td>
<td>
<em>Enter one IP range per Line</em><br/>
<textarea id="bindIgnoreRanges" rows="10" cols="40"></textarea>
</td>
</tr>
<tr>
<td><label for="bindAllowRanges">IP Ranges to Allow</label></td>
<td>
<em>Enter one IP range per Line</em><br/>
<textarea id="bindAllowRanges" rows="10" cols="40"></textarea>
</td>
</tr>
</table>
</div>
<!-- Flows Tab -->
<div class="tab-pane fade" id="v-pills-flows" role="tabpanel" aria-labelledby="v-pills-flow-tab">
<h2><i class="fa fa-arrow-circle-down"></i> Flows Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindFlowsTimeout">Flow Timeout (Seconds): </label></td>
<td><input type="number" id="bindFlowsTimeout"/></td>
</tr>
<tr>
<td><label for="bindEnableNetflow">Enable Netflow Collection: </label></td>
<td><input type="checkbox" id="bindEnableNetflow"/></td>
</tr>
<tr>
<td><label for="bindFlowsPort">Netflow Port Number: </label></td>
<td><input type="number" id="bindFlowsPort"/></td>
</tr>
<tr>
<td><label for="bindFlowsTarget">Send Netflow to IP: </label></td>
<td><input type="text" id="bindFlowsTarget"/></td>
</tr>
<tr>
<td><label for="bindFlowsVersion">Netflow Version: </label></td>
<td>
<select id="bindFlowsVersion">
<option value="5">Netflow v5 - fast, but IPv4 only</option>
<option value="9">Netflow v6/IPFIX</option>
</select>
</td>
</tr>
</table>
</div>
<!-- Integration Common Tab -->
<div class="tab-pane fade" id="v-pills-icommon" role="tabpanel" aria-labelledby="v-pills-icommon-tab">
<h2><i class="fa fa-link"></i> Common Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindCircuitNameAsAddress">Use Circuit Name as Address: </label></td>
<td><input type="checkbox" id="bindCircuitNameAsAddress"/></td>
</tr>
<tr>
<td><label for="bindOverwriteNetJson">Always Overwrite Network.json: </label></td>
<td><input type="checkbox" id="bindOverwriteNetJson"/></td>
</tr>
<tr>
<td><label for="bindIntegrationMikrotik">Enrich IPv6 via Mikrotik: </label></td>
<td><input type="checkbox" id="bindIntegrationMikrotik"/></td>
</tr>
<tr>
<td><label for="bindQueueRefreshInterval">Queue Refresh Interval (Minutes): </label></td>
<td><input type="number" id="bindQueueRefreshInterval"/></td>
</tr>
</table>
</div>
<!-- Splynx Tab -->
<div class="tab-pane fade" id="v-pills-splynx" role="tabpanel" aria-labelledby="v-pills-splynx-tab">
<h2><i class="fa fa-link"></i> Splynx Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSplynxEnable">Enable Splynx Integration: </label></td>
<td><input type="checkbox" id="bindSplynxEnable"/></td>
</tr>
<tr>
<td><label for="bindSplynxApiKey">API Key: </label></td>
<td><input type="text" id="bindSplynxApiKey"/></td>
</tr>
<tr>
<td><label for="bindSplynxApiSecret">API Secret: </label></td>
<td><input type="text" id="bindSplynxApiSecret"/></td>
</tr>
<tr>
<td><label for="bindSplynxApiUrl">API URL: </label></td>
<td><input type="text" id="bindSplynxApiUrl"/></td>
</tr>
</table>
</div>
<!-- Netzur Tab -->
<div class="tab-pane fade" id="v-pills-netzur" role="tabpanel" aria-labelledby="v-pills-netzur-tab">
<h2><i class="fa fa-link"></i> Netzur Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindNetzurEnable">Enable Netzur Integration: </label></td>
<td><input type="checkbox" id="bindNetzurEnable"/></td>
</tr>
<tr>
<td><label for="bindNetzurApiKey">API Key: </label></td>
<td><input type="text" id="bindNetzurApiKey"/></td>
</tr>
<tr>
<td><label for="bindNetzurApiUrl">API URL: </label></td>
<td><input type="text" id="bindNetzurApiUrl"/></td>
</tr>
<tr>
<td><label for="bindNetzurTimeout">Request Timeout (seconds): </label></td>
<td><input type="number" min="15" id="bindNetzurTimeout"/></td>
</tr>
</table>
</div>
<!-- UISP Tab -->
<div class="tab-pane fade" id="v-pills-uisp" role="tabpanel" aria-labelledby="v-pills-uisp-tab">
<h2><i class="fa fa-link"></i> UISP Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindUispEnable">Enable UISP Integration: </label></td>
<td><input type="checkbox" id="bindUispEnable"/></td>
</tr>
<tr>
<td><label for="bindUispToken">UISP Token: </label></td>
<td><input type="text" id="bindUispToken"/></td>
</tr>
<tr>
<td><label for="bindUispUrl">UISP URL: </label></td>
<td><input type="text" id="bindUispUrl"/></td>
</tr>
<tr>
<td><label for="bindUispSite">Root Site Name (for 'full' integrations): </label></td>
<td><input type="text" id="bindUispSite"/></td>
</tr>
<tr>
<td><label for="bindUispStrategy">Generation Strategy: </label></td>
<td>
<select id="bindUispStrategy">
<option value="flat">Flat: no topology</option>
<option value="full">Full: construct a full hierarchy</option>
</select>
</td>
</tr>
<tr>
<td><label for="bindUispSuspended">Suspended Customer Strategy: </label></td>
<td>
<select id="bindUispSuspended">
<option value="none">Do Nothing</option>
<option value="slow">Apply a Slow Speed</option>
</select>
</td>
</tr>
<tr>
<td><label for="bindUispAirmaxCapacity">Airmax Capacity Multiplier: </label></td>
<td><input type="text" id="bindUispAirmaxCapacity"/></td>
</tr>
<tr>
<td><label for="bindUispLtuCapacity">LTU Capacity Multiplier: </label></td>
<td><input type="text" id="bindUispLtuCapacity"/></td>
</tr>
<tr>
<td><label for="bindUispIpv6Mikrotik">Query Mikrotiks for IPv6 Information: </label></td>
<td><input type="checkbox" id="bindUispIpv6Mikrotik"/></td>
</tr>
<tr>
<td><label for="bindUispOverheadFactor">Bandwidth Overhead Factor: </label></td>
<td><input type="text" id="bindUispOverheadFactor"/></td>
</tr>
<tr>
<td><label for="bindUispCommitMultiplier">Commit Bandwidth Multiplier: </label></td>
<td><input type="text" id="bindUispCommitMultiplier"/></td>
</tr>
<tr>
<td><label for="bindUispExcludeSites">Exclude Sites: </label></td>
<td><input type="text" id="bindUispExcludeSites"/></td>
</tr>
<tr>
<td><label for="bindUispExceptionCpes">Exception CPEs: </label></td>
<td><input type="text" id="bindUispExceptionCpes"/></td>
</tr>
<tr>
<td><label for="bindUispUsePtmpAsParent">Use PTMP As Parent: </label></td>
<td><input type="checkbox" id="bindUispUsePtmpAsParent"/></td>
</tr>
<tr>
<td><label for="bindUispIgnoreCalculatedCapacity">Ignore Generated Capacity: </label></td>
<td><input type="checkbox" id="bindUispIgnoreCalculatedCapacity"/></td>
</tr>
</table>
</div>
<!-- Powercode Tab -->
<div class="tab-pane fade" id="v-pills-powercode" role="tabpanel"
aria-labelledby="v-pills-powercode-tab">
<h2><i class="fa fa-link"></i> Powercode Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindPowercodeEnable">Enable Powercode Integration: </label></td>
<td><input type="checkbox" id="bindPowercodeEnable"/></td>
</tr>
<tr>
<td><label for="bindPowercodeKey">Powercode API Key: </label></td>
<td><input type="text" id="bindPowercodeKey"/></td>
</tr>
<tr>
<td><label for="bindPowercodeUrl">Powercode API URL: </label></td>
<td><input type="text" id="bindPowercodeUrl"/></td>
</tr>
</table>
</div>
<!-- Sonar Tab -->
<div class="tab-pane fade" id="v-pills-sonar" role="tabpanel" aria-labelledby="v-pills-sonar-tab">
<h2><i class="fa fa-link"></i> Sonar Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSonarEnable">Enable Sonar Integration: </label></td>
<td><input type="checkbox" id="bindSonarEnable"/></td>
</tr>
<tr>
<td><label for="bindSonarApiKey">Sonar API Key: </label></td>
<td><input type="text" id="bindSonarApiKey"/></td>
</tr>
<tr>
<td><label for="bindSonarApiUrl">Sonar API URL: </label></td>
<td><input type="text" id="bindSonarApiUrl"/></td>
</tr>
<tr>
<td><label for="bindSonarSnmp">Sonar SNMP Community: </label></td>
<td><input type="text" id="bindSonarSnmp"/></td>
</tr>
<tr>
<td><label for="bindSonarAirmax">Sonar Airmax Model IDs: </label></td>
<td><input type="text" id="bindSonarAirmax"/></td>
</tr>
<tr>
<td><label for="bindSonarLtu">Sonar LTU Model IDs: </label></td>
<td><input type="text" id="bindSonarLtu"/></td>
</tr>
<tr>
<td><label for="bindSonarActive">Sonar Active Status IDs: </label></td>
<td><input type="text" id="bindSonarActive"/></td>
</tr>
</table>
</div>
<!-- InfluxDB Tab -->
<div class="tab-pane fade" id="v-pills-influxdb" role="tabpanel" aria-labelledby="v-pills-influxdb-tab">
<h2><i class="fa fa-link"></i> InfluxDB Integration Settings</h2>
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindInfluxEnable">Enable InfluxDB Integration: </label></td>
<td><input type="checkbox" id="bindInfluxEnable"/></td>
</tr>
<tr>
<td><label for="bindInfluxUrl">InfluxDB URL: </label></td>
<td><input type="text" id="bindInfluxUrl"/></td>
</tr>
<tr>
<td><label for="bindInfluxOrg">InfluxDB Organization: </label></td>
<td><input type="text" id="bindInfluxOrg"/></td>
</tr>
<tr>
<td><label for="bindInfluxBucket">InfluxDB Bucket: </label></td>
<td><input type="text" id="bindInfluxBucket"/></td>
</tr>
<tr>
<td><label for="bindInfluxToken">InfluxDB Token: </label></td>
<td><input type="text" id="bindInfluxToken"/></td>
</tr>
</table>
</div>
<!-- Network Layout/JSON Tab -->
<div class="tab-pane fade" id="v-pills-netjson" role="tabpanel" aria-labelledby="v-pills-netjson-tab">
<h2><i class="fa fa-map"></i> Network.Json - Network Layout</h2>
<div id="legacyNetworkEditorLock" class="alert alert-secondary d-none" role="alert"></div>
<h3>Network Adjustment Options</h3>
<div class="row">
<div class="col-3">
<button type="button" class="btn btn-outline-danger" id="btnFlattenNetwork"><i
class="fa fa-warning"></i> Flatten Network
</button>
</div>
<div style="margin-top: 8px;" class="col-3">
<input type="text" value="" placeholder="New Node Name" id="njsNewNodeName"/> <br/>
<input type="number" min="1" max="100000" placeholder="Max Download" id="njsNewNodeDown"/> <br/>
<input type="number" min="1" max="100000" placeholder="Max Upload" id="njsNewNodeUp"/> <br/>
<button type="button" class="btn btn-outline-success" id="btnAddNetworkNode"><i
class="fa fa-plus"></i> Add Node to Root
</button>
</div>
</div>
<hr/>
<div id="netjson"></div>
</div>
<!-- ShapedDevices.csv Tab -->
<div class="tab-pane fade" id="v-pills-shapeddevs" role="tabpanel"
aria-labelledby="v-pills-shapeddevs-tab">
<h2><i class="fa table"></i> Shaped Devices</h2>
<div id="legacyDevicesEditorLock" class="alert alert-secondary d-none" role="alert"></div>
<p>All shaped device rows currently known to LibreQoS.</p>
<button type="button" class="btn btn-sm btn-secondary" id="btnAddShapedDeviceRow" onclick="window.newSdRow()" disabled><i class="fa fa-plus"></i>
Add Row
</button>
<div id="shapedDeviceTable"></div>
</div>
<div class="tab-pane fade" id="v-pills-users" role="tabpanel" aria-labelledby="v-pills-users-tab">
<h2><i class="fa fa-users"></i> LibreQos Web Interface Users</h2>
<div id="userManager"></div>
</div>
</div>
</div>
<!-- End Ported -->
</div>
</div>
<script src="configuration.js%CACHEBUSTERS%"></script>
@@ -34,7 +34,6 @@ pub(super) fn static_routes() -> Result<Router> {
"tree.html",
"help.html",
"unknown_ips.html",
"configuration.html",
"circuit.html",
"ethernet_caps.html",
"site_map.html",
+150
View File
@@ -2004,3 +2004,153 @@ fn payload_hint(payload: &[u8]) -> &'static str {
_ => "binary",
}
}
#[cfg(test)]
mod tests {
use super::decode_ws_request;
use crate::node_manager::ws::messages::WsRequest;
use serde_cbor::Value as CborValue;
use std::collections::BTreeMap;
fn text(value: &str) -> CborValue {
CborValue::Text(value.to_string())
}
fn integer(value: i128) -> CborValue {
CborValue::Integer(value)
}
fn float(value: f64) -> CborValue {
CborValue::Float(value)
}
fn array(items: Vec<CborValue>) -> CborValue {
CborValue::Array(items)
}
fn map(entries: Vec<(&str, CborValue)>) -> CborValue {
let mut out = BTreeMap::new();
for (key, value) in entries {
out.insert(text(key), value);
}
CborValue::Map(out)
}
fn device_value(ipv4: CborValue, ipv6: CborValue) -> CborValue {
map(vec![
("circuit_id", text("circuit-1")),
("circuit_name", text("Circuit 1")),
("device_id", text("device-1")),
("device_name", text("Device 1")),
("parent_node", text("tower-a")),
("mac", text("")),
("ipv4", ipv4),
("ipv6", ipv6),
("download_min_mbps", float(100.0)),
("upload_min_mbps", float(100.0)),
("download_max_mbps", float(200.0)),
("upload_max_mbps", float(200.0)),
("comment", text("")),
])
}
fn create_shaped_device_payload(device: CborValue) -> Vec<u8> {
serde_cbor::to_vec(&map(vec![(
"CreateShapedDevice",
map(vec![("device", device)]),
)]))
.expect("valid create payload")
}
fn update_shaped_device_payload(device: CborValue) -> Vec<u8> {
serde_cbor::to_vec(&map(vec![(
"UpdateShapedDevice",
map(vec![
("original_device_id", text("device-1")),
("device", device),
]),
)]))
.expect("valid update payload")
}
#[test]
fn decodes_create_shaped_device_with_browser_style_ipv4_bytes() {
let payload = create_shaped_device_payload(device_value(
array(vec![array(vec![
array(vec![
integer(192),
integer(168),
integer(1),
integer(2),
]),
integer(32),
])]),
array(vec![]),
));
let request = decode_ws_request(&payload).expect("create request should decode");
match request {
WsRequest::CreateShapedDevice { device } => {
assert_eq!(device.device_id, "device-1");
assert_eq!(device.ipv4.len(), 1);
assert_eq!(device.ipv4[0].0.octets(), [192, 168, 1, 2]);
assert_eq!(device.ipv4[0].1, 32);
}
other => panic!("unexpected request: {other:?}"),
}
}
#[test]
fn decodes_update_shaped_device_with_browser_style_ipv6_bytes() {
let payload = update_shaped_device_payload(device_value(
array(vec![]),
array(vec![array(vec![
array(vec![
integer(0x20),
integer(0x01),
integer(0x0d),
integer(0xb8),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(0),
integer(1),
]),
integer(128),
])]),
));
let request = decode_ws_request(&payload).expect("update request should decode");
match request {
WsRequest::UpdateShapedDevice {
original_device_id,
device,
} => {
assert_eq!(original_device_id, "device-1");
assert_eq!(device.ipv6.len(), 1);
assert_eq!(device.ipv6[0].0.to_string(), "2001:db8::1");
assert_eq!(device.ipv6[0].1, 128);
}
other => panic!("unexpected request: {other:?}"),
}
}
#[test]
fn rejects_legacy_string_encoded_ipv4_payloads() {
let payload = create_shaped_device_payload(device_value(
array(vec![array(vec![text("192.168.1.2"), integer(32)])]),
array(vec![]),
));
let err = decode_ws_request(&payload).expect_err("legacy payload must fail");
assert!(err.contains("expected an array of length 4"));
assert!(err.contains("192.168.1.2"));
}
}